Моделювання обмежень на агрегати підмножини?


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

Очевидно, таке обмеження перевірки ніколи не спрацює. Він працює на рядок і може перевіряти весь db. Тож воно завжди буде провалюватися і буде повільно це робити.

Тож моє запитання - який найкращий спосіб моделювати це обмеження? Я в основному розглядав дві ідеї до цих пір. Цікаво, чи це єдині, чи хтось має кращий спосіб (окрім того, щоб залишити його на рівні додатків чи збережених програм).

  1. Я міг би запозичити сторінку із світового поняття бухгалтерського обліку про різницю між книгою оригіналу та книгою остаточного запису (загальний журнал проти загальної книги). У зв'язку з цим я міг би моделювати це як масив журнальних рядків, приєднаних до запису журналу, примусово застосовувати обмеження на масив (у термінах PostgreSQL виберіть суму (сума) = 0 від unnest (je.line_items). Тригер може розширитись і збережіть їх у таблиці рядків-позицій, де окремі обмеження стовпців можна легше застосувати, і де індекси тощо можуть бути кориснішими. Це напрямок, на який я схиляюся.
  2. Я міг би спробувати кодувати тригер обмеження, який би примусив це застосовувати кожну транзакцію з думкою про те, що сума серії 0 завжди буде дорівнює 0.

Я зважую їх проти поточного підходу до застосування логіки у збереженій процедурі. Вартість складності зважується на думку про те, що математичне доведення обмежень перевершує одиничні тести. Основним недоліком №1 вище є те, що типи як кортежі є однією з тих областей у PostgreSQL, де людина стикається з непослідовною поведінкою та зміною припущень регулярно, тому я навіть сподіваюся, що поведінка в цій області може змінитися з часом. Розробити майбутню безпечну версію не так просто.

Чи існують інші способи вирішення цієї проблеми, які дозволять масштабувати до мільйонів записів у кожній таблиці? Я щось пропускаю? Чи є компроміс, який я пропустив?

У відповідь на точку Крейга щодо версій, як мінімум, це доведеться працювати на PostgreSQL 9.2 і вище (можливо, 9.1 і вище, але, мабуть, ми можемо піти з прямим 9.2).

Відповіді:


13

Оскільки ми мусимо охопити кілька рядків, це не може бути реалізовано з простим 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це можливо, ви можете додати подібний тригер, зробивши перевірку балансу в цілій таблиці після того, як відбулося ВИДАЛЕННЯ. Це було б набагато дорожче, але не мало значення, як це рідко трапляється.


Отже, це голосування за пункт №2. Перевага полягає в тому, що у вас є лише одна таблиця для всіх обмежень, і це виграш складності, але, з іншого, ви встановлюєте тригери, які по суті є процедурними, і тому, якщо ми перевіряємо речі, не перевірені декларативно, то це отримує більше складний. Як би ви зважили шапку на те, щоб мати вкладене сховище з декларативними обмеженнями?
Кріс Траверс

Також оновлення неможливо, видалення може бути за певних обставин *, але майже напевно це буде дуже вузька, добре перевірена процедура. У практичних цілях видалення може ігноруватися як проблема обмеження. * Наприклад, очищення всіх даних старше 10 років, що було б можливо лише за умови використання журналу, агрегату та моделі знімків, що так чи інакше є типовим для облікових систем.
Кріс Траверс

@ChrisTravers. Я додав можливе оновлення та адресовано DELETE. Я б не знав, що є типовим чи необхідним у бухгалтерському обліку - не моя область знань. Просто намагаюся надати (досить ефективний ІМО) рішення описаної проблеми.
Ервін Брандстеттер

@Erwin Brandstetter Я б не хвилювався з приводу делетів. Видалення, якщо це застосовується, зазнало б набагато більшого набору обмежень, і одиничні тести там майже неминучі. Мене найбільше цікавили думки про складність витрат. У будь-якому випадку видалення можна вирішити дуже просто за допомогою каскаду на видаленні каскаду 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, він все ще надихає ідеї. Спасибі!
Кріс Траверс

@Chris: Я думаю, що це працює чудово в Postgres (після видалення dbo.та GO): sql-
fiddle

Гаразд, я нерозумів це. Схоже, тут можна було б використовувати подібне рішення. Однак чи не знадобиться вам окремий тригер, щоб шукати підсумковий результат попереднього рядка, щоб бути безпечним? Інакше ви довіряєте вашій програмі надсилати здорові дані, правда? Це все ще цікава модель, яку я міг би адаптувати.
Кріс Траверс

BTW, схвалив обидва рішення. Перерахувати інших як кращих, оскільки це здається менш складним. Однак я думаю, що це дуже цікаве рішення, і воно відкриває нові способи думати про дуже складні для мене обмеження. Спасибі!
Кріс Траверс

І вам не потрібен жоден тригер, щоб шукати підсумковий результат попереднього рядка, щоб бути безпечним. Про це піклується FK_Lines_PreviousLineобмеження зовнішнього ключа.
ypercubeᵀᴹ
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.