Pie*_* H. 3 sql database postgresql calendar
我目前正在建立一个基于网络的系统,以允许用户在日历中查找船只的可用性。
我正在使用 PostgreSQL 9.6
用户应该能够说我想找到一艘在未来三个月内早上 08:00 到 16:00 之间可运行三个小时的船。
我当前的数据模型如下。
CREATE TABLE cal_calendar (
id INTEGER NOT NULL
, start_time TIMESTAMP NOT NULL
, time_range TSRANGE NOT NULL
);
Run Code Online (Sandbox Code Playgroud)
cal_calendar 表有一个粒度为 15 分钟的 TSRANGE 列,包含:
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")
Run Code Online (Sandbox Code Playgroud)
该表基本上包含一个参考日历,其中包含未来 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++;
}
Run Code Online (Sandbox Code Playgroud)
另一方面,我有一张表,其中应该包含用户的实际预订。当然,当预订一艘特定的船时,其他人不应该能够同时预订它。
预订表如下所示:
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
);
Run Code Online (Sandbox Code Playgroud)
示例如下:
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")
Run Code Online (Sandbox Code Playgroud)
要在预订表中插入一些虚拟数据:
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);
Run Code Online (Sandbox Code Playgroud)
在我的方法中,我使用“start_time”列进行分区,它不打算用于查询表。但根据你的建议它可能会改变:)
因此,我正在寻找一种有效的方法来找到已记录的预订之间的“差距”,以便能够向我的用户建议最佳的可用性。
它应该说:“下周该特定船有两个小时的可用时间”。
请注意,我有一些数据库和 SQL 经验,但我对 PostgreSQL 中的时间范围概念完全陌生。
我提前非常感谢您的精彩回答。
我建议您对这个问题采取不同的方法。对于初学者来说,这cal_calendar不是必需的,而且start_time表中的字段usg_bookings也是多余的。相反,使用tsrange并使用窗口函数来识别可用周期。另外,EXCLUDE在您的表上设置限制以避免重复预订(在 Web 应用程序中,您可能会遇到多人同时尝试预订船只;在确定可用租赁并完成租赁本身所需的时间(填写姓名、信用卡详细信息……)其他人可能已经完成了同一时期和船只的预订)。
你的表变成:
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 &&)
);
Run Code Online (Sandbox Code Playgroud)
查找所有船只的可用时段:
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';
Run Code Online (Sandbox Code Playgroud)
一些解释:
您希望找到在指定时间段内白天至少有 3 小时可用的船只(假设您的营业时间为下午 4 点至上午 8 点关闭)。定义的时间段由查询中的<<<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)
Run Code Online (Sandbox Code Playgroud)
简而言之,为每艘船生成一系列日期,并从当天下午 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>>>
Run Code Online (Sandbox Code Playgroud)
当您订购所有上述不可用时段(现有预订和关闭时间)时,您可以使用窗口函数来确定每艘船的可用时间,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>>>
Run Code Online (Sandbox Code Playgroud)
这些行按boat_id(因此针对每个船 ID 评估所有不可用时段的行)进行分区并按lower(time_range)(不可用时段的开始时间)排序。tsrange()然后,该部件timestamp从当前租赁或开放时间结束到下一个租赁或关闭时间开始(lead()窗口函数)创建一个新范围。
最后,在主查询中,您为每艘船选择所有available至少 3 小时长的间隔 ( )。WHERE upper(available) - lower(available) >= interval '3 hours'
| 归档时间: |
|
| 查看次数: |
1881 次 |
| 最近记录: |