web-dev-qa-db-ja.com

更新する値をテーブルに保持しても問題ありませんか?

私たちは基本的にカードとその残高、支払いなどに関するデータを保持するプリペイドカード用のプラットフォームを開発しています。

これまでは、Accountエンティティのコレクションを持つCardエンティティがあり、各AccountにはAmountがあり、これはすべての預金/引き出しで更新されます。

現在、チーム内で議論があります。 Coddの12のルール が破られ、支払いごとに値を更新するのは面倒だと誰かが言った。

これは本当に問題ですか?

修正されている場合、どうすれば修正できますか?

31
Mithir

はい、これは正規化されていませんが、パフォーマンス上の理由から、正規化されていないデザインが優先されることがあります。

しかし、安全上の理由から、私はおそらく少し異なる方法でそれにアプローチするでしょう。 (免責事項:私は現在、金融業界で働いたことはありません。これをただ捨てているだけです。)

カードに記載された残高の表を持っています。これには、アカウントごとに行が挿入され、各期間の終わり(日、週、月、または適切なもの)に転記された残高を示します。このテーブルにアカウント番号と日付でインデックスを付けます。

オンザフライで挿入される保留中のトランザクションを保持するために別のテーブルを使用します。各期間の終了時に、未転記のトランザクションをアカウントの最後の決算残高に追加するルーチンを実行して、新しい残高を計算します。保留中のトランザクションに転記済みのマークを付けるか、日付を確認して、まだ保留中のものを判別します。

このようにして、すべてのアカウント履歴を合計する必要なくオンデマンドでカード残高を計算する手段があり、残高の再計算を専用の転記ルーチンに入れることにより、この再計算のトランザクションの安全性が単一の場所(およびバランステーブルのセキュリティを制限して、転記ルーチンのみが書き込みできるようにします)。

次に、監査、カスタマーサービス、およびパフォーマンスの要件で必要とされるだけの履歴データを保持します。

30
db2

一方で、会計ソフトウェアで頻繁に遭遇する問題があります。言い換え:

私は本当に当座預金の金額を調べるために10年分のデータを集計する必要がありますか?

もちろん、答えはあなたがそうではありません。ここにはいくつかのアプローチがあります。 1つは、計算された値を格納することです。不適切な値を引き起こすソフトウェアのバグを追跡することは非常に難しいため、このアプローチはお勧めしません。したがって、このアプローチは避けます。

これを行うためのより良い方法は、私がlog-snapshot-aggregateアプローチと呼ぶものです。このアプローチでは、支払いと使用は挿入であり、これらの値を更新することはありません。定期的にデータを一定期間集計し、スナップショットが有効になった時点のデータを表す計算されたスナップショットレコードを挿入します(通常、一定期間before存在します)。

スナップショットは、挿入された支払い/使用量データに完全に依存しているわけではないため、コッドの規則に違反することはありません。作業中のスナップショットがある場合は、現在の残高をオンデマンドで計算する機能に影響を与えずに、10年前のデータを削除することを決定できます。

17
Chris Travers

パフォーマンス上の理由から、ほとんどの場合、現在の残高を保存する必要があります。それ以外の場合、その場で計算すると、最終的には非常に遅くなる可能性があります。

事前計算された累計をシステムに保存します。数値が常に正しいことを保証するために、制約を使用します。次の解決策は私のブログからコピーされました。インベントリについて説明していますが、これは基本的に同じ問題です。

カーソルを使用した場合でも、三角形の結合を使用した場合でも、積算合計の計算は非常に遅くなります。特に頻繁に選択する場合は、列に累計を保存するように非正規化するのは非常に魅力的です。ただし、通常どおりに非正規化する場合は、非正規化データの整合性を保証する必要があります。幸いにも、制約付きの積算合計の整合性を保証できます。すべての制約が信頼できる限り、積算合計はすべて正しいです。また、この方法では、現在の残高(累計)が決してマイナスにならないことを簡単に確認できます。他の方法による強制も非常に遅くなる可能性があります。次のスクリプトは、この手法を示しています。

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
7
A-K

これは非常に良い質問です。

各借方/貸方を格納するトランザクションテーブルがあると仮定すると、設計に問題はありません。実際、私はこの方法で正確に機能するプリペイド電話会社のシステムを使用してきました。

あなたがする必要がある主なことは、あなたがSELECT ... FOR UPDATE残高のINSERT借方/貸方。これにより、問題が発生した場合に正しいバランスが保証されます(トランザクション全体がロールバックされるため)。

他の人が指摘したように、特定の期間の残高のスナップショットが必要です。特定の期間のすべてのトランザクションが、期間の開始/終了残高と正しく合計されていることを確認します。これを行うには、期間終了(月/週/日)の午前0時に実行するバッチジョブを記述します。

6
Philᵀᴹ

残高は特定のビジネスルールに基づいて計算された金額であるため、はい、残高を保持するのではなく、カードの取引から、したがって口座から計算します。

監査とステートメントのレポートのためにカード上のすべてのトランザクションを追跡し、後で異なるシステムからのデータを追跡したいとします。

結論-必要に応じて、必要に応じて計算する必要がある値を計算します