跨表中所有数据的复杂约束

Kay*_*Ess 4 postgresql constraint transaction data-integrity postgresql-8.4

我们有一个表来记录系统上发生的处理,我们需要确保只有一行处于“处理中”状态。

我想确保结果始终为零或一:

select count(id) from jobs where status in ('P', 'G');
Run Code Online (Sandbox Code Playgroud)

我们正在使用显式事务,因此理想情况下,此检查将在提交时进行,如果不变量不成立则中止事务。对于我们来说,处理任何偶尔会引发错误的异常处理比突然以多个“正在进行”的工作结束要容易得多。

该解决方案只需要与 Postgres 一起使用,因此我们很乐意为此采用非标准解决方案。我们目前使用 8.4,但如果有任何不同,我们将在某个时候升级到 9.x。

Erw*_*ter 7

用 Postgres 9.1 & 9.2测试。其中大部分也应该适用于 8.4。

简单案例

如果中间状态在单个事务的过程中不违反约束,则常量值上的部分 UNIQUE 索引可以完成这项工作。鉴于此测试用例:

CREATE TEMP TABLE jobs(jobs_id int primary key, status text);
INSERT INTO jobs (jobs_id, status)
VALUES (1, 'A'), (2, 'B'), (3, 'C'), (4, 'G');
Run Code Online (Sandbox Code Playgroud)

使用这个索引:

CREATE UNIQUE INDEX jobs_status_uni_idx ON jobs ((TRUE))
WHERE status in ('P', 'G');
Run Code Online (Sandbox Code Playgroud)

请注意值周围的附加括号TRUE

测试(一次一行):

INSERT INTO jobs (jobs_id, status) VALUES (5, 'G'); -- fails
INSERT INTO jobs (jobs_id, status) VALUES (5, 'P'); -- fails

DELETE FROM jobs WHERE status = 'G';

INSERT INTO jobs (jobs_id, status) VALUES (5, 'P'); -- succeeds
INSERT INTO jobs (jobs_id, status) VALUES (6, 'G'); -- fails
Run Code Online (Sandbox Code Playgroud)

SQL小提琴。

有关更多解释的相关答案:
PostgreSQL 多列唯一约束和 NULL 值

DEFERRED约束的高级案例

理想情况下,此检查将在提交时进行

一个部分唯一索引不能被推迟,并始终立即检查。因此,继续上面的例子,这将失败:

BEGIN;
INSERT INTO jobs (jobs_id, status) VALUES (6, 'G');  -- fails immediately!
DELETE FROM jobs WHERE status = 'P';
COMMIT;
Run Code Online (Sandbox Code Playgroud)

示例是显式事务处理。您的客户端可能具有自动事务处理(autocommit打开或关闭)。

如果你需要这个来工作,你需要一个DEFERRABLE约束,INITIALLY DEFERRED或者SET CONSTRAINTS ALL | 交易中的名称 DEFERRED

在这里引用手册

非延迟唯一性约束

当 a UNIQUEorPRIMARY KEY约束不可延迟时,每当插入或修改行时,PostgreSQL 都会立即检查唯一性。SQL 标准规定唯一性应该只在语句的末尾强制执行;例如,当单个命令更新多个键值时,这会有所不同。要获得符合标准的行为,请将约束声明为DEFERRABLE但不延迟(即INITIALLY IMMEDIATE)。请注意,这可能比立即唯一性检查慢得多。

手册中有关索引唯一性检查的更多详细信息。

但是, aUNIQUE CONSTRAINT只能为定义,不能为表达式定义。
为此目的添加一个冗余列并使其与触发器保持同步。像这样:

ALTER TABLE jobs ADD COLUMN status_uni boolean;
UPDATE jobs set status_uni = TRUE WHERE status in ('P', 'G');  -- rest stays NULL
Run Code Online (Sandbox Code Playgroud)
ALTER TABLE jobs ADD CONSTRAINT jobs_status_uni
UNIQUE(status_uni) DEFERRABLE INITIALLY DEFERRED;
Run Code Online (Sandbox Code Playgroud)

附加列很便宜,因为它填充了 NULL,现有的NULL 位图通常应该吞下它,而无需额外的物理磁盘空间。
NULL值不违反每个定义的唯一约束。

创建触发器以始终保持列最新:

CREATE OR REPLACE FUNCTION trg_jobs_status()
  RETURNS trigger AS
$func$
BEGIN
NEW.status_uni = (NEW.status IN ('G', 'P') OR NULL);
RETURN NEW;
END
$func$  LANGUAGE plpgsql;
Run Code Online (Sandbox Code Playgroud)

对于所有插入:

CREATE TRIGGER insbef
BEFORE INSERT ON jobs
FOR EACH ROW EXECUTE PROCEDURE trg_jobs_status();
Run Code Online (Sandbox Code Playgroud)

优化,只针对相关更新:

CREATE TRIGGER upbef
BEFORE UPDATE OF status, status_uni ON jobs
FOR EACH ROW EXECUTE PROCEDURE trg_jobs_status();
Run Code Online (Sandbox Code Playgroud)

测试

现在你的交易会通过,只要最后状态是一致的

BEGIN;
-- SET CONSTRAINTS jobs_status_uni IMMEDIATE;  -- only for INITIALLY IMMEDIATE
INSERT INTO jobs (jobs_id, status) VALUES (6, 'G');  -- check deferred
DELETE FROM jobs WHERE status = 'P';
COMMIT;  -- succeeds!
Run Code Online (Sandbox Code Playgroud)

SQL小提琴