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

One*_*ude 11 sql postgresql ruby-on-rails constraints range-types

我在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 < 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强制执行范围的包含边界,具有两个值得注意的后果:

  • 始终包括精确落在下边界或上边界的时间点.
  • 实际上不允许同一商店的相邻条目.通过包容性边界,这些将"重叠",排除约束将引发异常.相邻的条目必须合并为一行.除非它们在周日午夜时分缠绕,在这种情况下它们必须分成两排.见下面的工具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)
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(有更多示例).

如果你想计算总营业时间(每家商店),这里有一个配方:

指数和表现

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

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

索引列顺序很重要:

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

所以我们在这里有利益冲突.对于大表,将会有更多的不同的值shop_idhours.

  • 带有前导的GiST索引shop_id编写和执行排除约束的速度更快.
  • 但是我们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;
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;,我跑了两个问题:

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

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

  • 是的,这是令人难以置信的,我唯一的问题是:有没有办法让这个设计模式与环绕一起工作(商店有一小时的操作在周日开放并在星期一关闭)? (2认同)