私たちは、基本的にカードとその残高、支払いなどに関するデータを保持するプリペイドカードのプラットフォームを開発しています。
これまでは、アカウントエンティティのコレクションを持つカードエンティティがあり、各アカウントには、すべての預金/引き出しで更新される金額があります。
現在、チーム内で議論が行われています。誰かがこれがCoddの12の規則を破り、支払いごとに値を更新するのは面倒だと言っています。
これは本当に問題ですか?
もしそうなら、どうすれば修正できますか?
私たちは、基本的にカードとその残高、支払いなどに関するデータを保持するプリペイドカードのプラットフォームを開発しています。
これまでは、アカウントエンティティのコレクションを持つカードエンティティがあり、各アカウントには、すべての預金/引き出しで更新される金額があります。
現在、チーム内で議論が行われています。誰かがこれがCoddの12の規則を破り、支払いごとに値を更新するのは面倒だと言っています。
これは本当に問題ですか?
もしそうなら、どうすれば修正できますか?
回答:
はい、それは正規化されていませんが、パフォーマンス上の理由で正規化されていないデザインが勝つことがあります。
ただし、安全上の理由から、おそらく少し異なる方法でアプローチするでしょう。(免責事項:私は現在、金融業界で働いたことはありません。これをただ捨てています。)
カードに転記された残高の表を用意します。これには、各アカウントの行が挿入され、各期間(日、週、月、または適切なもの)の終わりに転記された残高を示します。アカウント番号と日付でこのテーブルにインデックスを付けます。
その場で挿入される保留中のトランザクションを保持するために別のテーブルを使用します。各期間の終わりに、未転記の取引をアカウントの最後の決算残高に追加して、新しい残高を計算するルーチンを実行します。保留中のトランザクションを転記済みとしてマークするか、日付を確認して、保留中のトランザクションを特定します。
このように、すべてのアカウント履歴を合計することなく、オンデマンドでカード残高を計算する手段があり、専用の投稿ルーチンに残高再計算を配置することにより、この再計算のトランザクションの安全性を制限できます。単一の場所(およびバランステーブルのセキュリティを制限して、投稿ルーチンのみが書き込みできるようにします)。
その後、監査、顧客サービス、およびパフォーマンスの要件によって必要とされるだけの履歴データを保持します。
一方、会計ソフトウェアで頻繁に遭遇する問題があります。言い換え:
当座預金口座にいくらのお金があるのかを知るために、本当に 10年分のデータを集める必要がありますか?
もちろん、答えはノーです。ここにはいくつかのアプローチがあります。1つは、計算された値を保存することです。不正な値を引き起こすソフトウェアバグを追跡するのは非常に難しいため、このアプローチはお勧めしません。したがって、このアプローチは避けます。
より良い方法は、私がlog-snapshot-aggregateアプローチと呼ぶものです。このアプローチでは、支払いと使用は挿入であり、これらの値を更新することはありません。定期的にデータを集計し、スナップショットが有効になった時点のデータ(通常は存在する前の期間)を表す計算されたスナップショットレコードを挿入します。
現在、スナップショットは挿入された支払い/使用データに完全に依存しなくなる可能性があるため、これはCoddのルールに違反しません。作業用スナップショットがある場合、オンデマンドで現在の残高を計算する能力に影響を与えることなく、10年前のデータを削除することを決定できます。
パフォーマンス上の理由から、ほとんどの場合、現在のバランスを保存する必要があります。そうしないと、オンザフライでの計算が最終的に非常に遅くなる可能性があります。
事前に計算された積算合計をシステムに保存します。数値が常に正しいことを保証するために、制約を使用します。次のソリューションが私のブログからコピーされました。インベントリについて説明しますが、これは本質的に同じ問題です。
カーソルを使用して実行する場合でも、三角結合を使用して実行する場合でも、積算合計の計算が遅いことで有名です。特に頻繁に選択する場合は、非正規化して現在の合計を列に格納するのは非常に魅力的です。ただし、通常どおり非正規化する場合は、非正規化データの整合性を保証する必要があります。幸いなことに、制約付きの積算合計の整合性を保証できます。すべての制約が信頼されている限り、すべての積算合計は正しいです。また、この方法により、現在の残高(実行中の合計)が決して負にならないことを簡単に確認できます。他の方法による強制も非常に遅くなる可能性があります。次のスクリプトは、この手法を示しています。
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
これは非常に良い質問です。
各借方/貸方を格納するトランザクションテーブルがあると仮定すると、設計に問題はありません。実際、私はまさにこのように機能するプリペイド電話会社のシステムで働いてきました。
あなたがする必要がある主なことSELECT ... FOR UPDATE
は、あなたINSERT
が借方/貸方の間にあなたがバランスをしていることを確実にすることです。これにより、問題が発生した場合に正しいバランスが保証されます(トランザクション全体がロールバックされるため)。
他の人が指摘したように、特定の期間の残高のスナップショットが必要であり、特定の期間のすべてのトランザクションが期間の開始/終了残高と正しく合計することを確認します。これを行うには、期間の終わり(月/週/日)の午前0時に実行されるバッチジョブを記述します。
残高は特定のビジネスルールに基づいて計算された金額です。そのため、残高を保持するのではなく、カード上の取引、したがって口座から計算する必要があります。
監査とステートメントのレポートのためにカード上のすべてのトランザクションを追跡し、さらに後で別のシステムからのデータを追跡する必要があります。
結論-必要なときに計算する必要がある値を計算する