การสร้างแบบจำลองข้อ จำกัด ในมวลรวมย่อย?


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

เห็นได้ชัดว่าข้อ จำกัด การตรวจสอบดังกล่าวจะไม่ทำงาน มันทำงานต่อแถวและอาจตรวจสอบฐานข้อมูลทั้งหมด ดังนั้นมันจะล้มเหลวและทำมันช้า

ดังนั้นคำถามของฉันคือวิธีที่ดีที่สุดในการจำลองข้อ จำกัด นี้คืออะไร ฉันได้ดูแนวคิดสองอย่างแล้ว สงสัยว่าสิ่งเหล่านี้เป็นเพียงคนเดียวหรือถ้าใครบางคนมีวิธีที่ดีกว่า (นอกเหนือจากระดับแอพหรือ proc ที่เก็บไว้)

  1. ฉันสามารถยืมหน้าจากแนวคิดการบัญชีของโลกเกี่ยวกับความแตกต่างระหว่างหนังสือต้นฉบับและหนังสือสุดท้าย (วารสารทั่วไปกับบัญชีแยกประเภททั่วไป) ในเรื่องนี้ฉันสามารถจำลองสิ่งนี้เป็นอาร์เรย์ของบรรทัดเจอร์นัลที่แนบกับรายการเจอร์นัลบังคับใช้ข้อ จำกัด ในอาร์เรย์ (ในเงื่อนไข PostgreSQL เลือกผลรวม (จำนวน) = 0 จากที่ไม่ถูกต้อง (je.line_items) ทริกเกอร์สามารถขยายและ บันทึกสิ่งเหล่านี้ลงในตารางรายการโฆษณาที่มีการบังคับใช้ข้อ จำกัด คอลัมน์แต่ละรายการได้ง่ายขึ้นและดัชนี ฯลฯ อาจมีประโยชน์มากขึ้นนี่คือทิศทางที่ฉันกำลังเอนกาย
  2. ฉันสามารถลองรหัสทริกเกอร์ข้อ จำกัด ที่จะบังคับใช้สิ่งนี้ต่อธุรกรรมโดยมีแนวคิดว่าผลรวมของชุด 0 จะเท่ากับ 0 เสมอ

ฉันชั่งน้ำหนักสิ่งเหล่านี้กับแนวทางปัจจุบันของการบังคับใช้ตรรกะในกระบวนการที่เก็บไว้ ค่าใช้จ่ายที่ซับซ้อนนั้นถูกนำมาเปรียบเทียบกับแนวคิดที่ว่าข้อพิสูจน์ทางคณิตศาสตร์ของข้อ จำกัด นั้นเหนือกว่าการทดสอบหน่วย ข้อเสียเปรียบที่สำคัญของ # 1 ข้างต้นคือประเภทที่ tuples เป็นหนึ่งในพื้นที่เหล่านั้นใน 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();

รอการตัดบัญชีดังนั้นมันจะถูกตรวจสอบในตอนท้ายของการทำธุรกรรม

การทดสอบ

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 ฉันจะไม่กังวลเกี่ยวกับการลบ การลบหากทำได้จะอยู่ภายใต้ข้อ จำกัด ที่ใหญ่กว่ามากและการทดสอบหน่วยนั้นค่อนข้างหลีกเลี่ยงไม่ได้เลย ฉันส่วนใหญ่สงสัยเกี่ยวกับความคิดเกี่ยวกับค่าใช้จ่ายที่ซับซ้อน การลบอัตราใด ๆ สามารถแก้ไขได้อย่างง่ายดายด้วยการลบแบบน้ำตกทั้งหมด
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) ;

นั่นเป็นวิธีที่น่าสนใจ ข้อ จำกัด ที่ดูเหมือนจะใช้ได้กับคำสั่งไม่ใช่ระดับ tuple หรือธุรกรรมใช่ไหม? นอกจากนี้ยังหมายความว่าชุดย่อยของคุณมีการสั่งซื้อชุดย่อยในตัวถูกต้องหรือไม่ นั่นเป็นวิธีการที่น่าสนใจจริงๆและถึงแม้ว่ามันจะไม่ได้แปลโดยตรงไปยัง Pgsql แต่ก็ยังเป็นแนวคิดที่สร้างแรงบันดาลใจ ขอบคุณ!
Chris Travers

@ Chris: ฉันคิดว่ามันทำงานได้ดีใน Postgres (หลังจากลบdbo.และGO): sql-fiddle
ypercubeᵀᴹ

ตกลงฉันเข้าใจผิด ดูเหมือนว่าจะมีวิธีการแก้ปัญหาที่คล้ายกันที่นี่ อย่างไรก็ตามคุณไม่ต้องการทริกเกอร์แยกต่างหากเพื่อค้นหาผลรวมย่อยของบรรทัดก่อนหน้าเพื่อความปลอดภัยหรือไม่ มิฉะนั้นคุณจะเชื่อใจแอปของคุณเพื่อส่งข้อมูลที่มีสติใช่ไหม? มันยังคงเป็นแบบจำลองที่น่าสนใจที่ฉันอาจจะปรับตัวได้
Chris Travers

BTW ยกระดับโซลูชั่นทั้งคู่ จะมีรายชื่ออื่น ๆ ที่เป็นที่นิยมเพราะมันดูซับซ้อนน้อยกว่า อย่างไรก็ตามฉันคิดว่านี่เป็นวิธีแก้ปัญหาที่น่าสนใจมากและเปิดวิธีการใหม่ในการคิดเกี่ยวกับข้อ จำกัด ที่ซับซ้อนมากสำหรับฉัน ขอบคุณ!
Chris Travers

และคุณไม่ต้องการทริกเกอร์ใด ๆ เพื่อค้นหาผลรวมย่อยของบรรทัดก่อนหน้าเพื่อความปลอดภัย นี่คือการดูแลโดยFK_Lines_PreviousLineข้อ จำกัด ที่สำคัญต่างประเทศ
ypercubeᵀᴹ
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.