在PostgreSQL中执行此小时的操作查询

我在RoR堆栈中,我必须编写一些实际的SQL来完成所有“打开”记录的查询,这意味着当前时间在指定的操作时间内。 在hours_of_operations表中,两个integeropens_oncloses_on存储工作日,两个time字段opens_atcloses_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  5) OR ((closes_on = opens_on) 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  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)) ) ) 

这种密集复杂性的原因在于,一小时的操作可以在一周结束时进行,例​​如,从周日中午开始到周一早上6点。 由于我以UTC格式存储值,因此在很多情况下用户的本地时间可以以非常奇怪的方式进行换行。 上面的查询确保您可以在一周中输入任意两次,并且我们会补偿包装。

表格布局

重新设计表并将开放时间(操作小时数)存储为一组tsrange (无时区范围的时间戳)值。 需要Postgres 9.2或更高版本

选择一个随机的周来开始你的营业时间。 我喜欢这一周:
1996-01-01(星期一)1996-01-07(星期日)
这是最近的闰年,1月1日恰好是星期一。 但对于这种情况,它可以是任何随机周。 只是保持一致。

首先安装附加模块btree_gist为什么?

 CREATE EXTENSION btree_gist; 

像这样创建表:

 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]') ); 

hours取代了所有列:

 opens_on, closes_on, opens_at, closes_at 

例如,从星期三,18:30星期四,0500 UTC的营业时间输入为:

 '[1996-01-03 18:30, 1996-01-04 05:00]' 

排除约束hoo_no_overlap可防止每个商店重叠条目。 它使用GiST索引实现 ,这也恰好支持您的查询。 请考虑下面的“索引与绩效”一章讨论索引策略。

检查约束hoo_bounds_inclusive强制执行范围的包含边界,具有两个值得注意的后果:

  • 始终包括精确落在下边界或上边界的时间点。
  • 实际上不允许同一商店的相邻条目。 对于包容性边界,这些将“重叠”,排除约束将引发exception。 相邻的条目必须合并为一行。 除非它们在周日午夜时分缠绕 ,在这种情况下它们必须分成两排。 见下面的工具2

检查约束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) 

您必须一次搜索两个时间戳。 这是一个独特上限的相关案例,不会出现这个缺点:

  • 使用PostgreSQL中的EXCLUDE防止相邻/重叠的条目

函数f_hoo_time(timestamptz)

timestamp with time zone “规范化”任何给定的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; 

该函数采用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; 

INSERT 单个输入行:

 INSERT INTO hoo(shop_id, hours) SELECT 123, f_hoo_hours('2016-01-11 00:00+04', '2016-01-11 08:00+04'); 

如果范围需要在星期一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 

关于隐式LATERAL连接:

  • LATERAL和PostgreSQL中的子查询有什么区别?

询问

通过调整后的设计, 您的整个庞大,复杂,昂贵的查询可以替换为...:

SELECT *
FROM hoo
WHERE hours @> f_hoo_time(now());

为了一点暂停,我在解决方案上放了一个扰流板。 鼠标移到它上面。

该查询由所述GiST索引支持并且速度很快,即使对于大表也是如此。

SQL Fiddle (有更多示例)。

如果你想计算总营业时间(每家商店),这是一个食谱:

  • 计算PostgreSQL中两个日期之间的工作时间

指数和表现

可以使用GiST或SP-GiST索引支持范围类型的包含运算符 。 两者都可用于实现排除约束,但只有GiST支持多列索引 :

目前,只有B树,GiST,GIN和BRIN索引类型支持多列索引。

并且索引列的顺序很重要 :

多列GiST索引可以与涉及索引列的任何子集的查询条件一起使用。 其他列的条件限制索引返回的条目,但第一列的条件是确定需要扫描多少索引的最重要条件。 如果GiST索引的第一列只有几个不同的值,即使其他列中有许多不同的值,它也会相对无效。

所以我们在这里有利益冲突 。 对于大表, shop_id将有更多不同的值而不是hours

  • 具有前导shop_id GiST索引编写速度更快,并强制执行排除约束。
  • 但是我们在查询中搜索hours列。 首先拥有该列会更好。
  • 如果我们需要在其他查询中查找shop_id ,那么普通的btree索引要快得多。
  • 最重要的是,我发现SP-GiST索引在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; 

结果是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 

shop_id首先快shop_id倍。

另外,我测试了两个以上的读取性能:

 CREATE INDEX hoo_hours_gist_idx on hoo USING gist (hours); CREATE INDEX hoo_hours_spgist_idx on hoo USING spgist (hours); -- !! 

VACUUM FULL ANALYZE hoo; ,我跑了两个问题:

  • Q1 :深夜,只找到53排
  • Q2 :下午,找到2423排

结果

每个都有一个仅索引扫描 (当然除了“无索引”):

 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 -- !! 
  • 对于查找结果很少的查询,SP-GiST和GiST是相同的(对于少数人来说,GiST甚至更快)。
  • SP-GiST随着越来越多的结果而更好地扩展,并且也更小。

如果您阅读的内容比编写的要多得多(典型用例),请按照开头的建议保留排除约束,并创建一个额外的SP-GiST索引以优化读取性能。