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。
问题:如何添加第二个约束?用例:我们有一个无限期的规则。我想切换到新规则,该规则应从下个月开始。在这种情况下,我想将当前规则结束期设置为当月月底,并在单个操作中添加新规则。
更新:将来我想添加以下行为:
换句话说,所有先前的约束都应仅应用于 user_id 为 NULL 的行。如何做到这一点?
PostgreSQL 版本:9.6.2
第 1 章:链表
在数据库中强制执行这种(无间隙)类型的约束的一种方法是拆分activity为开始和结束部分,然后使用UNIQUE和FOREIGN KEY约束来模拟链接列表。
1a.
activity_start应该参考前一个activity_end: (country, activity_start) REFERENCES (country, activity_end)。activity_start和country或活动结束和国家:对 的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)。
| 归档时间: |
|
| 查看次数: |
685 次 |
| 最近记录: |