부분 집합 집계에 대한 모델링 제약 조건


14

PostgreSQL을 사용하고 있지만 대부분의 최고급 DB에는 비슷한 기능이 있어야하며 그에 대한 솔루션이 나에게 솔루션을 불러 일으킬 수 있으므로이 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
);

분명히 그러한 검사 제약 조건은 작동하지 않습니다. 행마다 작동하며 전체 db를 점검 할 수 있습니다. 따라서 항상 실패하고 속도가 느려집니다.

내 질문은이 제약 조건을 모델링하는 가장 좋은 방법은 무엇입니까? 기본적으로 지금까지 두 가지 아이디어를 살펴 보았습니다. 이것들이 유일한 것인지 또는 누군가 더 나은 방법이 있는지 궁금합니다 (앱 레벨 또는 저장된 프로 시저로 두지 않는 것).

  1. 나는 원고와 최종 출입 장 간의 차이에 대한 회계 세계의 개념에서 페이지를 빌릴 수 있었다 (일반 저널 대 총계정 원장). 이와 관련하여 이것을 분개에 첨부 된 분개 라인 배열로 모델링하고 배열에 대한 제약 조건을 적용 할 수 있습니다 (PostgreSQL 용어로 unnest (je.line_items)에서 sum (amount) = 0을 선택하십시오. 개별 열 제약 조건을보다 쉽게 ​​적용 할 수 있고 인덱스 등이 더 유용한 광고 항목 테이블에 저장합니다.
  2. 일련의 0의 합계가 항상 0이라는 아이디어로 트랜잭션 당 이것을 강제하는 제약 조건 트리거를 코딩하려고 시도 할 수 있습니다.

나는 저장 프로 시저에서 논리를 적용하는 현재 접근 방식과 비교하여 이것들을 계량하고 있습니다. 복잡성 비용은 수학적 제약 증명이 단위 테스트보다 우수하다는 생각에 비추어지고 있습니다. 위의 # 1의 주요 단점은 튜플 유형은 PostgreSQL에서 일관성없는 동작과 가정의 규칙적인 변화가 발생하는 영역 중 하나이며,이 영역의 동작이 시간이 지남에 따라 변경되기를 바랍니다. 미래의 안전한 버전을 설계하는 것은 그리 쉬운 일이 아닙니다.

각 테이블에서 수백만 레코드까지 확장 할 수있는이 문제를 해결하는 다른 방법이 있습니까? 뭔가 빠졌습니까? 내가 놓친 트레이드 오프가 있습니까?

버전에 대한 Craig의 요점에 대한 응답으로 최소한 PostgreSQL 9.2 이상에서 실행해야 할 것입니다 (9.1 이상이지만 아마도 9.2로 계속 갈 수 있음).

답변:


12

여러 행에 걸쳐 있어야 하므로 간단한 제약 조건 으로 구현할 수 없습니다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();

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 번 항목에 대한 투표입니다. 장점은 모든 제약 조건에 대해 하나의 테이블 만 가지고 있고 거기에서 복잡하다는 것입니다. 그러나 다른 한편으로는 본질적으로 절차 적 인 트리거를 설정하고 있기 때문에 선언적으로 입증되지 않은 것을 단위 테스트하면 더 많이 얻을 수 있습니다 복잡한. 선언적 제약 조건을 가진 중첩 스토리지를 사용하는 것에 대해 어떻게 모자를 쓰시겠습니까?
Chris Travers

또한 업데이트가 불가능합니다. 삭제는 특정 상황에서 발생할 수 있지만 거의 확실하게 테스트 과정이 매우 좁습니다. 실제적인 목적으로 삭제는 제약 조건 문제로 무시 될 수 있습니다. 예를 들어, 어쨌든 회계 시스템에서 일반적으로 나타나는 로그, 집계 및 스냅 샷 모델을 사용하는 경우에만 가능한 10 세 이상의 모든 데이터를 제거합니다.
Chris Travers

@ChrisTravers. 나는 업데이트를 추가하고 가능한 해결했다 DELETE. 나는 전문 분야가 아닌 회계에서 일반적이거나 필요한 것이 무엇인지 알지 못했습니다. 설명 된 문제에 대해 (꽤 효과적인 IMO) 솔루션을 제공하려고합니다.
Erwin Brandstetter

@ Erwin Brandstetter 삭제에 대해 걱정하지 않아도됩니다. 해당되는 경우 삭제는 훨씬 더 큰 제약 조건이 적용되며 단위 테스트는 거의 피할 수 없습니다. 복잡성 비용에 대한 생각이 궁금했습니다. 여하튼 삭제는 on delete cascade fkey를 사용하여 매우 간단하게 해결할 수 있습니다.
Chris Travers

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로 직접 번역되지는 않지만 여전히 영감을주는 아이디어입니다. 감사!
Chris Travers

@ 크리스 : 나는 그것을합니다 (제거한 후 포스트 그레스에서 잘 작동 생각 dbo.하고 GO:) SQL - 바이올린
ypercubeᵀᴹ

좋아, 나는 그것을 오해하고 있었다. 비슷한 솔루션을 사용할 수있는 것처럼 보입니다. 그러나 안전을 위해 이전 줄의 소계를 조회하기 위해 별도의 트리거가 필요하지 않습니까? 그렇지 않으면 당신은 제정신 데이터를 보내도록 앱을 신뢰하고 있습니까? 여전히 적응할 수있는 흥미로운 모델입니다.
Chris Travers

BTW는 두 솔루션을 모두 상향 조정했습니다. 덜 복잡해 보이기 때문에 다른 것을 선호하는 것으로 나열하려고합니다. 그러나 이것은 매우 흥미로운 해결책이라고 생각하며 매우 복잡한 제약 조건에 대한 새로운 사고 방식을 열어줍니다. 감사!
Chris Travers

그리고 안전을 위해 이전 줄의 소계를 조회하는 트리거가 필요하지 않습니다. 이것은 FK_Lines_PreviousLine외래 키 제약 조건에 의해 처리됩니다 .
ypercubeᵀᴹ
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.