4

我有一个表,其中包含一对表示时间跨度的时间戳。这些行由用户 ID 限定,每个用户可以有一个或多个与其关联的行。

该数据是从一个抽象的“可用性”表单生成的,该表单表示用户在一周内何时有空。我需要输入一系列时间范围作为查询并返回表中所有行都匹配的所有用户 ID。

鉴于此表:

CREATE TABLE "public"."availability" (
  "id" int4 NOT NULL,
  "user_id" int4,
  "starts_at" timestamp(6),
  "ends_at" timestamp(6),
  PRIMARY KEY ("id")
) WITH (OIDS=FALSE)

这个数据:

User #1 is available Mon-Tue between 08:00 and 17:00

+----+---------+---------------------+---------------------+
| id | user_id | starts_at           | ends_at             |
+----+---------+---------------------+---------------------+
| 1  | 1       | 2013-03-18 08:00:00 | 2013-03-18 17:00:00 |
+----+---------+---------------------+---------------------+
| 2  | 1       | 2013-03-19 08:00:00 | 2013-03-19 17:00:00 |
+----+---------+---------------------+---------------------+

User #2 is available Sun-Sat all day

+----+---------+---------------------+---------------------+
| 3  | 2       | 2013-03-17 00:00:00 | 2013-03-23 23:59:59 |
+----+---------+---------------------+---------------------+

User #3 is available Wed between 06:00 and 18:00

+----+---------+---------------------+---------------------+
| 4  | 3       | 2013-03-20 06:00:00 | 2013-03-20 18:00:00 |
+----+---------+---------------------+---------------------+

我可以轻松地选择可用于任何给定时间戳的用户:

SELECT * FROM "public"."availability"
  WHERE ('2013-03-19 08:35:00' BETWEEN starts_at AND ends_at 
     AND '2013-03-19 18:25:00' BETWEEN starts_at AND ends_at)
    OR  ('2013-03-20 12:00:00' BETWEEN starts_at AND ends_at
     AND '2013-03-20 18:00:00' BETWEEN starts_at AND ends_at);

+----+---------+---------------------+---------------------+
| id | user_id | starts_at           | ends_at             |
+----+---------+---------------------+---------------------+
| 3  | 2       | 2013-03-17 00:00:00 | 2013-03-23 23:59:59 |
+----+---------+---------------------+---------------------+
| 4  | 3       | 2013-03-20 06:00:00 | 2013-03-20 18:00:00 |
+----+---------+---------------------+---------------------+

但我真正需要的是能够查询多个时间跨度并仅返回与所有条件user_id匹配的 s 。

查询:2013-03-17 10:00:00- 2013-03-17 16:00:002013-03-23 10:00:00-2013-03-23 16:00:00应该返回:

+----+---------+---------------------+---------------------+
| id | user_id | starts_at           | ends_at             |
+----+---------+---------------------+---------------------+
| 3  | 2       | 2013-03-17 00:00:00 | 2013-03-23 23:59:59 |
+----+---------+---------------------+---------------------+

查询:2013-03-18 09:00:00- 2013-03-18 16:00:002013-03-19 08:00:00-2013-03-19 15:45:00应该返回:

+----+---------+---------------------+---------------------+
| id | user_id | starts_at           | ends_at             |
+----+---------+---------------------+---------------------+
| 1  | 1       | 2013-03-18 08:00:00 | 2013-03-18 17:00:00 |
+----+---------+---------------------+---------------------+
| 2  | 1       | 2013-03-19 08:00:00 | 2013-03-19 17:00:00 |
+----+---------+---------------------+---------------------+
| 3  | 2       | 2013-03-17 00:00:00 | 2013-03-23 23:59:59 |
+----+---------+---------------------+---------------------+

查询:2013-03-18 07:00:00-2013-03-18 18:00:00应该什么都不返回。

SQLFiddle 示例

4

2 回答 2

4

对于这样的应用程序,如果您使用的是 PostgreSQL 9.2 或更高版本,您可能想尝试使用range type。以下是创建、加载和显示数据的示例:

CREATE TABLE availability (
  id      int4 NOT NULL,
  user_id int4,
  avail   tstzrange,
  PRIMARY KEY (id)
);
INSERT INTO availability VALUES
  (1, 1, '[2013-03-18 08:00:00, 2013-03-18 17:00:00)'),
  (2, 1, '[2013-03-19 08:00:00, 2013-03-19 17:00:00)'),
  (3, 2, '[2013-03-17 00:00:00, 2013-03-23 24:00:00)'),
  (4, 3, '[2013-03-20 06:00:00, 2013-03-20 18:00:00)');
SELECT * FROM availability ;
编号 | 用户 ID | 利用                        
----+----------+------------------------------------ ------------------
  1 | 1 | ["2013-03-18 08:00:00-05","2013-03-18 17:00:00-05")
  2 | 1 | ["2013-03-19 08:00:00-05","2013-03-19 17:00:00-05")
  3 | 2 | ["2013-03-17 00:00:00-05","2013-03-24 00:00:00-05")
  4 | 3 | ["2013-03-20 06:00:00-05","2013-03-20 18:00:00-05")
(4 行)

然后您可以使用各种运算符进行查询。如果您想要包含任何指定查询范围的所有可用性范围:

SELECT * FROM availability
  WHERE avail @> '[2013-03-19 08:35:00, 2013-03-19 18:25:00)'
     OR avail @> '[2013-03-20 12:00:00, 2013-03-20 18:00:00)';

或者:

SELECT * FROM availability
  WHERE avail @> ANY
          (ARRAY ['[2013-03-19 08:35:00, 2013-03-19 18:25:00)'::tstzrange,
                  '[2013-03-20 12:00:00, 2013-03-20 18:00:00)'::tstzrange]);
编号 | 用户 ID | 利用                        
----+----------+------------------------------------ ------------------
  3 | 2 | ["2013-03-17 00:00:00-05","2013-03-24 00:00:00-05")
  4 | 3 | ["2013-03-20 06:00:00-05","2013-03-20 18:00:00-05")
(2 行)

如果您想要在单个范围内包含所有指定查询范围的所有可用性范围:

SELECT * FROM availability
  WHERE avail @> '[2013-03-17 10:00:00, 2013-03-17 16:00:00)'
    AND avail @> '[2013-03-23 10:00:00, 2013-03-23 16:00:00)';

或者:

SELECT * FROM availability
  WHERE avail @> ALL
          (ARRAY ['[2013-03-17 10:00:00, 2013-03-17 16:00:00)'::tstzrange,
                  '[2013-03-23 10:00:00, 2013-03-23 16:00:00)'::tstzrange]);
编号 | 用户 ID | 利用                        
----+----------+------------------------------------ ------------------
  3 | 2 | ["2013-03-17 00:00:00-05","2013-03-24 00:00:00-05")
(1 行)

如果您想要包含任何指定查询范围的所有可用性范围,但仅适用于具有覆盖所有指定查询范围的可用性范围的用户:

WITH s(ts) AS
(
  VALUES
    ('[2013-03-18 09:00:00, 2013-03-18 16:00:00)'::tstzrange),
    ('[2013-03-19 08:00:00, 2013-03-19 15:45:00)'::tstzrange)
)
SELECT DISTINCT a1.*
  FROM s s1
  JOIN availability a1 ON a1.avail @> s1.ts
    AND NOT EXISTS
        (
          SELECT * FROM s s2
            WHERE NOT EXISTS
                  (
                    SELECT * FROM availability a2
                      WHERE a2.user_id = a1.user_id
                        AND a2.avail @> s2.ts
                  )
        );

或(调整 Clodoaldo Neto 的查询以使用范围):

SELECT a.*
  FROM availability a
  JOIN (
         SELECT
             user_id,
             sum(('[2013-03-18 09:00:00, 2013-03-18 16:00:00)'::tstzrange
                   <@ avail)::integer
                 +
                 ('[2013-03-19 08:00:00, 2013-03-19 15:45:00)'::tstzrange
                   <@ avail)::integer
                ) period
           FROM availability
           GROUP BY user_id
       ) s ON a.user_id = s.user_id
  WHERE period >= 2;

您可以创建一个索引,以便在大型表上非常快速地进行此类搜索,如下所示:

CREATE INDEX availability_avail ON availability USING gist (avail);

笔记:

  • 为了便于阅读,我省略了架构和引号。
  • 索引不太可能与四行一起使用,因为通过直接读取一个数据页可以更快地获得所有数据。对于大桌子,它可能会产生很大的不同。
  • 我使用范围是TIMESTAMP WITH TIME ZONE因为默认(裸机)TIMESTAMP时钟每年在夏令时结束时向后移动。要及时捕捉瞬间,请使用TIMESTAMP WITH TIME ZONEtimestamptz简称)。
  • 直接使用时,字面量不需要显式转换;当使用查询的ANYorALL形式时,需要显式转换。
  • 范围上的方括号表示该范围包括相邻时间,而圆括号表示该范围不包括相邻时间。时间戳通常被指定为[)使得以给定时间结束的范围和以相同时间开始的另一个范围被认为是相邻的而不是重叠的。
  • '24:00:00'一个日期和'00:00:00'下一个日期是同一时刻。
  • 前面两点允许更轻松地指定在午夜结束的时间戳。不存在“失去一秒”或其他陌生感的风险。
于 2013-03-21T21:21:02.877 回答
3

SQL小提琴

这利用了布尔转换为整数作为 0 或 1。

select a.*
from
    availability a
    inner join
    (
        select
            user_id,
            sum (
                ('2013-03-18 09:00:00' between starts_at and ends_at
                 and
                 '2013-03-18 16:00:00' between starts_at and ends_at
                )::integer
                +
                ('2013-03-19 08:00:00' between starts_at and ends_at
                 and
                 '2013-03-19 15:45:00' between starts_at and ends_at
                )::integer
            ) period
        from availability
        group by user_id
    ) s on a.user_id = s.user_id
where period >= 2

将条件更改为where要匹配的句点数。

于 2013-03-21T16:04:00.353 回答