Ограничения моделирования на подмножестве агрегатов?

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

Очевидно, что такое ограничение проверки никогда не сработает. Он работает для каждой строки и может проверять всю базу данных. Так что он всегда будет терпеть неудачу и будет делать это медленно.

Итак, мой вопрос, каков наилучший способ смоделировать это ограничение? Я в основном посмотрел на две идеи до сих пор. Хотите знать, являются ли они единственными, или если у кого-то есть лучший способ (кроме как оставить его на уровне приложения или хранимого процесса).

  1. Я мог бы позаимствовать страницу из концепции бухгалтерского мира о разнице между книгой первоначальной записи и книгой окончательной записи (общий журнал и общая бухгалтерская книга). В связи с этим я мог бы смоделировать это как массив строк журнала, прикрепленных к записи журнала, применить ограничение к массиву (в терминах PostgreSQL выберите sum (amount) = 0 из unnest (je.line_items). Триггер может расширяться и сохраните их в таблице отдельных позиций, где проще было бы применить ограничения на отдельные столбцы и где индексы и т. д. могли бы быть более полезными.
  2. Я мог бы попытаться закодировать триггер ограничения, который бы применял это для каждой транзакции с идеей, что сумма серии 0 всегда будет равна 0.

Я сравниваю их с текущим подходом применения логики в хранимой процедуре. Стоимость сложности сопоставляется с идеей, что математическое доказательство ограничений превосходит модульные тесты. Основным недостатком # 1 выше является то, что типы как кортежи являются одной из тех областей в PostgreSQL, где каждый сталкивается с противоречивым поведением и регулярно изменяет предположения, и поэтому я даже надеюсь, что поведение в этой области со временем может измениться. Создание будущей безопасной версии не так просто.

Существуют ли другие способы решения этой проблемы, которые увеличат до миллионов записей в каждой таблице? Я что-то пропустил? Есть ли компромисс, который я пропустил?

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

Эрвин Брандштеттер
источник
Так что это голосование за пункт № 2. Преимущество состоит в том, что у вас есть только одна таблица для всех ограничений, и это является выигрышной сложностью, но с другой стороны, вы устанавливаете триггеры, которые по сути являются процедурными, и, следовательно, если мы проводим модульное тестирование, которое не доказано декларативно, то это дает больше сложный. Как бы вы оценили наличие вложенного хранилища с декларативными ограничениями?
Крис Треверс
Также обновление невозможно, удаление может быть при определенных обстоятельствах *, но почти наверняка это будет очень узкая, хорошо протестированная процедура. В практических целях удаление может быть проигнорировано как проблема ограничения. * Например, очистка всех данных старше 10 лет, что было бы возможно только при использовании модели журналов, агрегатов и моментальных снимков, что в любом случае довольно типично для учетных систем.
Крис Треверс
@ChrisTravers. Я добавил обновление и обратился по возможности DELETE. Я бы не знал, что типично или требуется в бухгалтерском учете - не моя область знаний. Просто пытаюсь обеспечить (довольно эффективное IMO) решение описанной проблемы.
Эрвин Брандштеттер
@ 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
ypercubeᵀᴹ
Хорошо, я неправильно понял это. Похоже, можно использовать подобное решение здесь. Однако вам не нужен отдельный триггер для поиска промежуточной суммы предыдущей строки, чтобы быть в безопасности? В противном случае вы доверяете своему приложению отправлять вменяемые данные, верно? Это все еще интересная модель, которую я мог бы адаптировать.
Крис Треверс
Кстати, проголосовали оба решения. Перечислим другие как предпочтительные, потому что они кажутся менее сложными. Однако я думаю, что это очень интересное решение, и оно открывает для меня новые способы мышления об очень сложных ограничениях. Благодарность!
Крис Треверс
И вам не нужен какой-либо триггер для поиска промежуточной суммы предыдущей строки, чтобы быть в безопасности. Об этом заботится ограничение FK_Lines_PreviousLineвнешнего ключа.
ypercubeᵀᴹ