会议室预定系统实践(解放开发) - PostgreSQL tsrange(时间范围类型) + 排他约束

3 minute read

背景

PostgreSQL 范围、数组、空间类型(range, array, geometry),都有交叉属性,例如时间范围:7点到9点,8点到9点,这两个内容是有重叠部分的。例如数组类型:[1,2,3]和[2,4,5]是有交叉部分的。例如空间类型也有交叉的属性。

那么在设计时,实际上业务上会有这样的约束,不允许对象有相交。

例如会议室预定系统,不允许两个人预定的会议室时间交叉,否则就有可能一个会议室在某个时间段被多人共享了,业务上是不允许的。

那么如何做到这样的约束呢?

PostgreSQL 提供了exclude约束,可以实现这个需求。

exclude 约束的语法

  EXCLUDE [ USING index_method ] ( exclude_element WITH operator [, ... ] ) index_parameters [ WHERE ( predicate ) ] |    
    
exclude_element in an EXCLUDE constraint is:    
    
{ column_name | ( expression ) } [ opclass ] [ ASC | DESC ] [ NULLS { FIRST | LAST } ]    

exclude 约束常用的操作符

范围、数组、空间类型的相交操作符如下:

postgres=# \do &&    
                                          List of operators    
   Schema   | Name | Left arg type | Right arg type | Result type |           Description                
------------+------+---------------+----------------+-------------+----------------------------------    
 pg_catalog | &&   | anyarray      | anyarray       | boolean     | overlaps    
 pg_catalog | &&   | anyrange      | anyrange       | boolean     | overlaps    
 pg_catalog | &&   | box           | box            | boolean     | overlaps    
 pg_catalog | &&   | circle        | circle         | boolean     | overlaps    
 pg_catalog | &&   | inet          | inet           | boolean     | overlaps (is subnet or supernet)    
 pg_catalog | &&   | polygon       | polygon        | boolean     | overlaps    
 pg_catalog | &&   | tinterval     | tinterval      | boolean     | overlaps    
 pg_catalog | &&   | tsquery       | tsquery        | tsquery     | AND-concatenate    
 public     | &&   | integer[]     | integer[]      | boolean     | overlaps    
(9 rows)    

会议室预定系统的例子

1、创建btree_gist插件.

postgres=# create extension btree_gist;    
CREATE EXTENSION    

2、创建会议室预定表

postgres=# create table t_meeting (    
  roomid int,   -- 会议室ID    
  who int,      -- 谁定了这个会议室    
  ts tsrange,   -- 时间范围    
  desc text,    -- 会议内容描述    
  exclude using gist (roomid with = , ts with &&)   -- 排他约束,同一个会议室,不允许有时间范围交叉的记录    
);    
CREATE TABLE    

扩展CASE,通常会议室预定可能还有审核过程,那么也就是说用户提交请求的时候,允许时间重叠,但是对于审核后的数据,不允许重叠,假设审核后的状态为1. 比如我们只想针对状态为1的会议室时间设定不冲突,可以使用predict条件的exclude约束。 例子:

postgres=# create table t_meeting (    
  roomid int,   -- 会议室ID    
  who int,      -- 谁定了这个会议室    
  status char(1) not null,  -- 状态,1表示确定已定,0表示审核中。 
  ts tsrange,   -- 时间范围    
  ds text,    -- 会议内容描述    
  exclude using gist (roomid with = , ts with &&) where (status='1')   -- 排他约束,同一个会议室,不允许有时间范围交叉的记录    
);    
CREATE TABLE    
postgres=# insert into t_meeting values (1,123,'0', $$['2017-01-01 07:00:00', '2017-01-01 08:00:00')$$, 'PostgreSQL conference');
INSERT 0 1
postgres=# insert into t_meeting values (1,123,'0', $$['2017-01-01 07:00:00', '2017-01-01 08:00:00')$$, 'PostgreSQL conference');
INSERT 0 1
postgres=# insert into t_meeting values (1,123,'1', $$['2017-01-01 07:00:00', '2017-01-01 08:00:00')$$, 'PostgreSQL conference');
INSERT 0 1
postgres=# insert into t_meeting values (1,123,'1', $$['2017-01-01 07:00:00', '2017-01-01 08:00:00')$$, 'PostgreSQL conference');
ERROR:  conflicting key value violates exclusion constraint "t_meeting_roomid_ts_excl"
DETAIL:  Key (roomid, ts)=(1, ["2017-01-01 07:00:00","2017-01-01 08:00:00")) conflicts with existing key (roomid, ts)=(1, ["2017-01-01 07:00:00","2017-01-01 08:00:00")).

3、预定会议室,如果同一个会议室输入的时间不允许预定(有交叉),则自动报错。实现强约束。

postgres=# insert into t_meeting values (1, 1, $$['2017-01-01 07:00:00', '2017-01-01 08:00:00')$$);    
INSERT 0 1    
    
postgres=# insert into t_meeting values (1,1,$$['2017-01-01 07:00:00', '2017-01-01 08:00:00')$$);    
ERROR:  conflicting key value violates exclusion constraint "t_meeting_roomid_ts_excl"    
DETAIL:  Key (roomid, ts)=(1, ["2017-01-01 07:00:00","2017-01-01 08:00:00")) conflicts with existing key (roomid, ts)=(1, ["2017-01-01 07:00:00","2017-01-01 08:00:00")).    
    
postgres=# insert into t_meeting values (2,1,$$['2017-01-01 07:00:00', '2017-01-01 08:00:00')$$);    
INSERT 0 1    
    
postgres=# insert into t_meeting values (1,1,$$['2017-01-01 09:00:00', '2017-01-01 10:00:00')$$);    
INSERT 0 1    
    
postgres=# insert into t_meeting values (1,1,$$['2017-01-01 09:00:00', '2017-01-01 11:00:00')$$);    
ERROR:  conflicting key value violates exclusion constraint "t_meeting_roomid_ts_excl"    
DETAIL:  Key (roomid, ts)=(1, ["2017-01-01 09:00:00","2017-01-01 11:00:00")) conflicts with existing key (roomid, ts)=(1, ["2017-01-01 09:00:00","2017-01-01 10:00:00")).    
    
postgres=# insert into t_meeting values (1,1,$$['2017-01-01 08:00:00', '2017-01-01 09:00:00')$$);    
INSERT 0 1    
postgres=# select * from t_meeting order by roomid, ts;    
 roomid | who |                      ts                           
--------+-----+-----------------------------------------------    
      1 |   1 | ["2017-01-01 07:00:00","2017-01-01 08:00:00")    
      1 |   1 | ["2017-01-01 08:00:00","2017-01-01 09:00:00")    
      1 |   1 | ["2017-01-01 09:00:00","2017-01-01 10:00:00")    
      2 |   1 | ["2017-01-01 07:00:00","2017-01-01 08:00:00")    
(4 rows)    

4、查询某个时间段还有哪些会议室能预定

会议室ID表,假设有50个会议室。

create table t_room (roomid int primary key);    
    
insert into t_room select generate_series(1,50);    

假设用户要预定 某一天:7点到9点的会议室,这样操作即可:

select roomid from t_room    
except    
select roomid from t_meeting where ts && $$['2017-01-01 07:00:00', '2017-01-01 09:00:00')$$;    
 roomid     
--------    
     14    
      3    
      4    
     16    
     42    
     50    
     19    
     13    
     40    
     46    
     18    
     34    
     39    
      7    
     35    
     43    
     23    
     36    
     29    
     30    
     28    
      8    
     24    
     32    
     10    
     33    
      9    
     45    
     22    
     49    
     48    
     38    
     37    
      5    
     12    
     31    
     11    
     27    
     20    
     44    
     41    
      6    
     21    
     15    
     47    
     17    
     26    
     25    
(48 rows)    
    
    
postgres=# explain (analyze,verbose,timing,costs,buffers) select roomid from t_room    
except    
select roomid from t_meeting where ts && $$['2017-01-01 07:00:00', '2017-01-01 09:00:00')$$;    
                                                                     QUERY PLAN                                                                         
----------------------------------------------------------------------------------------------------------------------------------------------------    
 HashSetOp Except  (cost=0.00..77.28 rows=2550 width=8) (actual time=0.074..0.085 rows=48 loops=1)    
   Output: "*SELECT* 1".roomid, (0)    
   Buffers: shared hit=3    
   ->  Append  (cost=0.00..70.88 rows=2562 width=8) (actual time=0.013..0.058 rows=53 loops=1)    
         Buffers: shared hit=3    
         ->  Subquery Scan on "*SELECT* 1"  (cost=0.00..61.00 rows=2550 width=8) (actual time=0.012..0.029 rows=50 loops=1)    
               Output: "*SELECT* 1".roomid, 0    
               Buffers: shared hit=1    
               ->  Seq Scan on public.t_room  (cost=0.00..35.50 rows=2550 width=4) (actual time=0.010..0.016 rows=50 loops=1)    
                     Output: t_room.roomid    
                     Buffers: shared hit=1    
         ->  Subquery Scan on "*SELECT* 2"  (cost=1.44..9.88 rows=12 width=8) (actual time=0.018..0.019 rows=3 loops=1)    
               Output: "*SELECT* 2".roomid, 1    
               Buffers: shared hit=2    
               ->  Bitmap Heap Scan on public.t_meeting  (cost=1.44..9.76 rows=12 width=4) (actual time=0.018..0.018 rows=3 loops=1)    
                     Output: t_meeting.roomid    
                     Recheck Cond: (t_meeting.ts && '["2017-01-01 07:00:00","2017-01-01 09:00:00")'::tsrange)    
                     Heap Blocks: exact=1    
                     Buffers: shared hit=2    
                     ->  Bitmap Index Scan on t_meeting_roomid_ts_excl  (cost=0.00..1.44 rows=12 width=0) (actual time=0.010..0.010 rows=4 loops=1)    
                           Index Cond: (t_meeting.ts && '["2017-01-01 07:00:00","2017-01-01 09:00:00")'::tsrange)    
                           Buffers: shared hit=1    
 Planning time: 0.123 ms    
 Execution time: 0.172 ms    
(24 rows)    

速度杠杠的。开发也方便了。

小结

使用PostgreSQL,时间范围类型、exclude约束,很好的帮助业务系统实现会议室预定的强约束。

使用except语法,很方便的找到需要预定的时间段还有那些会议室是空闲的。

开不开心,解放开发人员的大脑。

参考

https://www.postgresql.org/docs/10/static/sql-createtable.html#SQL-CREATETABLE-EXCLUDE

Flag Counter

digoal’s 大量PostgreSQL文章入口