更新する値をテーブルに保持しても大丈夫ですか?


31

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

これまでは、アカウントエンティティのコレクションを持つカードエンティティがあり、各アカウントには、すべての預金/引き出しで更新される金額があります。

現在、チーム内で議論が行われています。誰かがこれがCoddの12の規則を破り、支払いごとに値を更新するのは面倒だと言っています。

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

もしそうなら、どうすれば修正できますか?


3
このトピックに関する広範な技術的な議論はDBA.SEにここにあります:シンプルな銀行スキーマ書く
ニックChammas

1
ここでチームが引用したCoddのルールはどれですか?ルールは、リレーショナルシステムを定義しようとする彼の試み​​であり、明示的に正規化について言及していませんでした。Coddは、彼の著書「データベース管理のリレーショナルモデル」で正規化について説明しました。
イアンサミュエルマクリーン長老

回答:


30

はい、それは正規化されていませんが、パフォーマンス上の理由で正規化されていないデザインが勝つことがあります。

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

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

その場で挿入される保留中のトランザクションを保持するために別のテーブルを使用します。各期間の終わりに、未転記の取引をアカウントの最後の決算残高に追加して、新しい残高を計算するルーチンを実行します。保留中のトランザクションを転記済みとしてマークするか、日付を確認して、保留中のトランザクションを特定します。

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

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


1
簡単な2つのメモ。まず、これは上で提案した対数集計スナップショットアプローチの非常に良い説明であり、おそらく私よりも明確です。(あなたを支持しました)。第二に、ここであなたが「ポスト」という用語を「奇妙なことに」「クロージングバランスの一部」という意味で使っているのではないかと思います。財務面では、投稿は通常「現在の元帳残高に表示される」ことを意味するため、説明する価値があり、混乱を引き起こさないように思えました。
クリストラヴァーズ

ええ、おそらく私が見逃している微妙な点がたくさんあります。取引終了時に当座預金口座に取引が「転記」されているように見え、それに応じて残高が更新されていることに言及しています。しかし、私は会計士ではありません。私はそれらのいくつかで作業しています。
-db2

これは将来SOXなどの要件になる可能性もあります。どのような種類のマイクロトランザクション要件をログに記録する必要があるのか​​は正確にはわかりませんが、レポート要件が何であるかを知っている人にはお問い合わせください。
jcolebrand

「毎年」のスナップショットが上書きされることのないように、毎年の開始時にバランスなどの永続データを保持する傾向があります。リストが単に追加されるだけです(システムが各アカウントに対して十分な長さの使用を続けた場合でも)年に合計1,000件を蓄積します[ 非常に楽観的]。これはほとんど管理できません。多くの年間合計を保持することで、最近のトランザクションが合計に適切な影響を与えたことを監査コードで確認できます(個々のトランザクションは5年後にパージされる可能性がありますが、それまでに十分に検査されます)。
supercat

17

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

当座預金口座にいくらのお金があるのか​​を知るために、本当に 10年分のデータを集める必要がありますか?

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

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

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


2
私の番号は常に正しいことを確認、信頼できる制約-私は計算実行中の合計を保存することができ、私は完全に安全だ:sqlblog.com/blogs/alexander_kuznetsov/archive/2009/01/23/...
AK

1
私のソリューションにはエッジケースはありません-信頼できる制約は何も忘れさせません。実際のシステムでは、積算合計を知る必要があるNULL量の実用的な必要性は見当たりません-これらは互いに矛盾しています。実用的なニーズがある場合は、あなたのsceanrioを共有してください。
AK

1
わかりましたが、一意性に違反することなく複数のNULLを許可するdbの場合と同様に、これは機能しません。また、過去のデータを消去すると、保証が悪くなりますか?
クリストラバーズ

1
たとえば、PostgreSQLの(a、b)に一意の制約がある場合、各nullは潜在的に一意として扱われるため、(a、b)に複数の(1、null)値を持つことができます。値.....
クリス・トラヴァース

1
「PostgreSQLの(a、b)に一意の制約があり、複数(1、null)の値を持つことができます」について-PostgreSqlでは、bがnullの(a)に一意の部分インデックスを使用する必要があります。
AK 14

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

あなたのアプローチの大きな制限の1つは、特定の履歴上のアカウントの残高を計算することはまだすべてのトランザクションが日付順にシーケンシャルに入力されるという仮定をしない限り、集計が必要であることです仮定)。
クリストラバーズ14

@ChrisTraversのすべての現在の合計は、すべての過去の日付に対して常に最新です。制約はそれを保証します。そのため、過去の日付に対して集計する必要はありません。過去の行を更新したり、過去のものを挿入したりする必要がある場合は、後の行のすべての合計を更新します。これはpostgreSqlでは制約が遅延しているため、はるかに簡単だと思います。
AK

6

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

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

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

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


4

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

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

結論-必要なときに計算する必要がある値を計算する


たとえ数千のトランザクションがあったとしても?だから私は毎回それを再計算する必要がありますか?パフォーマンスが少し難しくなることはありませんか?これがなぜこのような問題なのか少し説明していただけますか?
ミシール

2
@Mithir会計のほとんどのルールに反するため、問題を追跡することが不可能になるためです。現在の合計を更新する場合、どの調整が適用されているかをどのように確認しますか?その請求書は1回または2回貸方記入されましたか?すでに支払い金額を差し引いていましたか?トランザクションを追跡する場合は答えがわかりますが、合計を追跡する場合はわかりません。
JNK

4
Coddの規則への言及は、それが通常の形式を破ることです。トランザクションをどこかで追跡し(これは私が考える必要があります)、別の積算合計があると仮定します。真実の単一バージョンが必要です。パフォーマンスの問題は、実際に存在する場合を除いて修正しないでください。
JNK

@JNKの現状-トランザクションと合計を保持しているため、必要に応じてあなたが言及したすべてを完全に追跡できます。残高合計は、アクションごとに金額を再計算することを防ぐためのものです。
ミシール

2
古いデータがたとえば5年間しか保持されない場合でも、Coddのルールに違反することはありませんか?その時点でのバランスは、単に既存のレコードの合計ではなく、パージされてから以前に存在したレコードでもありますか、それとも何か不足していますか?無限のデータ保持を想定した場合にのみCoddのルールに違反するように思えますが、これはほとんどありません。これは、以下に述べる理由で言われていますが、継続的に更新される値を保存することは問題を引き起こしていると思います。
ラヴァーズ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.