Можно ли сохранить значение, которое обновляется в таблице?

31

Мы разрабатываем платформу для карт предоплаты, которая в основном содержит данные о картах и ​​их балансе, платежах и т. Д.

До сих пор у нас была сущность Карточка, которая имеет набор сущностей Счета, и у каждой Счета есть Сумма, которая обновляется при каждом пополнении / снятии.

Сейчас в команде идут дебаты; кто-то сказал нам, что это нарушает 12 правил Кодда и что обновление его стоимости при каждом платеже является проблемой.

Это действительно проблема?

Если это так, как мы можем это исправить?

Mithir
источник
3
На DBA.SE проводится обширное техническое обсуждение этой темы: написание простой банковской схемы
Ник Чаммас,
1
На какие из правил Кодда ссылалась ваша команда? Правила были его попыткой определить реляционную систему и не упоминали нормализацию в явном виде. Кодд обсуждал нормализацию в своей книге «Реляционная модель управления базами данных» .
Иэн Сэмюэл Маклин Старейшина

Ответы:

30

Да, это ненормализовано, но иногда ненормализованные проекты выигрывают по причинам производительности.

Тем не менее, я бы, вероятно, подошел к этому немного по-другому из соображений безопасности. (Отказ от ответственности: в настоящее время я не работаю в финансовом секторе и никогда не работал).

Есть таблица для размещенных остатков на карточках. Для каждой учетной записи будет вставлена ​​строка с указанием проведенного баланса на конец каждого периода (день, неделя, месяц или что-либо более подходящее). Индексируйте эту таблицу по номеру счета и дате.

Используйте другую таблицу для хранения ожидающих транзакций, которые вставляются на лету. В конце каждого периода запустите процедуру, которая добавляет неопубликованные транзакции к последнему сальдо закрытия счета для расчета нового сальдо. Либо пометьте ожидающие транзакции как опубликованные, либо посмотрите на даты, чтобы определить, что еще ожидает рассмотрения.

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

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

db2
источник
1
Всего две быстрые заметки. Во-первых, это очень хорошее описание подхода log-aggregate-snapshot, которое я предлагал выше, и, возможно, более ясного, чем я. (Проголосовал за тебя). Во-вторых, я подозреваю, что вы используете термин «размещенный» здесь несколько странно, что означает «часть итогового баланса». С финансовой точки зрения опубликованный обычно означает «показ в текущем балансе бухгалтерской книги», и поэтому казалось, что это стоило объяснить, так что это не вызывает путаницы.
Крис Траверс
Да, возможно, мне не хватает многих тонкостей. Я просто имею в виду, как транзакции, по-видимому, «публикуются» на моем текущем счете в конце рабочего дня, и баланс обновляется соответствующим образом. Но я не бухгалтер; Я просто работаю с несколькими из них.
db2
Это также может быть требованием для SOX или подобного в будущем, я не знаю точно, какие требования к микротранзакциям вы должны регистрировать, но я бы не стал спрашивать кого-то, кто знает, каковы требования к отчетности на будущее.
Jcolebrand
Я был бы склонен хранить бессрочные данные, например, баланс в начале каждого года, чтобы моментальный снимок «итогов» никогда не перезаписывался - к списку просто добавляется (даже если система использовалась достаточно долго для каждой учетной записи, чтобы накапливать 1000 годовых итогов ( ОЧЕНЬ оптимистично), что вряд ли будет неуправляемым). Сохранение большого количества годовых итогов позволило бы коду аудита подтвердить, что транзакции между последними годами оказали надлежащее влияние на итоги [отдельные транзакции могут быть удалены через 5 лет, но к тому времени они будут тщательно проверены].
суперкат
17

С другой стороны, есть проблема, с которой мы часто сталкиваемся в бухгалтерском программном обеспечении. Перефразировано:

Должен ли я на самом деле нужно агрегатных десять лет данных , чтобы узнать, сколько денег на расчетный счет?

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

Лучший способ сделать это - то, что я называю подходом log-snapshot-aggregate. При таком подходе наши платежи и использование являются вставками, и мы никогда не обновляем эти значения. Периодически мы агрегируем данные за определенный период времени и вставляем вычисленную запись моментального снимка, которая представляет данные в момент, когда моментальный снимок стал действительным (обычно за период до его появления ).

Теперь это не нарушает правила Кодда, потому что со временем моментальные снимки могут не полностью зависеть от введенных данных об оплате / использовании. Если у нас есть рабочие снимки, мы можем решить очистить данные за 10 лет, не влияя на нашу способность вычислять текущие остатки по требованию.

Крис Траверс
источник
2
Я могу хранить рассчитанные промежуточные итоги, и я в полной
AK
1
В моем решении нет крайних случаев - доверенное ограничение не позволит вам ничего забыть. Я не вижу практической необходимости в NULL количествах в реальной системе, которая должна знать промежуточные суммы - эти вещи противоречат друг другу. Если вы видите практическую необходимость, пожалуйста, поделитесь своим sceanrio.
АК
1
Хорошо, но тогда это не сработает, как на БД, которые допускают множественные значения NULL без нарушения уникальности, верно? Кроме того, ваша гарантия испортится, если вы удалите прошлые данные, верно?
Крис Траверс
1
Например, если у меня есть уникальное ограничение на (a, b) в PostgreSQL, у меня может быть несколько (1, нулевых) значений для (a, b), потому что каждый ноль рассматривается как потенциально уникальный, который я считаю семантически правильным для неизвестного значения .....
Крис Трэверс
1
Что касается «У меня есть уникальное ограничение на (a, b) в PostgreSQL, я могу иметь несколько (1, нулевых) значений» - в PostgreSql нам нужно использовать уникальный частичный индекс для (a), где b равно нулю.
AK
7

Из соображений производительности в большинстве случаев мы должны хранить текущий баланс - в противном случае его вычисление на лету может в конечном итоге стать чрезмерно медленным.

Мы храним предварительно рассчитанные промежуточные итоги в нашей системе. Чтобы гарантировать, что числа всегда правильные, мы используем ограничения. Следующее решение было скопировано из моего блога. Он описывает инвентарь, который по сути та же проблема:

Вычисление промежуточных итогов печально известно медленно, независимо от того, делаете ли вы это с помощью курсора или треугольного соединения. Очень заманчиво денормализовать, хранить промежуточные итоги в столбце, особенно если вы часто его выбираете. Однако, как обычно, когда вы денормализуете, вы должны гарантировать целостность ваших денормализованных данных. К счастью, вы можете гарантировать целостность промежуточных итогов с ограничениями - если все ваши ограничения являются доверенными, все промежуточные итоги верны. Таким образом, вы также можете легко гарантировать, что текущий баланс (промежуточные итоги) никогда не будет отрицательным - применение других методов также может быть очень медленным. Следующий скрипт демонстрирует технику.

CREATE TABLE Data.Inventory(InventoryID INT NOT NULL IDENTITY,
  ItemID INT NOT NULL,
  ChangeDate DATETIME NOT NULL,
  ChangeQty INT NOT NULL,
  TotalQty INT NOT NULL,
  PreviousChangeDate DATETIME NULL,
  PreviousTotalQty INT NULL,
  CONSTRAINT PK_Inventory PRIMARY KEY(ItemID, ChangeDate),
  CONSTRAINT UNQ_Inventory UNIQUE(ItemID, ChangeDate, TotalQty),
  CONSTRAINT UNQ_Inventory_Previous_Columns UNIQUE(ItemID, PreviousChangeDate, PreviousTotalQty),
  CONSTRAINT FK_Inventory_Self FOREIGN KEY(ItemID, PreviousChangeDate, PreviousTotalQty)
    REFERENCES Data.Inventory(ItemID, ChangeDate, TotalQty),
  CONSTRAINT CHK_Inventory_Valid_TotalQty CHECK(TotalQty >= 0 AND (TotalQty = COALESCE(PreviousTotalQty, 0) + ChangeQty)),
  CONSTRAINT CHK_Inventory_Valid_Dates_Sequence CHECK(PreviousChangeDate < ChangeDate),
  CONSTRAINT CHK_Inventory_Valid_Previous_Columns CHECK((PreviousChangeDate IS NULL AND PreviousTotalQty IS NULL)
            OR (PreviousChangeDate IS NOT NULL AND PreviousTotalQty IS NOT NULL))
);
GO
-- beginning of inventory for item 1
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
VALUES(1, '20090101', 10, 10, NULL, NULL);
-- cannot begin the inventory for the second time for the same item 1
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
VALUES(1, '20090102', 10, 10, NULL, NULL);

Msg 2627, Level 14, State 1, Line 10
Violation of UNIQUE KEY constraint 'UNQ_Inventory_Previous_Columns'. Cannot insert duplicate key in object 'Data.Inventory'.
The statement has been terminated.

-- add more
DECLARE @ChangeQty INT;
SET @ChangeQty = 5;
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
SELECT TOP 1 ItemID, '20090103', @ChangeQty, TotalQty + @ChangeQty, ChangeDate, TotalQty
  FROM Data.Inventory
  WHERE ItemID = 1
  ORDER BY ChangeDate DESC;

SET @ChangeQty = 3;
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
SELECT TOP 1 ItemID, '20090104', @ChangeQty, TotalQty + @ChangeQty, ChangeDate, TotalQty
  FROM Data.Inventory
  WHERE ItemID = 1
  ORDER BY ChangeDate DESC;

SET @ChangeQty = -4;
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
SELECT TOP 1 ItemID, '20090105', @ChangeQty, TotalQty + @ChangeQty, ChangeDate, TotalQty
  FROM Data.Inventory
  WHERE ItemID = 1
  ORDER BY ChangeDate DESC;

-- try to violate chronological order

SET @ChangeQty = 5;
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
SELECT TOP 1 ItemID, '20081231', @ChangeQty, TotalQty + @ChangeQty, ChangeDate, TotalQty
  FROM Data.Inventory
  WHERE ItemID = 1
  ORDER BY ChangeDate DESC;

Msg 547, Level 16, State 0, Line 4
The INSERT statement conflicted with the CHECK constraint "CHK_Inventory_Valid_Dates_Sequence". The conflict occurred in database "Test", table "Data.Inventory".
The statement has been terminated.


SELECT ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty
FROM Data.Inventory ORDER BY ChangeDate;

ChangeDate              ChangeQty   TotalQty    PreviousChangeDate      PreviousTotalQty
----------------------- ----------- ----------- ----------------------- -----
2009-01-01 00:00:00.000 10          10          NULL                    NULL
2009-01-03 00:00:00.000 5           15          2009-01-01 00:00:00.000 10
2009-01-04 00:00:00.000 3           18          2009-01-03 00:00:00.000 15
2009-01-05 00:00:00.000 -4          14          2009-01-04 00:00:00.000 18


-- try to change a single row, all updates must fail
UPDATE Data.Inventory SET ChangeQty = ChangeQty + 2 WHERE InventoryID = 3;
UPDATE Data.Inventory SET TotalQty = TotalQty + 2 WHERE InventoryID = 3;
-- try to delete not the last row, all deletes must fail
DELETE FROM Data.Inventory WHERE InventoryID = 1;
DELETE FROM Data.Inventory WHERE InventoryID = 3;

-- the right way to update

DECLARE @IncreaseQty INT;
SET @IncreaseQty = 2;
UPDATE Data.Inventory SET ChangeQty = ChangeQty + CASE WHEN ItemID = 1 AND ChangeDate = '20090103' THEN @IncreaseQty ELSE 0 END,
  TotalQty = TotalQty + @IncreaseQty,
  PreviousTotalQty = PreviousTotalQty + CASE WHEN ItemID = 1 AND ChangeDate = '20090103' THEN 0 ELSE @IncreaseQty END
WHERE ItemID = 1 AND ChangeDate >= '20090103';

SELECT ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty
FROM Data.Inventory ORDER BY ChangeDate;

ChangeDate              ChangeQty   TotalQty    PreviousChangeDate      PreviousTotalQty
----------------------- ----------- ----------- ----------------------- ----------------
2009-01-01 00:00:00.000 10          10          NULL                    NULL
2009-01-03 00:00:00.000 7           17          2009-01-01 00:00:00.000 10
2009-01-04 00:00:00.000 3           20          2009-01-03 00:00:00.000 17
2009-01-05 00:00:00.000 -4          16          2009-01-04 00:00:00.000 20
Аляска
источник
Мне приходит в голову, что одним из огромных ограничений вашего подхода является то, что для расчета баланса счета на определенную историческую дату все еще требуется агрегирование, если только вы не предполагаете, что все транзакции вводятся последовательно по дате (что, как правило, плохо предположение).
Крис Трэверс
@ChrisTravers все текущие итоги всегда обновляются, для всех исторических дат. Ограничения гарантируют это. Таким образом, нет необходимости в агрегировании для каких-либо исторических дат. Если нам нужно обновить какую-либо историческую строку или вставить что-то задним числом, мы обновим промежуточные итоги всех последующих строк. Я думаю, что это намного проще в postgreSql, потому что он имеет отложенные ограничения.
AK
6

Это очень хороший вопрос.

Предполагая, что у вас есть таблица транзакций, в которой хранятся все дебеты / кредиты, в вашем дизайне нет ничего плохого. Фактически, я работал с предоплаченными телекоммуникационными системами, которые работали именно таким образом.

Главное, что вам нужно сделать, это убедиться, что вы делаете SELECT ... FOR UPDATEбаланс, пока вы INSERTдебет / кредит. Это будет гарантировать правильный баланс, если что-то пойдет не так (потому что вся транзакция будет откатана).

Как уже отмечали другие, вам потребуется снимок сальдо за определенные периоды времени, чтобы убедиться, что все транзакции в данном периоде суммируются с правильными значениями начального / конечного сальдо периода. Для этого напишите пакетное задание, которое выполняется в полночь в конце периода (месяц / неделя / день).

Philᵀᴹ
источник
4

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

Вы хотите отслеживать все транзакции на карте для аудита и отчетности, а также даже данные из других систем в дальнейшем.

Итог - вычисление любых значений, которые должны быть вычислены, как и когда вам нужно

Стивен Сенкомаго Мусоке
источник
даже если может быть тысячи транзакций? Так что мне нужно будет каждый раз пересчитывать? не может ли это быть немного трудно на производительность? Вы можете добавить немного о том, почему это такая проблема?
Mithir
2
@Mithir Потому что это противоречит большинству правил бухгалтерского учета и делает невозможным отслеживание проблем. Если вы просто обновите промежуточный итог, как узнать, какие корректировки были применены? Этот счет был зачислен один или два раза? Мы уже вычли сумму платежа? Если вы отслеживаете транзакции, вы знаете ответы, если вы отслеживаете итоги, вы не знаете.
JNK
4
Ссылка на правила Кодда заключается в том, что он нарушает нормальную форму. Предполагая, что вы отслеживаете транзакции ГДЕ-ТО (что, я думаю, вам придется), и у вас есть отдельная промежуточная сумма, которая верна, если они не согласны? Вам нужна единственная версия правды. Не устраняйте проблему с производительностью до тех пор, пока она не появится.
JNK
@JNK так, как есть сейчас - мы храним транзакции и итоги, поэтому все, что вы упомянули, можно идеально отследить, если необходимо, итоговое сальдо просто мешает нам пересчитать сумму каждого действия.
Mithir
2
Теперь, это не нарушит правила Кодда, если старые данные будут храниться, скажем, 5 лет, верно? Баланс в этой точке - это не просто сумма существующих записей, но также и ранее существующих записей после очистки, или я что-то упустил? Мне кажется, что это нарушит правила Кодда только в том случае, если мы допустим бесконечное хранение данных, что маловероятно. Это объясняется причинами, о которых я говорю ниже, и я думаю, что хранение постоянно обновляемого значения вызывает проблемы.
Крис Трэверс