Alt küme toplamalarında modelleme kısıtlamaları?


14

PostgreSQL kullanıyorum, ancak üst düzey db'lerin çoğunun bazı benzer yeteneklere sahip olması gerektiğini ve dahası, onlar için çözümlerin benim için çözümlere ilham verebileceğini, bu PostgreSQL'e özel olduğunu düşünmeyin.

Bu sorunu çözmeye çalışan ilk kişi olmadığımı biliyorum, bu yüzden burada sormaya değer olduğunu düşünüyorum ama muhasebe verilerini modelleme maliyetlerini her işlem temel olarak dengeli olacak şekilde değerlendirmeye çalışıyorum. Muhasebe verileri yalnızca ektedir. Buradaki genel kısıt (sözde kodla yazılmış) kabaca şöyle görünebilir:

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

Açıkçası böyle bir kontrol kısıtı asla işe yaramayacaktır. Satır başına çalışır ve tüm db'yi kontrol edebilir. Bu yüzden her zaman başarısız olur ve bunu yapmak yavaş olur.

Benim sorum şu, bu kısıtlamayı modellemenin en iyi yolu nedir? Temelde şu ana kadar iki fikre baktım. Bunların sadece bunlar mı, yoksa birinin daha iyi bir yolu olup olmadığını merak etmek (uygulama seviyesine veya depolanmış bir proc'a bırakmak dışında).

  1. Muhasebe dünyasının orijinal giriş kitabı ile nihai giriş kitabı (genel dergi ile genel muhasebe defteri) arasındaki farktan bir sayfa ödünç alabilirim. Bu bağlamda, bunu günlük girişine bağlı bir günlük satırı dizisi olarak modelleyebilir, dizi üzerindeki kısıtlamayı uygulayabilirim (PostgreSQL açısından, dürüst olmayandan (je.line_items) toplamı (miktar) = 0 seçin. bunları tek tek sütun kısıtlamalarının daha kolay uygulanabileceği ve dizinlerin vb. daha kullanışlı olabileceği satır öğeleri tablosuna kaydedin.
  2. 0'ın bir dizi toplamı her zaman 0 olacağı fikri ile bu işlem başına zorunlu kılan bir kısıtlama tetik kod çalıştırabilirsiniz.

Bunları mantığın saklı bir yordamda uygulanmasına yönelik mevcut yaklaşıma göre tartıyorum. Karmaşıklık maliyeti, kısıtlamaların matematiksel kanıtının birim testlerden üstün olduğu fikrine karşı tartılmaktadır. Yukarıdaki # 1'in en büyük dezavantajı, tuples gibi türlerin PostgreSQL'de düzenli olarak tutarsız davranışlara ve varsayımlardaki değişikliklere maruz kaldığı alanlardan biridir ve bu alandaki davranışların zamanla değişebileceğini umuyorum. Gelecekteki güvenli bir sürümü tasarlamak o kadar kolay değil.

Bu sorunu çözmek için her tabloda milyonlarca kaydı ölçeklendirecek başka yollar var mı? Bir şey mi kaçırıyorum? Kaçırdığım bir ödünleşim var mı?

Craig'in sürümlerle ilgili aşağıdaki noktaya cevaben, en azından, PostgreSQL 9.2 ve daha üstü (belki 9.1 ve üstü, ancak muhtemelen düz 9.2 ile gidebiliriz) çalışması gerekecektir.

Yanıtlar:


13

Birden fazla satıra yaymamız gerektiğinden , basit bir CHECKkısıtlamayla uygulanamaz.

Ayrıca, hariç tutma kısıtlamalarını da ekarte edebiliriz . Bunlar birden çok satıra yayılır, ancak yalnızca eşitsizliği kontrol eder. Birden fazla satır üzerinden bir toplam gibi karmaşık işlemler mümkün değildir.

Durumunuza en uygun araç bir CONSTRAINT TRIGGER(veya sadece bir düz TRIGGER- mevcut uygulamadaki tek fark, tetikleyicinin zamanlamasını ayarlayabilmenizdir SET CONSTRAINTS.

Bu seçenek 2 .

Her zaman uygulanan kısıtlamaya güvenebildiğimizde, artık tüm tabloyu kontrol etmemiz gerekmiyor. Yalnızca geçerli işleme eklenen işlemlerin ( işlemin sonunda) kontrol edilmesi yeterlidir. Performans iyi olmalıdır.

Aynı zamanda

Muhasebe verileri yalnızca ektedir.

... sadece yeni eklenen satırları önemsememiz gerekiyor . ( Mümkün UPDATEveya DELETEmümkün değildir.)

Sistem sütununu kullanıyorum xidve geçerli işlemin txid_current()döndürdüğü işleve karşılaştırıyorum xid. Tipleri karşılaştırmak için döküm gereklidir ... Bu oldukça güvenli olmalıdır. Bu daha sonra daha güvenli bir yöntemle ilgili cevabı düşünün:

gösteri

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

Ertelendi , bu nedenle yalnızca işlemin sonunda kontrol edilir.

Testler

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

İşler.

INSERT INTO journal_line(amount) VALUES (1);

başarısız:

HATA: Girişler dengeli değil!

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;

İşler. :)

İşleminizin bitiminden önce kısıtlamanızı zorunlu kılmanız gerekiyorsa, bunu işlemin herhangi bir noktasında, başlangıçta bile yapabilirsiniz:

SET CONSTRAINTS insaft_check_balance IMMEDIATE;

Düz tetiklemeli daha hızlı

Çok satırlı INSERTçalışıyorsanız, ifade başına tetiklemek daha etkilidir - kısıtlama tetikleyicileriyle mümkün değildir :

Kısıtlama tetikleyicileri yalnızca belirtilebilir FOR EACH ROW.

Bunun yerine düz bir tetikleyici kullanın ve FOR EACH STATEMENT...

  • seçeneğini kaybetmek SET CONSTRAINTS.
  • performans kazanmak.

DELETE mümkün

DELETEYorumunuza yanıt olarak: Mümkünse , bir DELETE gerçekleştikten sonra tüm tablodaki bakiye kontrolünü yaparak benzer bir tetikleyici ekleyebilirsiniz . Bu çok daha pahalı olurdu, ancak nadiren olduğu kadar önemli olmayacak.


Yani bu 2. maddeye verilen oy. Avantajı, tüm kısıtlamalar için sadece tek bir tablonuz olması ve buradaki bir karmaşıklık kazanmasıdır, ancak diğer yandan, esas olarak prosedürel olan tetikleyiciler oluşturuyorsunuz ve bu nedenle, kanıtlanmamış kanıtlanmamış şeyleri birim test edersek, karmaşık. Açıklayıcı kısıtlamalara sahip iç içe bir depolamaya karşı nasıl şapka yüklersiniz?
Chris Travers

Ayrıca güncelleme mümkün değildir, silme belirli koşullar altında * olabilir, ancak neredeyse kesinlikle çok dar, iyi test edilmiş bir prosedür olacaktır. Pratik amaçlar için, silme bir sınırlama sorunu olarak göz ardı edilebilir. * Örneğin, yalnızca muhasebe sistemlerinde oldukça tipik olan bir günlük, toplu ve anlık görüntü modeli kullanıldığında mümkün olabilecek 10 yaşın üzerindeki tüm verilerin temizlenmesi.
Chris Travers

@ChrisTravers. Bir güncelleme ekledim ve mümkün olana değindim DELETE. Muhasebe alanında neyin tipik veya gerekli olduğunu bilmiyorum - uzmanlık alanım değil. Sadece açıklanan soruna (oldukça etkili bir IMO) çözümü sağlamaya çalışıyoruz.
Erwin Brandstetter

@Erwin Brandstetter Silme işlemleri için endişelenmezdim. Silinmeler, eğer uygulanabilirse, çok daha büyük kısıtlamalara maruz kalır ve birim testleri burada neredeyse tamamen kaçınılmazdır. Çoğunlukla karmaşıklık maliyetleri hakkındaki düşünceleri merak ediyordum. Her halükarda silme işlemleri bir silme kaskad fkey ile kolayca çözülebilir.
Chris Travers

4

Aşağıdaki SQL Server çözümü yalnızca kısıtlamalar kullanır. Sistemimdeki birden çok yerde benzer yaklaşımlar kullanıyorum.

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

bu ilginç bir yaklaşım. Buradaki kısıtlamalar, tuple veya işlem seviyesi yerine ifade üzerinde çalışıyor gibi görünüyor, değil mi? Ayrıca, alt kümelerinizde yerleşik alt küme düzeni olduğu anlamına gelir, değil mi? Bu gerçekten büyüleyici bir yaklaşım ve kesinlikle doğrudan Pgsql'e tercüme edilmese de, hala ilham verici fikirler. Teşekkürler!
Chris Travers

@Chris: Ben Postgres ( dbo.ve kaldırdıktan sonra) sadece iyi çalışıyor düşünüyorum GO: sql-fiddle
ypercubeᵀᴹ

Tamam, yanlış anladım. Burada benzer bir çözüm kullanılabilir gibi görünüyor. Ancak, güvenli olması için önceki satırın alt toplamını aramak için ayrı bir tetikleyiciye ihtiyacınız olmaz mı? Aksi takdirde uygulamanızın akılcı veri göndermesine güveniyorsunuz, değil mi? Hala adapte edebileceğim ilginç bir model.
Chris Travers

BTW, her iki çözümü de onayladı. Diğerini daha az karmaşık göründüğü için tercih edilebilir olarak listeleyecek. Ancak bunun çok ilginç bir çözüm olduğunu düşünüyorum ve benim için çok karmaşık kısıtlamalar hakkında yeni düşünme yolları açıyor. Teşekkürler!
Chris Travers

Ayrıca, güvenli olması için önceki satırın alt toplamını aramak için herhangi bir tetikleyiciye ihtiyacınız yoktur. Bu FK_Lines_PreviousLineyabancı anahtar kısıtı tarafından halledilir .
ypercubeᵀᴹ
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.