Mô hình hóa các ràng buộc trên tập hợp con?


14

Tôi đang sử dụng PostgreSQL nhưng tôi cho rằng hầu hết các db hàng đầu phải có một số khả năng tương tự, và hơn nữa, các giải pháp cho chúng có thể truyền cảm hứng cho các giải pháp cho tôi, vì vậy đừng xem xét cụ thể PostgreQuery này.

Tôi biết tôi không phải là người đầu tiên cố gắng giải quyết vấn đề này vì vậy tôi cho rằng đáng để hỏi ở đây nhưng tôi đang cố gắng đánh giá chi phí của việc lập mô hình dữ liệu kế toán sao cho mọi giao dịch đều được cân bằng cơ bản. Các dữ liệu kế toán là chỉ phụ lục. Ràng buộc tổng thể (được viết bằng mã giả) ở đây có thể trông gần giống như:

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

Rõ ràng một ràng buộc kiểm tra như vậy sẽ không bao giờ làm việc. Nó hoạt động trên mỗi hàng và có thể kiểm tra toàn bộ db. Vì vậy, nó sẽ luôn luôn thất bại và làm chậm nó.

Vì vậy, câu hỏi của tôi là cách tốt nhất để mô hình ràng buộc này là gì? Tôi đã cơ bản xem xét hai ý tưởng cho đến nay. Tự hỏi nếu đây là những người duy nhất, hoặc nếu ai đó có một cách tốt hơn (ngoài việc để nó ở cấp độ ứng dụng hoặc một Proc được lưu trữ).

  1. Tôi có thể mượn một trang từ khái niệm thế giới kế toán về sự khác biệt giữa một cuốn sách gốc và một cuốn sách cuối cùng (tạp chí chung so với sổ cái chung). Về vấn đề này, tôi có thể mô hình hóa nó như một mảng các dòng nhật ký được đính kèm với mục nhật ký, thực thi các ràng buộc trên mảng (theo thuật ngữ PostgreQuery, chọn sum (số tiền) = 0 từ không nhất định (je.line_items). lưu chúng vào bảng mục hàng, trong đó các ràng buộc cột riêng lẻ có thể được thi hành dễ dàng hơn và trong đó các chỉ mục, v.v có thể hữu ích hơn. Đây là hướng tôi đang nghiêng.
  2. Tôi có thể thử mã hóa một trình kích hoạt ràng buộc sẽ thực thi điều này trên mỗi giao dịch với ý tưởng rằng tổng của một chuỗi 0 sẽ luôn là 0.

Tôi đang cân nhắc những điều này so với cách tiếp cận hiện tại là thực thi logic trong một thủ tục được lưu trữ. Chi phí phức tạp đang được cân nhắc dựa trên ý tưởng rằng bằng chứng toán học về các ràng buộc là vượt trội so với các bài kiểm tra đơn vị. Hạn chế lớn của # 1 ở trên là các loại như tuples là một trong những lĩnh vực trong PostgreQuery, nơi người ta có hành vi không nhất quán và thay đổi các giả định thường xuyên và vì vậy tôi thậm chí hy vọng rằng hành vi trong lĩnh vực này có thể thay đổi theo thời gian. Thiết kế một phiên bản an toàn trong tương lai không phải là quá dễ dàng.

Có cách nào khác để giải quyết vấn đề này sẽ mở rộng lên tới hàng triệu bản ghi trong mỗi bảng không? Tui bỏ lỡ điều gì vậy? Có một sự đánh đổi tôi đã bỏ lỡ?

Để đáp lại quan điểm của Craig bên dưới về các phiên bản, ở mức tối thiểu, điều này sẽ phải chạy trên PostgreQuery 9.2 trở lên (có thể là 9.1 trở lên, nhưng có lẽ chúng ta có thể đi thẳng với 9.2).

Câu trả lời:


12

Vì chúng ta phải kéo dài nhiều hàng, nó không thể được thực hiện với một CHECKràng buộc đơn giản .

Chúng tôi cũng có thể loại trừ các ràng buộc loại trừ . Những hàng đó sẽ trải rộng trên nhiều hàng, nhưng chỉ kiểm tra sự bất bình đẳng. Các hoạt động phức tạp như một tổng trên nhiều hàng là không thể.

Công cụ có vẻ phù hợp nhất với trường hợp của bạn là CONSTRAINT TRIGGER(Hoặc thậm chí chỉ là đơn giản TRIGGER- sự khác biệt duy nhất trong triển khai hiện tại là bạn có thể điều chỉnh thời gian của trình kích hoạt SET CONSTRAINTS.

Vì vậy, đó là lựa chọn của bạn 2 .

Một khi chúng ta có thể dựa vào các ràng buộc được thi hành mọi lúc, chúng ta không cần phải kiểm tra toàn bộ bảng nữa. Chỉ kiểm tra các hàng được chèn trong giao dịch hiện tại - vào cuối giao dịch - là đủ. Hiệu suất nên ok.

Ngoài ra, như

Các dữ liệu kế toán là chỉ phụ lục.

... chúng ta chỉ cần quan tâm đến các hàng mới được chèn . (Giả sử UPDATEhoặc DELETEkhông thể.)

Tôi sử dụng cột hệ thống xidvà so sánh nó với hàm txid_current()- trả vềxid giao dịch hiện tại. Để so sánh các loại, đúc là cần thiết ... Điều này nên an toàn hợp lý. Xem xét điều này có liên quan, trả lời sau với một phương pháp an toàn hơn:

Bản giới thiệu

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

Trì hoãn , do đó, nó chỉ được kiểm tra vào cuối giao dịch.

Xét nghiệm

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

Làm.

INSERT INTO journal_line(amount) VALUES (1);

Thất bại:

LRI: Bài dự thi không cân bằng!

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;

Làm. :)

Nếu bạn cần thực thi ràng buộc của mình trước khi kết thúc giao dịch, bạn có thể làm như vậy tại bất kỳ thời điểm nào trong giao dịch, ngay cả khi bắt đầu:

SET CONSTRAINTS insaft_check_balance IMMEDIATE;

Nhanh hơn với kích hoạt đơn giản

Nếu bạn hoạt động với nhiều hàng INSERT, sẽ hiệu quả hơn khi kích hoạt mỗi câu lệnh - điều này là không thể đối với các kích hoạt ràng buộc :

Hạn chế kích hoạt chỉ có thể được chỉ định FOR EACH ROW.

Sử dụng một kích hoạt đơn giản thay vào đó và bắn FOR EACH STATEMENTđể ...

  • mất tùy chọn của SET CONSTRAINTS.
  • đạt được hiệu suất.

XÓA có thể

Trả lời nhận xét của bạn: Nếu DELETEcó thể, bạn có thể thêm trình kích hoạt tương tự khi thực hiện kiểm tra cân bằng toàn bộ bảng sau khi XÓA xảy ra. Điều này sẽ tốn kém hơn nhiều, nhưng sẽ không quan trọng lắm vì nó hiếm khi xảy ra.


Vì vậy, đây là một cuộc bỏ phiếu cho mục số 2. Ưu điểm là bạn chỉ có một bảng duy nhất cho tất cả các ràng buộc và đó là một chiến thắng phức tạp ở đó, nhưng mặt khác, bạn đang thiết lập các kích hoạt về cơ bản là thủ tục và do đó nếu chúng tôi kiểm tra đơn vị những thứ không được chứng minh một cách khai báo, thì điều đó sẽ được nhiều hơn phức tạp. Làm thế nào bạn có thể cân mũ chống lại việc lưu trữ lồng nhau với các ràng buộc khai báo?
Chris Travers

Ngoài ra cập nhật là không thể, xóa có thể trong một số trường hợp nhất định * nhưng gần như chắc chắn sẽ là một thủ tục rất hẹp, được thử nghiệm tốt. Đối với mục đích thực tế, xóa có thể được bỏ qua như là một vấn đề ràng buộc. * Ví dụ: thanh lọc tất cả dữ liệu trên 10 năm tuổi chỉ có thể nếu sử dụng mô hình nhật ký, tổng hợp và ảnh chụp nhanh, khá điển hình trong các hệ thống kế toán.
Chris Travers

@ChrisTravers. Tôi đã thêm một bản cập nhật và địa chỉ có thể DELETE. Tôi sẽ không biết những gì điển hình hoặc bắt buộc trong kế toán - không phải lĩnh vực chuyên môn của tôi. Chỉ cần cố gắng cung cấp một giải pháp (IMO khá hiệu quả) cho vấn đề được mô tả.
Erwin Brandstetter

@Erwin Brandstetter Tôi sẽ không lo lắng về việc xóa. Việc xóa, nếu có thể, sẽ phải chịu một loạt các ràng buộc lớn hơn và các bài kiểm tra đơn vị hoàn toàn không thể tránh khỏi ở đó. Tôi chủ yếu tự hỏi về những suy nghĩ về chi phí phức tạp. Ở bất kỳ tỷ lệ xóa nào cũng có thể được giải quyết rất đơn giản với một fkey cascade xóa.
Chris Travers

4

Giải pháp SQL Server sau chỉ sử dụng các ràng buộc. Tôi đang sử dụng các phương pháp tương tự ở nhiều nơi trong hệ thống của mình.

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

đó là một cách tiếp cận thú vị Các ràng buộc ở đó dường như hoạt động trên bảng sao kê thay vì tuple hoặc mức giao dịch, phải không? Ngoài ra, điều đó có nghĩa là tập hợp con của bạn có tập hợp con được đặt sẵn, đúng không? Đó là một cách tiếp cận thực sự hấp dẫn và mặc dù nó chắc chắn không dịch trực tiếp sang Pssql, nhưng nó vẫn là những ý tưởng đầy cảm hứng. Cảm ơn!
Chris Travers

@Chris: Tôi nghĩ rằng nó chỉ hoạt động tốt trong Postgres (sau khi xóa dbo.GO): sql-fiddle
ypercubeᵀᴹ

Ok, tôi đã hiểu lầm nó. Có vẻ như người ta có thể sử dụng một giải pháp tương tự ở đây. Tuy nhiên, bạn sẽ không cần một trình kích hoạt riêng để tra cứu tổng phụ của dòng trước để được an toàn? Nếu không, bạn đang tin tưởng ứng dụng của bạn để gửi dữ liệu lành mạnh, phải không? Nó vẫn là một mô hình thú vị mà tôi có thể thích nghi.
Chris Travers

BTW, nâng cao cả hai giải pháp. Sẽ được liệt kê khác là thích hợp hơn vì nó có vẻ ít phức tạp hơn. Tuy nhiên tôi nghĩ rằng đây là một giải pháp rất thú vị và nó mở ra những cách nghĩ mới về những ràng buộc rất phức tạp đối với tôi. Cảm ơn!
Chris Travers

Và bạn không cần bất kỳ trình kích hoạt nào để tra cứu tổng phụ của dòng trước để được an toàn. Điều này được chăm sóc bởi các FK_Lines_PreviousLineràng buộc khóa nước ngoài.
ypercubeᵀᴹ
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.