Chr*_*ers 14 postgresql database-design data-integrity
我正在使用 PostgreSQL,但我认为大多数高端数据库必须具有一些类似的功能,而且,它们的解决方案可能会启发我的解决方案,所以不要考虑这个特定于 PostgreSQL 的解决方案。
我知道我不是第一个尝试解决这个问题的人,所以我认为这里值得一问,但我正在尝试评估建模会计数据的成本,以便从根本上平衡每笔交易。会计数据是仅附加的。此处的总体约束(以伪代码编写)可能大致如下:
CREATE TABLE journal_entry (
id bigserial not null unique, --artificial candidate key
journal_type_id int references journal_type(id),
reference text, -- source document identifier, unique per journal
date_posted date not null,
PRIMARY KEY (journal_type_id, reference)
);
CREATE TABLE journal_line (
entry_id bigint references journal_entry(id),
account_id int not null references account(id),
amount numeric not null,
line_id bigserial not null unique,
CHECK ((sum(amount) over (partition by entry_id) = 0) -- this won't work
);
Run Code Online (Sandbox Code Playgroud)
显然,这样的检查约束永远不会起作用。它按行操作,可能会检查整个数据库。所以它总是会失败并且做起来很慢。
所以我的问题是对这种约束进行建模的最佳方法是什么?到目前为止,我基本上已经研究了两个想法。想知道这些是否是唯一的,或者是否有人有更好的方法(除了将其留给应用程序级别或存储过程)。
我正在权衡这些与当前在存储过程中强制执行逻辑的方法。正在权衡复杂性成本与约束的数学证明优于单元测试的想法。上面#1 的主要缺点是,作为元组的类型是 PostgreSQL 中的那些领域之一,在这些领域中,人们会经常遇到不一致的行为和假设的变化,所以我什至希望这个领域的行为可能会随着时间的推移而改变。设计一个未来的安全版本并不是那么容易。
是否有其他方法可以解决这个问题,将每个表中的记录扩展到数百万条?我错过了什么吗?我错过了权衡吗?
为了回应 Craig 在下面关于版本的观点,至少,它必须在 PostgreSQL 9.2 及更高版本上运行(可能是 9.1 及更高版本,但可能我们可以直接使用 9.2)。
Erw*_*ter 13
因为我们必须跨越多行,所以不能用简单的CHECK
约束来实现。
我们也可以排除排除约束。这些将跨越多行,但只检查不平等。像对多行求和这样的复杂操作是不可能的。
似乎最适合您的情况的工具是CONSTRAINT TRIGGER
(或者甚至只是一个普通的TRIGGER
- 当前实现的唯一区别是您可以使用SET CONSTRAINTS
.
所以这是你的选择 2。
一旦我们可以依赖始终强制执行的约束,我们就不需要再检查整个表了。只检查当前事务中插入的行——在事务结束时——就足够了。性能应该没问题。
另外,作为
会计数据是仅附加的。
...我们只需要关心新插入的行。(假设UPDATE
或DELETE
不可能。)
我使用系统列xid
并将其与txid_current()
返回xid
当前事务的函数进行比较。为了比较类型,需要强制转换......
这应该是相当安全的。考虑这个相关的,稍后用更安全的方法回答:
CREATE TABLE journal_line(amount int); -- simplistic table for demo
CREATE OR REPLACE FUNCTION trg_insaft_check_balance()
RETURNS trigger AS
$func$
BEGIN
IF sum(amount) <> 0
FROM journal_line
WHERE xmin::text::bigint = txid_current() -- consider link above
THEN
RAISE EXCEPTION 'Entries not balanced!';
END IF;
RETURN NULL; -- RETURN value of AFTER trigger is ignored anyway
END;
$func$ LANGUAGE plpgsql;
CREATE CONSTRAINT TRIGGER insaft_check_balance
AFTER INSERT ON journal_line
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE PROCEDURE trg_insaft_check_balance();
Run Code Online (Sandbox Code Playgroud)
Deferred,所以它只在事务结束时检查。
INSERT INTO journal_line(amount) VALUES (1), (-1);
Run Code Online (Sandbox Code Playgroud)
作品。
INSERT INTO journal_line(amount) VALUES (1);
Run Code Online (Sandbox Code Playgroud)
失败:
错误:条目不平衡!
BEGIN;
INSERT INTO journal_line(amount) VALUES (7), (-5);
-- do other stuff
SELECT * FROM journal_line;
INSERT INTO journal_line(amount) VALUES (-2);
-- INSERT INTO journal_line(amount) VALUES (-1); -- make it fail
COMMIT;
Run Code Online (Sandbox Code Playgroud)
作品。:)
如果您需要在事务结束之前强制执行约束,您可以在事务的任何时候执行此操作,甚至在开始时:
SET CONSTRAINTS insaft_check_balance IMMEDIATE;
Run Code Online (Sandbox Code Playgroud)
如果您使用多行操作,INSERT
则触发每个语句会更有效 - 这对于约束触发器是不可能的:
只能指定约束触发器
FOR EACH ROW
。
改用普通触发器并开火FOR EACH STATEMENT
以...
SET CONSTRAINTS
。回复您的评论:如果DELETE
可能,您可以添加类似的触发器,在DELETE 发生后进行全表余额检查。这会贵得多,但不会有太大影响,因为这种情况很少发生。