对子集聚合建模约束?


14

我使用的是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
);

显然,这样的检查约束永远不会起作用。它按行运行,并可能检查整个数据库。因此,它将始终失败,并且执行起来会很缓慢。

所以我的问题是建模此约束的最佳方法是什么?到目前为止,我基本上已经看了两个想法。想知道这是否是唯一的方法,或者是否有人有更好的方法(除了将其置于应用程序级别或存储的过程之外)。

  1. 我可以借鉴会计界关于原始账簿和最终账簿(一般日记帐与总帐)之间的区别的概念。在这方面,我可以将此模型建模为附加到日记帐分录上的日记帐行,对数组施加约束(以PostgreSQL的术语,从unnest(je.line_items)中选择sum(amount)= 0。将这些保存到订单项表中,在该表中可以更容易地实施各个列约束,而在索引等处可能更有用,这就是我所追求的方向。
  2. 我可以尝试编写一个约束触发器,该触发器将针对每个事务强制执行此操作,其思路是一系列0的总和始终为0。

我将这些与当前在存储过程中执行逻辑的方法进行权衡。相对于约束条件的数学证明优于单元测试的想法,要权衡复杂性成本。上面#1的主要缺点是,作为元组的类型是PostgreSQL中的那些区域之一,该区域会出现不一致的行为并定期改变假设,因此我什至希望该区域的行为会随着时间而改变。设计将来的安全版本并非易事。

还有其他方法可以解决此问题,这些问题将在每个表中扩展到数百万条记录吗?我想念什么吗?我错过了权衡吗?

为了回应Craig关于以下版本的观点,至少必须在PostgreSQL 9.2及更高版本(也许是9.1及更高版本,但我们可以直接使用9.2)上运行。

Answers:


12

由于我们必须跨越多行,因此无法通过简单的CHECK约束来实现。

我们还可以排除排除约束。这些将跨越多行,但仅检查不平等。诸如多行总和之类的复杂操作是不可能的。

似乎最适合您的情况的工具是CONSTRAINT TRIGGER(甚至只是一个简单的工具TRIGGER-当前实现中的唯一区别是您可以使用调整触发的时间SET CONSTRAINTS

这就是您的选择2

一旦我们可以一直依赖于强制执行的约束,就不再需要检查整个表。仅检查在当前事务中插入的行(在事务结束时)就足够了。性能应该还可以。

另外,作为

会计数据是仅追加的。

...我们只需要关心新插入的行。(假设UPDATE还是DELETE不可能。)

我使用system列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();

Deferred,因此仅在事务结束时进行检查。

测验

INSERT INTO journal_line(amount) VALUES (1), (-1);

作品。

INSERT INTO journal_line(amount) VALUES (1);

失败:

错误:条目不平衡!

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;

作品。:)

如果您需要在事务结束之前强制执行约束,则可以在事务的任何时候(甚至在开始时)执行此操作:

SET CONSTRAINTS insaft_check_balance IMMEDIATE;

普通触发速度更快

如果您使用多行操作,则按INSERT语句触发更有效- 使用约束触发器不可能的

只能指定约束触发器FOR EACH ROW

改用普通触发器,然后触发FOR EACH STATEMENT...

  • 失去了选择SET CONSTRAINTS
  • 获得性能。

可能删除

回复您的评论:如果DELETE可能,您可以添加类似的触发器,在发生DELETE之后执行全表余额检查。这会贵得多,但没有什么关系,因为这种情况很少发生。


这是对项目2的投票。好处是您只有一个表来处理所有约束,这在那是一项复杂的工作,但是另一方面,您将设置本质上是过程性的触发器,因此,如果我们对未声明性证明的单元测试进行测试,那么将获得更多复杂。您如何权衡使用具有声明性约束的嵌套存储呢?
克里斯·特拉弗斯

更新也是不可能的,删除可能在某些情况下*,但是几乎可以肯定是一个非常狭窄且经过充分测试的过程。出于实际目的,删除可以作为约束问题而忽略。*例如,清除所有10年以上的数据,这只有在使用日志,聚合和快照模型的情况下才有可能,这在会计系统中还是很常见的。
克里斯·特拉弗斯

@ChrisTravers。我添加了一个更新并解决了可能DELETE。我不知道会计中通常需要什么,也不知道我的专业领域。只是试图提供一个(非常有效的IMO)解决方案来解决所描述的问题。
Erwin Brandstetter'9

@Erwin Brandstetter我不会为删除而担心。如果适用,删除操作将受到更大的限制,单元测试几乎是不可避免的。我最想知道关于复杂性成本的想法。无论如何,删除都可以使用on delete级联fkey非常简单地解决。
克里斯·特拉弗斯

4

以下SQL Server解决方案仅使用约束。我在系统的多个地方都使用了类似的方法。

CREATE TABLE dbo.Lines
  (
    EntryID INT NOT NULL ,
    LineNumber SMALLINT NOT NULL ,
    CONSTRAINT PK_Lines PRIMARY KEY ( EntryID, LineNumber ) ,
    PreviousLineNumber SMALLINT NOT NULL ,
    CONSTRAINT UNQ_Lines UNIQUE ( EntryID, PreviousLineNumber ) ,
    CONSTRAINT CHK_Lines_PreviousLineNumber_Valid CHECK ( ( LineNumber > 0
            AND PreviousLineNumber = LineNumber - 1
          )
          OR ( LineNumber = 0 ) ) ,
    Amount INT NOT NULL ,
    RunningTotal INT NOT NULL ,
    CONSTRAINT UNQ_Lines_FkTarget UNIQUE ( EntryID, LineNumber, RunningTotal ) ,
    PreviousRunningTotal INT NOT NULL ,
    CONSTRAINT CHK_Lines_PreviousRunningTotal_Valid CHECK 
        ( PreviousRunningTotal + Amount = RunningTotal ) ,
    CONSTRAINT CHK_Lines_TotalAmount_Zero CHECK ( 
            ( LineNumber = 0
                AND PreviousRunningTotal = 0
              )
              OR ( LineNumber > 0 ) ),
    CONSTRAINT FK_Lines_PreviousLine 
        FOREIGN KEY ( EntryID, PreviousLineNumber, PreviousRunningTotal )
        REFERENCES dbo.Lines ( EntryID, LineNumber, RunningTotal )
  ) ;
GO

-- valid subset inserts
INSERT INTO dbo.Lines(EntryID ,
        LineNumber ,
        PreviousLineNumber ,
        Amount ,
        RunningTotal ,
        PreviousRunningTotal )
VALUES(1, 0, 2, 10, 10, 0),
(1, 1, 0, -5, 5, 10),
(1, 2, 1, -5, 0, 5);

-- invalid subset fails
INSERT INTO dbo.Lines(EntryID ,
        LineNumber ,
        PreviousLineNumber ,
        Amount ,
        RunningTotal ,
        PreviousRunningTotal )
VALUES(2, 0, 1, 10, 10, 5),
(2, 1, 0, -5, 5, 10) ;

这是一个有趣的方法。那里的约束似乎适用于语句而不是元组或事务级别,对吗?这也意味着您的子集具有内置的子集排序,对吗?这确实是一种令人着迷的方法,尽管它绝对不能直接转换为Pgsql,但它仍在激发灵感。谢谢!
克里斯·特拉弗斯

@克里斯:我认为它在Postgres的就好了(去掉后dbo.GO):SQL-小提琴
ypercubeᵀᴹ

好吧,我误会了。看起来确实可以在这里使用类似的解决方案。但是,为了安全起见,您是否不需要单独的触发器来查找前一行的小计?否则,您将信任您的应用程序发送健全的数据,对吗?它仍然是我可能能够适应的有趣模型。
克里斯·特拉弗斯

顺便说一句,赞成两个解决方案。最好将另一个列出,因为它看起来不太复杂。但是,我认为这是一个非常有趣的解决方案,它为我提供了思考非常复杂的约束的新方法。谢谢!
克里斯·特拉弗斯

而且为了安全起见,您无需任何触发器即可查询前一行的小计。FK_Lines_PreviousLine外键约束对此很小心。
ypercubeᵀᴹ
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.