One*_*ude 11 sql postgresql ruby-on-rails constraints range-types
我在RoR堆栈中,我必须编写一些实际的SQL来完成所有"打开"记录的查询,这意味着当前时间在指定的操作时间内.在hours_of_operations
表中有两integer
列opens_on
并closes_on
存储一个工作日,以及两个time
字段opens_at
并closes_at
存储当天的相应时间.
我做了一个查询,将当前日期和时间与存储的值进行比较,但我想知道是否有一种方法可以转换为某种日期类型并让PostgreSQL完成剩下的工作?
查询的内容是:
WHERE (
(
/* Opens in Future */
(opens_on > 5 OR (opens_on = 5 AND opens_at::time > '2014-03-01 00:27:25.851655'))
AND (
(closes_on < opens_on AND closes_on > 5)
OR ((closes_on = opens_on)
AND (closes_at::time < opens_at::time AND closes_at::time > '2014-03-01 00:27:25.851655'))
OR ((closes_on = 5)
AND (closes_at::time > '2014-03-01 00:27:25.851655' AND closes_at::time < opens_at::time)))
OR
/* Opens in Past */
(opens_on < 5 OR (opens_on = 5 AND opens_at::time < '2014-03-01 00:27:25.851655'))
AND
(closes_on > 5)
OR
((closes_on = 5)
AND (closes_at::time > '2014-03-01 00:27:25.851655'))
OR (closes_on < opens_on)
OR ((closes_on = opens_on)
AND (closes_at::time < opens_at::time))
)
)
Run Code Online (Sandbox Code Playgroud)
这种密集复杂性的原因在于,一小时的操作可以在一周结束时进行,例如,从星期日中午开始到星期一早上6点.由于我以UTC格式存储值,因此很多情况下用户的本地时间可以以非常奇怪的方式进行换行.上面的查询确保您可以在一周中输入任意两次,并且我们会补偿包装.
Erw*_*ter 24
重新设计表并将开放时间(操作小时数)存储为一组tsrange
(没有时区的时间戳范围)值.需要Postgres 9.2或更高版本.
选择一个随机周来开始营业时间.我喜欢这一周:
1996-01-01(星期一)到1996-01-07(星期日)
这是最近的闰年,1月1日恰好是星期一.但对于这种情况,它可以是任何随机周.只是保持一致.
首先安装附加模块btree_gist
.为什么?
CREATE EXTENSION btree_gist;
Run Code Online (Sandbox Code Playgroud)
像这样创建表:
CREATE TABLE hoo (
hoo_id serial PRIMARY KEY
, shop_id int NOT NULL REFERENCES shop(shop_id) -- reference to shop
, hours tsrange NOT NULL
, CONSTRAINT hoo_no_overlap EXCLUDE USING gist (shop_id with =, hours WITH &&)
, CONSTRAINT hoo_bounds_inclusive CHECK (lower_inc(hours) AND upper_inc(hours))
, CONSTRAINT hoo_standard_week CHECK (hours <@ tsrange '[1996-01-01 0:0, 1996-01-08 0:0]')
);
Run Code Online (Sandbox Code Playgroud)
在一个列hours
替换所有列:
opens_on, closes_on, opens_at, closes_at
Run Code Online (Sandbox Code Playgroud)
例如,从星期三,18:30到星期四,05:00 UTC的营业时间输入为:
'[1996-01-03 18:30, 1996-01-04 05:00]'
Run Code Online (Sandbox Code Playgroud)
排除约束hoo_no_overlap
可防止每个商店重叠条目.它使用GiST索引实现,这也恰好支持您的查询.请考虑下面的"索引与绩效"一章,讨论索引策略.
检查约束hoo_bounds_inclusive
强制执行范围的包含边界,具有两个值得注意的后果:
检查约束hoo_standard_week
使用"范围包含"运算符<@
强制执行分段周的外部边界.
在包含边界的情况下,您必须观察周日午夜时间周围的特殊/角落情况:
'1996-01-01 00:00+0' = '1996-01-08 00:00+0'
Mon 00:00 = Sun 24:00 (= next Mon 00:00)
Run Code Online (Sandbox Code Playgroud)
您必须一次搜索两个时间戳.这是一个独特上限的相关案例,不会出现这个缺点:
f_hoo_time(timestamptz)
要"规范化"任何给定的timestamp with time zone
:
CREATE OR REPLACE FUNCTION f_hoo_time(timestamptz)
RETURNS timestamp AS
$func$
SELECT date '1996-01-01'
+ ($1 AT TIME ZONE 'UTC' - date_trunc('week', $1 AT TIME ZONE 'UTC'))
$func$ LANGUAGE sql IMMUTABLE;
Run Code Online (Sandbox Code Playgroud)
该函数接受timestamptz
并返回timestamp
.它将($1 - date_trunc('week', $1)
UTC时间(!)中相应周的经过时间间隔添加到我们的分段周的起始点.(date
+ interval
产生timestamp
.)
f_hoo_hours(timestamptz, timestamptz)
规范化范围并分割那些穿越星期一00:00.此函数采用任何间隔(为两个timestamptz
)并生成一个或两个标准化tsrange
值.它涵盖了任何法律意见,并且不允许其他内容:
CREATE OR REPLACE FUNCTION f_hoo_hours(_from timestamptz, _to timestamptz)
RETURNS TABLE (hoo_hours tsrange) AS
$func$
DECLARE
ts_from timestamp := f_hoo_time(_from);
ts_to timestamp := f_hoo_time(_to);
BEGIN
-- test input for sanity (optional)
IF _to <= _from THEN
RAISE EXCEPTION '%', '_to must be later than _from!';
ELSIF _to > _from + interval '1 week' THEN
RAISE EXCEPTION '%', 'Interval cannot span more than a week!';
END IF;
IF ts_from > ts_to THEN -- split range at Mon 00:00
RETURN QUERY
VALUES (tsrange('1996-01-01 0:0', ts_to , '[]'))
, (tsrange(ts_from, '1996-01-08 0:0', '[]'));
ELSE -- simple case: range in standard week
hoo_hours := tsrange(ts_from, ts_to, '[]');
RETURN NEXT;
END IF;
RETURN;
END
$func$ LANGUAGE plpgsql IMMUTABLE COST 1000 ROWS 1;
Run Code Online (Sandbox Code Playgroud)
以INSERT
一个单一的输入行:
INSERT INTO hoo(shop_id, hours)
SELECT 123, f_hoo_hours('2016-01-11 00:00+04', '2016-01-11 08:00+04');
Run Code Online (Sandbox Code Playgroud)
如果范围需要在星期一00:00分割,则会产生两行.
要INSERT
多个输入行:
INSERT INTO hoo(shop_id, hours)
SELECT id, hours
FROM (
VALUES (7, timestamp '2016-01-11 00:00', timestamp '2016-01-11 08:00')
, (8, '2016-01-11 00:00', '2016-01-11 08:00')
) t(id, f, t), f_hoo_hours(f, t) hours; -- LATERAL join
Run Code Online (Sandbox Code Playgroud)
关于隐式LATERAL
连接:
通过调整后的设计,您的整个庞大,复杂,昂贵的查询可以替换为...:
SELECT *
FROM hoo
WHERE hours @> f_hoo_time(now());
为了一点悬念,我在解决方案上放了一个扰流板.将鼠标移到它上面.
该查询由所述GiST索引支持并且速度很快,即使对于大表也是如此.
SQL Fiddle(有更多示例).
如果你想计算总营业时间(每家商店),这里有一个配方:
可以使用GiST或SP-GiST索引支持范围类型的包含运算符.两者都可用于实现排除约束,但只有GiST支持多列索引:
目前,只有B树,GiST,GIN和BRIN索引类型支持多列索引.
多列GiST索引可以与涉及索引列的任何子集的查询条件一起使用.其他列的条件限制索引返回的条目,但第一列的条件是确定需要扫描多少索引的最重要条件.如果GiST索引的第一列只有几个不同的值,即使其他列中有许多不同的值,它也会相对无效.
所以我们在这里有利益冲突.对于大表,将会有更多的不同的值shop_id
比hours
.
shop_id
编写和执行排除约束的速度更快.hours
在查询中搜索该列.首先拥有该列会更好.shop_id
查询其他查询,那么普通的btree索引要快得多.hours
是最快的查询.我的脚本生成虚拟数据:
INSERT INTO hoo(shop_id, hours)
SELECT id, hours
FROM generate_series(1, 30000) id, generate_series(0, 6) d
, f_hoo_hours(((date '1996-01-01' + d) + interval '4h' + interval '15 min' * trunc(32 * random())) AT TIME ZONE 'UTC'
, ((date '1996-01-01' + d) + interval '12h' + interval '15 min' * trunc(64 * random() * random())) AT TIME ZONE 'UTC') AS hours
WHERE random() > .33;
Run Code Online (Sandbox Code Playgroud)
结果在141k随机生成的行,30k不同shop_id
,12k不同hours
.(通常差异会更大.)表大小为8 MB.
我删除并重新创建了排除约束:
ALTER TABLE hoo ADD CONSTRAINT hoo_no_overlap
EXCLUDE USING gist (shop_id WITH =, hours WITH &&); -- 4.4 sec !!
ALTER TABLE hoo ADD CONSTRAINT hoo_no_overlap
EXCLUDE USING gist (hours WITH &&, shop_id WITH =); -- 16.4 sec
Run Code Online (Sandbox Code Playgroud)
shop_id
首先是快4倍.
另外,我测试了两个以上的读取性能:
CREATE INDEX hoo_hours_gist_idx on hoo USING gist (hours);
CREATE INDEX hoo_hours_spgist_idx on hoo USING spgist (hours); -- !!
Run Code Online (Sandbox Code Playgroud)
之后VACUUM FULL ANALYZE hoo;
,我跑了两个问题:
每个都有一个仅索引扫描(当然除了"无索引"):
index idx size Q1 Q2
------------------------------------------------
no index 41.24 ms 41.2 ms
gist (shop_id, hours) 8MB 14.71 ms 33.3 ms
gist (hours, shop_id) 12MB 0.37 ms 8.2 ms
gist (hours) 11MB 0.34 ms 5.1 ms
spgist (hours) 9MB 0.29 ms 2.0 ms -- !!
Run Code Online (Sandbox Code Playgroud)
如果您阅读的内容比编写的要多得多(典型用例),请按照开头的建议保留排除约束,并创建一个额外的SP-GiST索引以优化读取性能.
归档时间: |
|
查看次数: |
2864 次 |
最近记录: |