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

我在RoR堆栈中,我必须编写一些实际的SQL来完成所有“打开”记录的查询,这意味着当前时间在指定的操作时间内.在hours_of_operations表中,两个整数列opens_on和closes_on存储工作日,两个时间字段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格式存储值,因此很多情况下用户的本地时间可以以非常奇怪的方式进行换行.上面的查询确保您可以在一周中输入任意两次,并且我们可以补偿包装.

解决方法

表格布局

重新设计表格并将开放时间(营业时间)存储为一组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]')
);

一列时间取代了所有列:

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

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

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

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

> Preventing adjacent/overlapping entries with EXCLUDE in PostgreSQL

函数f_hoo_time(timestamptz)

要使用时区“标准化”任何给定的时间戳:

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并返回时间戳.它将相应周的经过时间间隔($1 – date_trunc(‘week’,$1)以UTC时间(!)添加到我们的分段周的起始点.(日期间隔生成时间戳.)

函数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,'[]');
      RETURN NEXT;
   END IF;

   RETURN;
END
$func$ LANGUAGE plpgsql IMMUTABLE COST 1000 ROWS 1;

要插入单个输入行:

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 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连接:

> What is the difference between LATERAL and a subquery in PostgreSQL?

询问

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

   选择 *
 来自hoo
 在哪里@> f_hoo_time(现在的());

为了一点悬念,我在解决方案上放了一个扰流板.将鼠标移到它上面.

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

SQL Fiddle(有更多例子).

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

> Calculate working hours between 2 dates in PostgreSQL

指数和表现

可以使用GiSTSP-GiST索引支持containment operator for range types.两者都可用于实现排除约束,但只有GiST支持multicolumn indexes

Currently,only the B-tree,GiST,GIN,and BRIN index types support multicolumn indexes.

order of index columns matters

A multicolumn GiST index can be used with query conditions that
involve any subset of the index’s columns. Conditions on additional
columns restrict the entries returned by the index,but the condition
on the first column is the most important one for determining how much
of the index needs to be scanned. A GiST index will be relatively
ineffective if its first column has only a few distinct values,even
if there are many distinct values in additional columns.

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

>具有前导shop_id的GiST索引编写速度更快,并强制执行排除约束.
>但我们正在搜索查询中的小时列.首先拥有该列会更好.
>如果我们需要在其他查询中查找shop_id,那么普通的btree索引要快得多.
>最重要的是,我发现SP-GiST索引只需几个小时即可获得最快的查询速度.

基准

我的脚本生成虚拟数据:

INSERT INTO hoo(shop_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个不同的小时. (通常差异会更大.)表大小为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;之后,我运行了两个查询:

> 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索引以优化读取性能.

相关文章

文章浏览阅读601次。Oracle的数据导入导出是一项基本的技能,...
文章浏览阅读553次。开头还是介绍一下群,如果感兴趣polardb...
文章浏览阅读3.5k次,点赞3次,收藏7次。折腾了两个小时多才...
文章浏览阅读2.7k次。JSON 代表 JavaScript Object Notation...
文章浏览阅读2.9k次,点赞2次,收藏6次。navicat 连接postgr...
文章浏览阅读1.4k次。postgre进阶sql,包含分组排序、JSON解...