Чи правильно зберігати значення, яке оновлюється в таблиці?


31

Ми розробляємо платформу для передплачених карток, яка в основному містить дані про картки та їх баланс, платежі тощо.

До цих пір у нас була картка, яка має колекцію сукупності рахунку, і кожен рахунок має суму, яка оновлюється в кожному депозиті / знятті коштів.

Зараз у колективі ведуться дебати; хтось сказав нам, що це порушує 12 Правил Кодда і що оновлення його вартості для кожного платежу є проблемою.

Це справді проблема?

Якщо це так, як це можна виправити?


3
На цій DBA.SE проходить широке технічне обговорення на цій темі: Написання простої банківської схеми
Нік Чаммас

1
Які з правил Кодда тут наводила ваша команда? Правила були його спробою визначити реляційну систему, і не згадували про нормалізацію прямо. Кодд обговорив нормалізацію у своїй книзі Реляційна модель управління базами даних .
Ієн Самуель Маклін Старший

Відповіді:


30

Так, це ненормовані, але періодично ненормовані конструкції виграють з міркувань продуктивності.

Однак я б, мабуть, підходив до цього дещо інакше, з міркувань безпеки. (Відмова: Я зараз не працюю і не працював у фінансовому секторі. Я просто викидаю це.)

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

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

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

Тоді просто зберігайте стільки історичних даних, скільки необхідно для аудиту, обслуговування клієнтів та вимог щодо ефективності.


1
Всього дві швидкі нотатки. По-перше, це дуже хороший опис підходу, який я пропонував вище, і, можливо, ясніше, ніж я. (Визвали вас). По-друге, я підозрюю, що ви тут дещо дивним чином використовуєте термін "розміщений", щоб означати "частина залишкового балансу". У фінансовому плані розміщення, як правило, означає "показ у поточному балансі книги", і тому здавалося, що варто пояснити, що це не викликає плутанини.
Кріс Траверс

Так, є, мабуть, багато тонкощів, які мені не вистачає. Я просто маю на увазі те, як видається, що транзакції "розміщуються" на мій розрахунковий рахунок в кінці бізнесу, і залишок коштів оновлений відповідно. Але я не бухгалтер; Я просто працюю з декількома з них.
db2

Це може також бути вимогою для SOX чи щось таке в майбутньому, я не знаю , які саме вимоги мікро-транзакцій ви повинні увійти, але я б Дефо запитати кого - то , хто знає , які вимоги до звітності є для подальшого використання .
jcolebrand

Я б схильний зберігати вічні дані, наприклад, баланс на початку кожного року, щоб знімок "підсумків" ніколи не перезаписувався - до списку просто додається (навіть якщо система залишається у використанні досить довго для кожного облікового запису, щоб накопичуйте 1000 річних підсумків [ ДУЖЕ оптимістично], що навряд чи буде неможливим). Збереження багатьох підсумків щорічно дозволить аудиторським кодом підтвердити, що транзакції за останні роки мали належний вплив на загальну суму (окремі транзакції можуть бути очищені через 5 років, але до цього часу вони будуть добре перевірені].
supercat

17

З іншого боку, існує проблема, з якою ми часто стикаємося в бухгалтерському програмному забезпеченні. Перефразоване:

Чи дійсно мені потрібно зібрати дані за десять років, щоб дізнатися, скільки грошей на чековому рахунку?

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

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

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


2
Я можу зберігати обчислені підсумки, і я абсолютно безпечний - довірені обмеження забезпечують, щоб мої номери завжди були правильними: sqlblog.com/blogs/alexander_kuznetsov/archive/2009/01/23/…
AK

1
У моєму рішенні немає крайових випадків - довірене обмеження не дозволить вам нічого забути. Я не бачу жодної практичної потреби в кількості NULL в системі реального життя, яка повинна знати загальні підсумки - ці речі суперечать одна одній. Якщо ви бачите практичну потребу, поділіться своїми сценаріями.
АК

1
Гаразд, але тоді це не буде працювати, як на db, які дозволяють отримати кілька NULL, не порушуючи унікальності, правда? Також гарантія погана, якщо ви очистите попередні дані, правда?
Кріс Траверс

1
Наприклад, якщо в PostgreSQL у мене є унікальне обмеження на (a, b), я можу мати кілька (1, null) значень для (a, b), тому що кожен нуль трактується як потенційно унікальний, що, на мою думку, є семантично правильним для невідомого значення .....
Кріс Траверс

1
Щодо "У мене є унікальне обмеження на (a, b) в PostgreSQL, я можу мати кілька (1, null) значень" - у PostgreSql нам потрібно використовувати унікальний частковий індекс на (a), де b - null.
АК

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, оскільки він відклав обмеження.
АК

6

Це дуже гарне запитання.

Якщо припустити, що у вас є таблиця транзакцій, яка зберігає кожен дебет / кредит, у вашому дизайні немає нічого поганого. Насправді я працював із передплаченими системами телекомунікацій, які працювали саме таким чином.

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

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


4

Залишок - це обчислена сума, заснована на певних правилах бізнесу, тому так, ви не хочете зберігати баланс, а скоріше обчислити його з транзакцій на картці, а отже, і на рахунку.

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

Підсумок - обчислює будь-які значення, які потрібно обчислити як і коли потрібно


навіть якщо може бути 1000 тисяч угод? Тож мені потрібно буде перераховувати її кожен раз? хіба це може бути трохи важким у виконанні? Ви можете додати трохи про те, чому це така проблема?
Мітір

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

4
Посилання на правила Кодда полягає в тому, що він порушує нормальну форму. Якщо припустити, що ви відстежуєте транзакції ДЕЙШЕ (що, на мою думку, доведеться), і у вас є окрема загальна сума, що правильно, якщо вони не згодні? Вам потрібна єдина версія істини. Не виправляйте проблеми з продуктивністю до тих пір, поки вона фактично не існує.
JNK

@JNK, як зараз - ми зберігаємо транзакції та загальну суму, тому все, що ви згадали, може бути відстежено ідеально, якщо потрібно, загальний баланс - це лише для того, щоб ми не перерахували суму кожної дії.
Мітір

2
Тепер це не порушить правила Кодда, якщо старі дані можуть зберігатися лише протягом, скажімо, 5 років, правда? Баланс у цьому моменті - це не лише сума існуючих записів, а й раніше існуючі записи після очищення, чи я щось пропускаю? Мені здається, це порушить правила Кодда лише в тому випадку, якщо ми припустимо нескінченне збереження даних, що малоймовірно. Це сказано з причин, про які я розповідаю нижче, я думаю, що зберігання значення, що постійно оновлюється, викликає проблеми.
Кріс Траверс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.