SQL - PostgreSQL - 查找预订日历中的差距

时间:2017-02-12 13:36:07

标签: sql database postgresql calendar

我目前正在建立一个基于网络的系统,允许用户在日历中查找船只的可用性。

我正在使用PostgreSQL 9.6

用户应该可以说我想在接下来的三个月内在早上8点到下午16点之间找到一艘可以使用三小时的船。

我目前的数据模型如下。

 CREATE TABLE cal_calendar (
  id INTEGER NOT NULL
 , start_time TIMESTAMP NOT NULL
 , time_range TSRANGE NOT NULL
);  

cal_calendar表有一个TSRANGE列,粒度为15分钟,包含:

  id  |     start_time      |                  time_range
------+---------------------+-----------------------------------------------
 4225 | 2017-02-14 00:00:00 | ["2017-02-14 00:00:00","2017-02-14 00:15:00")
 4226 | 2017-02-14 00:15:00 | ["2017-02-14 00:15:00","2017-02-14 00:30:00")
 4227 | 2017-02-14 00:30:00 | ["2017-02-14 00:30:00","2017-02-14 00:45:00")
 4228 | 2017-02-14 00:45:00 | ["2017-02-14 00:45:00","2017-02-14 01:00:00")
 4229 | 2017-02-14 01:00:00 | ["2017-02-14 01:00:00","2017-02-14 01:15:00")

此表包含一个参考日历,其中包含未来5年内所有15分钟的时间段。

要填充cal_calendar表,我使用以下Perl脚本:

#!/usr/bin/perl
use strict;
use POSIX qw(strftime);
use DBI;
use DateTime;

my $database = "mydatabase";
my $db_host = "localhost";
my $db_user = "nobody";
my $db_passwd = "noneofyourbusiness";
my $years_to_populate = $ARGV[0];

my $dbh = DBI->connect("DBI:Pg:dbname=".$database.";host=".$db_host, $db_user, $db_passwd, {'RaiseError' => 0});

my $start_time = DateTime->new( year  => 2016, month => 12, day   => 31, hour => 23, minute => 45);
my $end_time = $start_time->clone->add(years => $years_to_populate);

my $i=1;     
while ( $start_time->add(minutes => 15) < $end_time ) {

  my $period_start= $start_time->strftime( "%Y-%m-%d %H:%M:%S" );
  my $period_end = $start_time->clone->add(minutes => 15)->strftime( "%Y-%m-%d %H:%M:%S" );
  $dbh->do("INSERT INTO cal_calendar (id, start_time, time_range) VALUES (".$i.",'".$period_start."'::timestamp without time zone, '[".$period_start.",".$period_end.")'::tsrange );");    

$i++;
}

另一方面,我有一张桌子,应该包含来自用户的实际预订。当然,当预订某艘船时,没有其他人能够同时预订。

预订表如下:

CREATE TABLE usg_bookings (
   id INTEGER NOT NULL DEFAULT nextval('sq$usg_bookings_id')
   , user_id INTEGER NOT NULL
   , boat_id INTEGER NOT NULL
   , start_time TIMESTAMP
   , time_range tsrange NOT NULL
);

示例如下:

 id | user_id |   boat_id   |     start_time      |                  time_range
----+---------+-------------+---------------------+-----------------------------------------------
  5 |       1 |           1 | 2017-02-11 08:00:00 | ["2017-02-11 08:00:00","2017-02-11 12:00:00")
  6 |       1 |           2 | 2017-02-11 13:00:00 | ["2017-02-11 13:00:00","2017-02-11 14:00:00")
  7 |       1 |           1 | 2017-02-14 09:00:00 | ["2017-02-14 09:00:00","2017-02-14 12:30:00")
  8 |       1 |           2 | 2017-02-14 13:30:00 | ["2017-02-14 13:30:00","2017-02-14 15:15:00")

在预订表中插入一些虚拟数据:

INSERT INTO usg_bookings (user_id, group_id, boat_id, start_time,  time_range) VALUES
   (1,1,1, '2017-02-11 08:00:00'::timestamp, '["2017-02-11 08:00:00","2017-02-11 12:00:00")'::tsrange) 
  ,(1,1,2, '2017-02-11 13:00:00'::timestamp, '["2017-02-11 13:00:00","2017-02-11 14:00:00")'::tsrange)
  ,(1,1,1, '2017-02-14 09:00:00'::timestamp, '["2017-02-14 09:00:00","2017-02-14 12:30:00")'::tsrange) 
  ,(1,1,2, '2017-02-14 13:30:00'::timestamp, '["2017-02-14 13:30:00","2017-02-14 15:15:00")'::tsrange);

在我的方法中,我使用“start_time”列进行分区,并不打算用于查询表。但它可能会改变你的建议:)

因此,我正在寻找一种有效的方法来查找已记录的预订之间的“差距”,以便能够为我的用户提供最佳可用性。

它应该说:“下周将为该特定船只提供两小时的服务”。

请注意我有一些数据库和SQL经验,但我对PostgreSQL中时间范围的概念完全不了解。

我非常感谢你提供了很好的答案。

2 个答案:

答案 0 :(得分:2)

我建议你对这个问题采取不同的方法。对于初学者,cal_calendar不是必需的,表start_time中的usg_bookings字段也是多余的。相反,使用tsrange并使用窗口函数来标识可用的句点。此外,在您的桌面上设置EXCLUDE约束以避免双重预订(在网络应用程序中,您可能会让多个人同时尝试预订船只;在识别可用租赁和完成租赁本身(填写姓名,信用卡详细信息,......)其他人可能已完成同一时期和船的预订。

你的桌子变成了:

CREATE EXTENSION btree_gist;

CREATE TABLE usg_bookings (
   id serial PRIMARY KEY,
   user_id integer NOT NULL,
   boat_id integer NOT NULL,
   time_range tsrange NOT NULL,
   EXCLUDE USING gist (boat_id WITH =, time_range WITH &&)
);

查找所有船只的可用时段:

SELECT boat_id, available
FROM (
    SELECT boat_id,
           tsrange(upper(time_range), lower(lead(time_range) OVER 
               (PARTITION BY boat_id ORDER BY lower(time_range)))) AS available
    FROM (
        SELECT boat_id, time_range
        FROM usg_bookings
        WHERE lower(time_range)::date BETWEEN <<<start_date>>> AND <<<final_date>>>
        UNION
        SELECT boat_id,
               tsrange(closed + interval '16 hours', closed + interval '32 hours')
        FROM generate_series(<<<start_date>>> - 1, <<<final_date>>>) dates(closed),
             VALUES(<<<boat ids>>>) b(boat_id) ) sub2
    ) sub
WHERE upper(available) - lower(available) >= interval '3 hours';

一些解释:

您希望在规定的时间内在白天(假设您的操作从下午4点到早上8点关闭)在白天至少可以找到3小时的船只。定义的时间段由查询中的<<<start_date>>><<<final_date>>>表示。由于您正在开发Web应用程序,我假设您将在您使用的任何框架中使用位置参数。

您不希望在关闭操作时进行预订,因此这些时间会变黑。有效地进行查询,这与在所有非办公时间租用所有船只的情况相同:

SELECT boat_id,
       tsrange(closed + interval '16 hours', closed + interval '32 hours')
FROM generate_series(<<<start_date>>> - 1, <<<final_date>>>) dates(closed),
     VALUES(<<<boat ids>>>) b(boat_id)

简而言之,为每艘船产生一系列日子,并从当天下午4点到第二天上午8点(= 32小时)阻挡。请注意,start_date - 1涵盖了第一天午夜至上午8点的时段。

如果你有几艘船,VALUES条款没问题。如果有很多船只,或者您可能会随着时间的推移添加或删除船只,请使用SELECT DISTINCT boat_id FROM boats之类的子查询。

此阻止列表与感兴趣期间的现有预订合并:

SELECT boat_id, time_range
FROM usg_bookings
WHERE lower(time_range)::date BETWEEN <<<start_date>>> AND <<<final_date>>>
UNION
<<<closed hours>>>

当您订购上述所有不可用时段(现有预订和关闭时间)时,您可以使用boat_id分区的窗口功能确定每艘船的可用时间,并减去租赁结束或开放时间从下一个租金或办公室关闭时间开始的办公室:

SELECT boat_id,
       tsrange(upper(time_range), lower(lead(time_range) OVER 
           (PARTITION BY boat_id ORDER BY lower(time_range)))) AS available
FROM 
    <<<inner query>>>

行由boat_id分区(因此对每个船只ID评估所有不可用时段的行)并按lower(time_range)排序(不可用时段的开始)。然后,tsrange()部分会从当前租借或开放时间结束以及下一个租赁或关闭时间的开始(timestamp窗口功能)创建新的lead()范围。

最后,在主要查询中,为每条船只选择至少3小时(available)的所有WHERE upper(available) - lower(available) >= interval '3 hours'间隔。

答案 1 :(得分:0)

可能你需要这样的东西:

WITH param AS (
    SELECT '{1}'::int4[] AS boats, '["2017-02-11 08:00:00","2017-02-16 20:00:00")'::tsrange AS time, '03:00:00'::time as period
), periods AS (
        SELECT c.start_time, time_range,  boat_id
          FROM cal_calendar AS c
          JOIN param AS p ON (time_range <@ time)
          JOIN boats AS b ON (b.boat_id = ANY(p.boats) OR p.boats IS NULL)
         WHERE
            NOT EXISTS (
                SELECT 1
                  FROM usg_bookings AS u
                 WHERE u.boat_id = b.boat_id
                   AND c.time_range <@ u.time_range
            )
), avail AS ( 
        WITH RECURSIVE x AS (
                SELECT p.* , 1 AS level
                  FROM periods AS p, param
                 WHERE NOT EXISTS (
                        SELECT 1
                          FROM periods AS p2
                         WHERE p2.start_time + '15 min'::interval = p.start_time
                           AND p2.boat_id = p.boat_id
                )
                UNION ALL
                SELECT x.start_time, x.time_range + p.time_range, x.boat_id, level + 1
                  FROM x
                  JOIN periods AS p ON (
                           p.start_time = upper(x.time_range)
                           AND p.boat_id = x.boat_id)
        ) SELECT *
            FROM x
           WHERE NOT EXISTS (
                     SELECT 1
                       FROM x AS x2
                      WHERE x.boat_id = x2.boat_id
                        AND x.start_time = x2.start_time
                        AND x2.level > x.level)
)
SELECT a.start_time, a.time_range, a.boat_id
  FROM avail AS a, param AS p 
 WHERE p.period <= upper(a.time_range) - lower(a.time_range)
 ORDER BY boat_id, start_time, time_range

param是参数,您可以将某些船设置为{1,3}或任何AS NULL。

periods所有期间都不包括usg_booking

已保留的期间

avail是可分组的(在WITH RECRSIVE x中)。