我在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))
)
)
这种密集复杂性的原因在于,一小时的操作可能会在本周末结束,例如,从周日中午开始到周一早上6点。由于我以UTC格式存储值,因此很多情况下用户的本地时间可以以非常奇怪的方式进行换行。上面的查询确保您可以在一周中输入任意两次,并且我们会补偿包装。
答案 0 :(得分:18)
重新设计表格并将营业时间(营业时间)存储为一组tsrange
(range of timestamp without time zone)值。需要Postgres 9.2或更高版本。
选择一个随机周来开始营业时间。我喜欢这一周:
1996-01-01(星期一)至 1996-01-07(星期日)
这是最近的闰年,1月1日恰好是星期一。但对于这种情况,它可以是任何随机周。只是保持一致。
首先安装附加模块btree_gist
。 Why?
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索引实现,这也恰好支持您的查询。考虑以下&#34; Index and Performance&#34; 这一章讨论索引策略。
检查约束 hoo_bounds_inclusive
强制执行范围的包含边界,并带来两个值得注意的后果:
检查约束 hoo_standard_week
使用"range is contained by" operator <@
强制执行分段周的外部边界。
使用 包含 边界,你必须观察一个特殊/角落的情况,时间周日午夜时分:
'1996-01-01 00:00+0' = '1996-01-08 00:00+0'
Mon 00:00 = Sun 24:00 (= next Mon 00:00)
您必须一次搜索两个时间戳。以下是 独占 上限的相关案例,不会出现此缺点:
f_hoo_time(timestamptz)
To&#34; normalize&#34;任何给定的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
。它将UTC时间(!)中相应周($1 - date_trunc('week', $1)
的已用间隔添加到我们的分段周的起始点。 (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
加入:
通过调整后的设计,您的整个大而复杂,昂贵的查询可以替换为...:
SELECT *
的FROM hoo
强>
的WHERE hours @> f_hoo_time(now());
强>
为了一点暂停,我在解决方案上放了一个扰流板。将鼠标移到上。
查询由所述GiST索引支持并且速度很快,即使对于大表也是如此。
SQL Fiddle(有更多例子)。
如果你想计算总营业时间(每家商店),这是一个食谱:
containment operator for range types或GiST索引可支持SP-GiST。两者都可用于实现排除约束,但只有GiST支持multicolumn indexes:
目前,只有B树,GiST,GIN和BRIN索引类型支持多列索引。
order of index columns matters:
多列GiST索引可以与查询条件一起使用 涉及索引列的任何子集。附加条件 列限制索引返回的条目,但条件 在第一列是确定多少最重要的一列 索引需要扫描。 GiST指数相对较低 如果它的第一列只有几个不同的值,则无效 如果其他列中有许多不同的值。
所以我们在这里有冲突的利益。对于大型表格,shop_id
的价值会比hours
更多。
shop_id
的GiST索引更快写入并强制执行排除约束。hours
列。首先拥有该列会更好。shop_id
,那么普通的btree索引要快得多。hours
上的 SP-GiST 索引为查询的最快。我的脚本生成虚拟数据:
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
首先要快4倍。
此外,我还测试了两个读取性能:
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;
之后,我运行了两个查询:
每个人都有仅索引扫描(#34;没有索引&#34;当然除外):
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索引以优化读取性能。