サブセット集合体の制約のモデリング?


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
);

明らかに、このようなチェック制約は機能しません。行ごとに動作し、データベース全体をチェックする場合があります。そのため、常に失敗し、実行が遅くなります。

私の質問は、この制約をモデル化する最良の方法は何ですか?私はこれまで基本的に2つのアイデアを見てきました。これらが唯一のものであるのか、誰かがより良い方法を持っているのか疑問に思う(アプリレベルまたはストアドプロシージャに任せる以外)。

  1. 元のエントリの本と最終エントリの本(一般仕訳帳と総勘定元帳)の違いの会計世界の概念からページを借りることができます。この点で、これをジャーナルエントリに添付されたジャーナル行の配列としてモデル化し、配列に制約を適用することができます(PostgreSQLの用語では、unnest(je.line_items)からsum(amount)= 0を選択します。これらを個々の列の制約をより簡単に実施でき、インデックスなどがより便利になる可能性のある明細テーブルに保存します。これが私が傾いている方向です。
  2. 一連の0の合計が常に0になるという考えで、トランザクションごとにこれを強制する制約トリガーをコーディングすることができます。

ストアドプロシージャでロジックを実行する現在のアプローチに対して、これらを比較検討しています。複雑さのコストは、制約の数学的証明が単体テストよりも優れているという考えと比較検討されています。上記#1の主な欠点は、タプルとしての型がPostgreSQLで一貫性のない動作や仮定の定期的な変更に遭遇する領域の1つであるため、この領域の動作が時間とともに変化することを期待することです。将来の安全なバージョンの設計はそれほど簡単ではありません。

この問題を解決するために、各テーブルで数百万件のレコードにスケールアップする他の方法はありますか?何か不足していますか?見逃したトレードオフはありますか?

バージョンに関する以下の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に対する投票です。利点は、すべての制約に対して単一のテーブルのみがあり、そこに複雑な勝利があることですが、他方では、本質的に手続き型のトリガーを設定しているため、宣言的に証明されていないことを単体テストしている場合、それはより多くなります複雑です。宣言的な制約を持つネストされたストレージを持つことに対して、どのように帽子に重みを付けますか?
クリストラバーズ

また、更新は不可能です。削除は特定の状況*で行われる場合がありますが、ほぼ確実に非常に狭い、十分にテストされた手順になります。実用的な目的のために、削除は制約の問題として無視できます。*たとえば、経理システムで非常に一般的なログ、集計、およびスナップショットモデルを使用する場合にのみ可能になる10年以上のすべてのデータをパージします。
クリス・トラヴァース

@ChrisTravers。アップデートを追加し、可能性に対処しましたDELETE。専門分野ではなく、会計で典型的なものや必要なものはわかりません。説明した問題に対する(かなり効果的なIMO)ソリューションを提供しようとしています。
アーウィンブランドステッター

@Erwin Brandstetter削除については心配しません。削除は、該当する場合、はるかに大きな制約の対象となり、単体テストはほとんど完全に避けられません。私は複雑さのコストに関する考えについてほとんど疑問に思っていました。いずれにせよ、削除はon delete cascade 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ᵀᴹ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.