如何检查 PostgreSQL 中是否没有时间间隔?

Str*_*667 7 postgresql database-design constraint interval

架构

CREATE TABLE "expenses_commissionrule" (
    "id" serial NOT NULL PRIMARY KEY, 
    "country" varchar(2) NOT NULL, 
    "created" timestamp with time zone NOT NULL, 
    "modified" timestamp with time zone NOT NULL, 
    "activity" tsrange NOT NULL
); 
Run Code Online (Sandbox Code Playgroud)

说明

我想创建一个应用程序来管理佣金计算。每个规则都有活动期。每个国家都有一套独立的规则。

约束

第一的。为避免歧义,这些活动期不应重叠。我做了以下约束:

ALTER TABLE expenses_commissionrule 
ADD CONSTRAINT non_overlapping_activity 
EXCLUDE USING GIST (country WITH =, activity WITH &&);
Run Code Online (Sandbox Code Playgroud)

第二。在任何时间点都应该只有一个佣金规则,因此表中的间隔之间不应存在间隙。换句话说,所有区间的总和应该是 -INF:+INF。

问题:如何添加第二个约束?用例:我们有一个无限期的规则。我想切换到新规则,该规则应从下个月开始。在这种情况下,我想将当前规则结束期设置为当月月底,并在单个操作中添加新规则。

更新:将来我想添加以下行为:

  1. 仅为特定用户指定规则的能力(通过可为空的外键“user_id”);
  2. 每个用户都可以为每个国家/地区制定自己的规则。如果用户没有规则——“全局”规则将用于佣金计算,所以它就像一个后备;
  3. 这些“用户”规则可以有任何活动时间段 - (-inf: inf) 和具体的时间间隔。它与“全局”规则不同——这里的差距是可以的。
  4. 由于“可能是”功能 - 额外的 varchar 字段,这将有一种情况,当可以应用特定规则以在计算过程中具有更大的灵活性时。这个“部分”规则也可能有不同的间隔,间隙是可以的。

换句话说,所有先前的约束都应仅应用于 user_id 为 NULL 的行。如何做到这一点?

PostgreSQL 版本:9.6.2

ype*_*eᵀᴹ 9

第 1 章:链表

在数据库中强制执行这种(无间隙)类型的约束的一种方法是拆分activity为开始和结束部分,然后使用UNIQUEFOREIGN KEY约束来模拟链接列表。

1a.

  • 每个都activity_start应该参考前一个activity_end:
    (country, activity_start) REFERENCES (country, activity_end)
  • 两个期间不能有相同的activity_startcountry或活动结束和国家:对 的
    UNIQUE约束(country, activity_start)
    UNIQUE (country, activity_end)
  • 我们实际上不需要它们两个,只需要第二个来定义外键。排除约束不允许两个期间具有相同的开始或结束时间。
  • 我们不应该允许多行带有(-Infinity, +Infinity)或系列,例如:-Infinity -> DateA -> Infinity -> DateB -> +Infinity。这是通过两个部分索引实现的。

代码:

CREATE TABLE expenses_commissionrule (
    id serial NOT NULL PRIMARY KEY,
    country varchar(2) NOT NULL,
    created timestamp with time zone NOT NULL,
    modified timestamp with time zone NOT NULL,
    activity tsrange NOT NULL,
    activity_start timestamp,
    activity_end timestamp,
    CONSTRAINT non_overlapping_activity
        EXCLUDE USING GIST (country WITH =, activity WITH &&),
    CONSTRAINT country_activity_end_uq
        UNIQUE (country, activity_end),
    CONSTRAINT activity_start_end_fk
        FOREIGN KEY (country, activity_start)
        REFERENCES expenses_commissionrule (country, activity_end),
    CONSTRAINT activity_ck
        CHECK (activity IS NOT DISTINCT FROM 
               tsrange(activity_start, activity_end, '[)') )
) ;


CREATE UNIQUE INDEX country_start_ufx
    ON expenses_commissionrule
        (country)
    WHERE (activity_start IS NULL) ;

CREATE UNIQUE INDEX country_end_ufx
    ON expenses_commissionrule
        (country)
    WHERE (activity_end IS NULL) ;
Run Code Online (Sandbox Code Playgroud)

然后我们可以尝试插入(有效)数据:

WITH ins
    (country, activity_start, activity_end)
  AS
    ( VALUES
          ('IT',  null::timestamp,  null::timestamp),

          ('FR',  null,             '2000-01-01'),
          ('FR',  '2000-01-01',     null),

          ('GR',  null,             '2000-01-01'),
          ('GR',  '2000-01-01',     '2012-01-01'),
          ('GR',  '2012-01-01',     '2017-06-01'),
          ('GR',  '2017-06-01',     null)
    )
INSERT INTO expenses_commissionrule
    (country, created, modified, activity, activity_start, activity_end)
SELECT
    country, now(), now(),
    tsrange(activity_start, activity_end, '[)'),
    activity_start, activity_end
FROM ins ;
Run Code Online (Sandbox Code Playgroud)

工作正常:

> INSERT 0 7
Run Code Online (Sandbox Code Playgroud)

并尝试使用无效数据:

--
( VALUES
      ('US',  null::timestamp,  '2000-01-01'::timestamp)
)
--

-- Fails:

> ERROR:  insert or update on table "expenses_commissionrule" violates 
      foreign key constraint "activity_start_end_fk"
> DETAIL:  Key (country, activity_end)=(US, 2000-01-01 00:00:00) is not
      present in table "expenses_commissionrule".
Run Code Online (Sandbox Code Playgroud)

另一个尝试:

( VALUES
      ('UK',  null::timestamp,  '2000-01-01'::timestamp),
      ('UK',  '2000-01-01',     '2000-01-01')
)

-- Fails:

> ERROR:  duplicate key value violates unique constraint 
      "country_activity_end_uq"
> DETAIL:  Key (country, activity_end)=(UK, 2000-01-01 00:00:00) 
      already exists.
Run Code Online (Sandbox Code Playgroud)

1b.

在所有这些之后,我们可以发现表activity中并不真正需要它,因为我们有开始和结束,我们可以计算它。因此它可以被删除:

CREATE TABLE expenses_commissionrule (
    id serial NOT NULL PRIMARY KEY,
    country varchar(2) NOT NULL,
    created timestamp with time zone NOT NULL,
    modified timestamp with time zone NOT NULL,
    activity_start timestamp,
    activity_end timestamp,
    CONSTRAINT non_overlapping_activity
        EXCLUDE USING GIST 
            (country WITH =, 
             tsrange(activity_start, activity_end, '[)') WITH &&),
    CONSTRAINT country_activity_end_uq
        UNIQUE (country, activity_end),
    CONSTRAINT activity_start_end_fk
        FOREIGN KEY (country, activity_start)
        REFERENCES expenses_commissionrule (country, activity_end)
) ;

-- plus the two filtered indexes. We do need those.
Run Code Online (Sandbox Code Playgroud)

1c。

然后我们意识到——由于我们添加了外键——我们真的不再需要排除约束了。我们可以通过activity_end在 之后强制执行来达到相同的效果activity_start

CREATE TABLE expenses_commissionrule (
    id serial NOT NULL PRIMARY KEY,
    country varchar(2) NOT NULL,
    created timestamp with time zone NOT NULL,
    modified timestamp with time zone NOT NULL,
    activity_start timestamp,
    activity_end timestamp,
    CONSTRAINT non_overlapping_activity
        CHECK (activity_start < activity_end),
    CONSTRAINT country_activity_end_uq
        UNIQUE (country, activity_end),
    CONSTRAINT activity_start_end_fk
        FOREIGN KEY (country, activity_start)
        REFERENCES expenses_commissionrule (country, activity_end)
) ;


-- plus the two filtered indexes. We do need those.
Run Code Online (Sandbox Code Playgroud)

第 2 章:无清单

在所有繁琐的努力之后,让我们尝试一些更简单的事情。这次不解释了,先看代码再解释:

CREATE TABLE ec_rule (
    id serial NOT NULL PRIMARY KEY,
    country varchar(2) NOT NULL,
    created timestamp with time zone NOT NULL,
    modified timestamp with time zone NOT NULL,
    activity_end timestamp,
    CONSTRAINT country_activity_end_uq
        UNIQUE (country, activity_end)
) ;

CREATE UNIQUE INDEX country_end_ufx
    ON ec_rule (country)
    WHERE (activity_end IS NULL) ;
Run Code Online (Sandbox Code Playgroud)

好的,这里发生了什么?这要简单得多,它不可能像以前的设计一样使用!它不可能是等效的。或者也许可以?

让我们看看这里发生了什么:

  • activity_start完全失踪。如果它们没有存储在表中,我们应该如何找到它们?

答案是 Activity start 确实已存储,只是与 Activity end 不在同一行中。这就是外键的全部意义,以确保每一端都有一个匹配的开始。因此,我们可以使用以下命令轻松找到每个时期的开始LAG()

CREATE VIEW expenses_commissionrule AS
SELECT 
    id,
    country,
    created,
    modified,
    LAG(activity_end) OVER (PARTITION BY country
                            ORDER BY activity_end)
        AS activity_start,
    activity_end
FROM
    ec_rule ;
Run Code Online (Sandbox Code Playgroud)

第 3 章:找出差异

虽然上述所有设计都设法强制执行“无间隙”规则,但它们都未能强制执行第二条规则:“所有间隔的总和应该是 ” -INF : +INF

这个可以修改吗?毕竟,一旦我们没有差距,这似乎更容易了。

嗯,是和不是。和不。这并不容易。这类似于强制一个表至少有一行。这看起来也很容易,但实际上很难,如果单独使用 DDL 是不可能的。

但是,对于特定问题,“是和否”意味着规则:

  • 可以通过设计 1 来强制执行(尽管它使它变得更加复杂)。

  • 但不是设计 2(至少我看不到方法)。

对于设计 1,我们需要添加第二个外键(从活动结束到下一个开始)和唯一约束(国家/地区,activity_start)。这基本上会将我们的列表转换为双向链表。并且要满足外键,列表必须是无限的(这是不可能的)或左右两端都有结尾,这意味着两行有空值,一列左边,一列右边(外键是如果其中一列是NULL) ,则满意。

对于设计 2,您必须确保 - 在 DDL 之外 - 每个国家/地区都有一行 where activity_endis NULL(该行是期间的规则+Infinity)。