在PostgreSQL中执行此小时的操作查询
我在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 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到星期四,05 : 00 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索引以优化读取性能。
- 使用Rails 5在Heroku,PostgreSQL上运行迁移时出错
- 我的应用程序访问远程数据库。 如何有效地运行unit testing?
- 如何在双边市场中构建关系表
- Heroku – ActionView :: Template :: Error(PG ::错误:错误:列category_products.desc不存在
- rails 3.2.2(或3.2.1)+ Postgresql 9.1.3 + Ubuntu 11.10连接错误
- 当ID存在时,获取“表的未知主键”
- Postgres随机停止工作(Rails,PGSQL.5432)
- Rails和postgres – 在heroku上部署时忽略了pg gem
- Rails 3.1。 Heroku PGError:运算符不存在:字符变化=整数