Batasan pemodelan pada agregat subset?

14

Saya menggunakan PostgreSQL tapi saya pikir sebagian besar top-end db harus memiliki beberapa kemampuan yang serupa, dan terlebih lagi, solusi bagi mereka dapat menginspirasi solusi bagi saya, jadi jangan pertimbangkan PostgreSQL khusus ini.

Saya tahu saya bukan orang pertama yang mencoba menyelesaikan masalah ini, jadi saya pikir pantas untuk ditanyakan di sini, tetapi saya mencoba untuk mengevaluasi biaya pemodelan data akuntansi sehingga setiap transaksi seimbang secara fundamental. Data akuntansi hanya ditambahkan. Batasan keseluruhan (ditulis dalam pseudo-code) di sini mungkin terlihat kira-kira seperti:

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

Jelas kendala pemeriksaan seperti itu tidak akan pernah berhasil. Ini beroperasi per baris dan mungkin memeriksa seluruh db. Jadi itu akan selalu gagal dan lambat melakukannya.

Jadi pertanyaan saya adalah apa cara terbaik untuk memodelkan batasan ini? Saya pada dasarnya telah melihat dua ide sejauh ini. Ingin tahu apakah ini satu-satunya, atau jika seseorang memiliki cara yang lebih baik (selain menyerahkannya ke tingkat aplikasi atau proc yang disimpan).

  1. Saya bisa meminjam halaman dari konsep dunia akuntansi tentang perbedaan antara buku entri asli dan buku entri akhir (jurnal umum vs buku besar). Dalam hal ini saya bisa memodelkan ini sebagai array dari garis jurnal yang dilampirkan pada entri jurnal, menegakkan batasan pada array (dalam istilah PostgreSQL, pilih jumlah (jumlah) = 0 dari undest (je.line_items). Pemicu dapat berkembang dan simpan ini ke tabel item baris, di mana batasan kolom individu bisa lebih mudah ditegakkan, dan di mana indeks dll bisa lebih berguna. Ini adalah arah yang saya condongkan.
  2. Saya bisa mencoba kode pemicu kendala yang akan memberlakukan ini per transaksi dengan gagasan bahwa jumlah rangkaian 0 akan selalu 0.

Saya menimbang ini terhadap pendekatan saat ini menegakkan logika dalam prosedur tersimpan. Biaya kompleksitas ditimbang terhadap gagasan bahwa bukti matematika kendala lebih unggul daripada tes unit. Kelemahan utama # 1 di atas adalah bahwa tipe sebagai tupel adalah salah satu area di PostgreSQL di mana orang mengalami perilaku yang tidak konsisten dan perubahan asumsi secara teratur sehingga saya bahkan berharap perilaku di area ini dapat berubah seiring waktu. Merancang versi aman di masa depan tidaklah mudah.

Apakah ada cara lain untuk memecahkan masalah ini yang akan skala hingga jutaan catatan di setiap tabel? Apakah saya melewatkan sesuatu? Apakah ada tradeoff yang saya lewatkan?

Menanggapi poin Craig di bawah ini tentang versi, paling tidak, ini harus dijalankan pada PostgreSQL 9.2 dan lebih tinggi (mungkin 9,1 dan lebih tinggi, tetapi mungkin kita bisa langsung menggunakan 9.2).

Chris Travers
sumber

Jawaban:

12

Karena kita harus menjangkau beberapa baris, itu tidak dapat diimplementasikan dengan CHECKbatasan sederhana .

Kami juga dapat mengesampingkan batasan pengecualian . Itu akan span beberapa baris, tetapi hanya memeriksa ketidaksetaraan. Operasi kompleks seperti penjumlahan dari beberapa baris tidak dimungkinkan.

Alat yang tampaknya paling sesuai dengan kasus Anda adalah CONSTRAINT TRIGGER(Atau bahkan sekadar polos TRIGGER- satu-satunya perbedaan dalam implementasi saat ini adalah bahwa Anda dapat menyesuaikan waktu pemicu SET CONSTRAINTS.

Jadi itu pilihan Anda 2 .

Setelah kita dapat mengandalkan batasan yang ditegakkan setiap saat, kita tidak perlu memeriksa seluruh tabel lagi. Memeriksa hanya baris yang dimasukkan dalam transaksi saat ini - di akhir transaksi - sudah cukup. Kinerja harus ok.

Juga, sebagai

Data akuntansi hanya ditambahkan.

... kita hanya perlu peduli dengan baris yang baru saja dimasukkan . (Dengan asumsi UPDATEatau DELETEtidak mungkin.)

Saya menggunakan kolom sistem xiddan membandingkannya dengan fungsi txid_current()- yang mengembalikan xidtransaksi saat ini. Untuk membandingkan tipenya, casting diperlukan ... Ini harusnya cukup aman. Pertimbangkan jawaban terkait ini nanti dengan metode yang lebih aman:

Demo

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

Ditangguhkan , sehingga hanya diperiksa di akhir transaksi.

Tes

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

Bekerja

INSERT INTO journal_line(amount) VALUES (1);

Gagal:

GALAT: Entri tidak seimbang!

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;

Bekerja :)

Jika Anda perlu memaksakan kendala Anda sebelum akhir transaksi, Anda dapat melakukannya kapan saja dalam transaksi, bahkan di awal:

SET CONSTRAINTS insaft_check_balance IMMEDIATE;

Lebih cepat dengan pemicu biasa

Jika Anda beroperasi dengan multi-baris INSERT, ini lebih efektif untuk memicu per pernyataan - yang tidak mungkin dengan pemicu kendala :

Pemicu kendala hanya dapat ditentukan FOR EACH ROW.

Gunakan pemicu biasa dan gulir FOR EACH STATEMENTke ...

  • kehilangan opsi SET CONSTRAINTS.
  • mendapatkan kinerja.

HAPUS mungkin

Sebagai balasan untuk komentar Anda: Jika DELETEmemungkinkan, Anda dapat menambahkan pemicu serupa dengan melakukan pemeriksaan saldo seluruh tabel setelah DELETE terjadi. Ini akan jauh lebih mahal, tetapi tidak akan menjadi masalah karena jarang terjadi.

Erwin Brandstetter
sumber
Jadi ini adalah suara untuk item # 2. Keuntungannya adalah Anda hanya memiliki satu tabel untuk semua kendala dan itu adalah kompleksitas yang menang di sana, tetapi di sisi lain, Anda sedang menyiapkan pemicu yang pada dasarnya bersifat prosedural dan oleh karena itu jika kami unit menguji hal-hal yang tidak terbukti secara deklaratif, maka itu menjadi lebih rumit. Bagaimana Anda mempertimbangkan untuk tidak memiliki penyimpanan bersarang dengan batasan deklaratif?
Chris Travers
Pembaruan juga tidak dimungkinkan, penghapusan mungkin dalam keadaan tertentu * tetapi hampir pasti akan menjadi prosedur yang sangat sempit dan teruji dengan baik. Untuk tujuan praktis, hapus dapat diabaikan sebagai masalah kendala. * Misalnya, membersihkan semua data di atas 10 tahun yang hanya mungkin jika menggunakan model log, agregat, dan snapshot yang cukup khas dalam sistem akuntansi.
Chris Travers
@ChrisTravers. Saya menambahkan pembaruan dan membahas kemungkinan DELETE. Saya tidak akan tahu apa yang khas atau diperlukan dalam akuntansi - bukan bidang keahlian saya. Hanya mencoba memberikan solusi (IMO sangat efektif) untuk masalah yang dijelaskan.
Erwin Brandstetter
@ Erwin Brandstetter Saya tidak akan khawatir tentang itu untuk dihapus. Penghapusan, jika berlaku, akan dikenakan serangkaian kendala yang jauh lebih besar dan unit test hampir tidak dapat dihindarkan di sana. Saya kebanyakan bertanya-tanya tentang pemikiran tentang biaya kompleksitas. Bagaimanapun penghapusan dapat diselesaikan dengan sangat sederhana dengan pada kaskade kalkun delete.
Chris Travers
4

Solusi SQL Server berikut ini hanya menggunakan kendala. Saya menggunakan pendekatan serupa di banyak tempat di sistem saya.

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) ;
AK
sumber
itu pendekatan yang menarik. Kendala di sana tampaknya bekerja pada pernyataan daripada tingkat tuple atau transaksi, kan? Juga itu berarti bahwa himpunan bagian Anda memiliki subset pemesanan bawaan, benar? Itu pendekatan yang sangat menarik dan walaupun itu tidak diterjemahkan secara langsung ke Pgsql, itu masih merupakan ide yang menginspirasi. Terima kasih!
Chris Travers
@ Chris: Saya pikir itu berfungsi dengan baik di Postgres (setelah menghapus dbo.dan GO): sql-fiddle
ypercubeᵀᴹ
Ok, saya salah paham. Memang terlihat seperti orang dapat menggunakan solusi serupa di sini. Namun, tidakkah Anda memerlukan pemicu terpisah untuk mencari subtotal baris sebelumnya agar aman? Kalau tidak, Anda mempercayai aplikasi Anda untuk mengirim data yang waras, bukan? Ini masih merupakan model yang menarik yang mungkin bisa saya adaptasi.
Chris Travers
BTW, memutakhirkan kedua solusi. Akan daftar yang lain sebagai lebih disukai karena tampaknya kurang kompleks. Namun saya pikir ini adalah solusi yang sangat menarik dan membuka cara berpikir baru tentang kendala yang sangat kompleks bagi saya. Terima kasih!
Chris Travers
Dan Anda tidak perlu pemicu apa pun untuk mencari subtotal baris sebelumnya agar aman. Ini ditangani oleh FK_Lines_PreviousLinebatasan kunci asing.
ypercubeᵀᴹ