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

时间:2014-03-01 00:37:31

标签: sql ruby-on-rails postgresql 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))
 )

 )

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

1 个答案:

答案 0 :(得分:18)

表格布局

重新设计表格并将营业时间(营业时间)存储为一组tsrange (range of timestamp without time zone)值。需要Postgres 9.2或更高版本

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

首先安装附加模块btree_gistWhy?

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

  • 始终包含精确落在下边界或上边界的时间点。
  • 有效禁止同一商店的相邻参赛作品。通过包容性边界,那些将重叠&#34;并且排除约束会引发异常。相邻的条目必须合并为一行。除非他们环绕周日午夜,在这种情况下,他们必须分成两行。请参阅下面的工具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)

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

功能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 typesGiST索引可支持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;之后,我运行了两个查询:

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

结果

每个人都有仅索引扫描(#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和GiST是相同的(对于非常,GiST甚至更快)。
  • SP-GiST随着越来越多的结果而更好地扩展,并且也更小。

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