単純な銀行データベースのスキーマを書いています。基本的な仕様は次のとおりです。
銀行アプリケーションは、ストアード・プロシージャーのみを介してデータベースと通信します。
このデータベースは、1日あたり数十万の新しいトランザクションを受け入れるだけでなく、クエリのバランスをより高い桁で期待しています。 残高をすばやく提供するには、事前に集計する必要があります。同時に、残高が取引履歴と矛盾しないことを保証する必要があります。
私のオプションは:
別のbalances
テーブルを用意し、次のいずれかを実行します。
transactions
テーブルとbalances
テーブルの両方にトランザクションを適用します。ストアドプロシージャレイヤーでTRANSACTION
ロジックを使用して、残高とトランザクションが常に同期していることを確認します。 ( Jack でサポートされています。)
トランザクションをtransactions
テーブルに適用し、balances
テーブルをトランザクション量で更新するトリガーを設定します。
トランザクションをbalances
テーブルに適用し、transactions
テーブルにトランザクション量を含む新しいエントリを追加するトリガーを設定します。
ストアドプロシージャの外部で変更が行われないようにするために、セキュリティベースのアプローチに依存する必要があります。それ以外の場合、たとえば、一部のプロセスがトランザクションをtransactions
テーブルに直接挿入し、スキーム1.3
の下で、関連する残高が同期しなくなります。
トランザクションを適切に集計するbalances
インデックス付きビューを用意します。残高はストレージエンジンによってトランザクションとの同期が保たれることが保証されているため、これを保証するためにセキュリティベースのアプローチに依存する必要はありません。一方、ビュー(インデックス付きビューでさえ)にCHECK
制約を設定することはできないため、バランスを負でない値にすることはできません。 ( Denny でサポートされています。)
transactions
テーブルのみが含まれていますが、トランザクションの実行直後に有効な残高を格納するための追加の列があります。したがって、ユーザーと通貨の最新のトランザクションレコードには、現在の残高も含まれます。 (以下で推奨 Andrew ; garik によって提案されたバリアント。)
この問題に最初に取り組んだとき、私は thesetwo の議論を読み、オプション2
を決定しました。参考までに、必要最小限の実装 here を参照してください。
高負荷プロファイルでこのようなデータベースを設計または管理しましたか?この問題の解決策は何でしたか?
私が正しいデザインを選択したと思いますか?覚えておくべきことはありますか?
たとえば、transactions
テーブルのスキーマを変更すると、balances
ビューを再構築する必要があることがわかります。データベースを小さく保つためにトランザクションをアーカイブしている場合でも(たとえば、トランザクションを別の場所に移動し、それらをサマリートランザクションに置き換えることにより)、スキーマを更新するたびに数千万のトランザクションからビューを再構築する必要があると、デプロイメントごとのダウンタイムが大幅に増えることになります。
インデックス付きビューが適している場合、マイナスのバランスがないことをどのように保証できますか?
トランザクションのアーカイブ:
トランザクションのアーカイブと、前述の「サマリートランザクション」について少し詳しく説明します。まず、このような高負荷のシステムでは、定期的なアーカイブが必要になります。古い取引を別の場所に移動できるようにしながら、残高と取引履歴の整合性を維持したいと考えています。これを行うには、アーカイブされたトランザクションのすべてのバッチを、ユーザーおよび通貨ごとの金額の要約に置き換えます。
したがって、たとえば、次のトランザクションのリスト:
user_id currency_id amount is_summary
------------------------------------------------
3 1 10.60 0
3 1 -55.00 0
3 1 -12.12 0
アーカイブされ、これに置き換えられます:
user_id currency_id amount is_summary
------------------------------------------------
3 1 -56.52 1
このように、アーカイブされたトランザクションとのバランスにより、完全で一貫したトランザクション履歴が維持されます。
私は会計に精通していませんが、在庫タイプの環境でいくつかの同様の問題を解決しました。トランザクションと同じ行に累計を保存します。私は制約を使用しているので、同時実行性が高くてもデータが間違っていることはありません。 2009年に次のソリューションを作成しました: :
カーソルを使用した場合でも、三角形の結合を使用した場合でも、積算合計の計算は非常に遅くなります。特に頻繁に選択する場合は、列に累計を保存するように非正規化するのは非常に魅力的です。ただし、通常どおりに非正規化する場合は、非正規化データの整合性を保証する必要があります。幸いなことに、制約付きの積算合計の整合性を保証できます。すべての制約が信頼されている限り、積算合計はすべて正しいです。また、この方法では、現在の残高(累計)が決してマイナスにならないことを簡単に確認できます。他の方法による強制も非常に遅くなる可能性があります。次のスクリプトは、この手法を示しています。
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)
)
);
-- 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
考慮すべき少し異なるアプローチ(2ndオプションと同様)は、以下の定義を含むトランザクションテーブルのみを持つことです。
CREATE TABLE Transaction (
UserID INT
, CurrencyID INT
, TransactionDate DATETIME
, OpeningBalance MONEY
, TransactionAmount MONEY
);
同じ日付の2つのトランザクションを処理して検索クエリを改善できるように、トランザクションID /注文が必要な場合もあります。
現在の残高を取得するには、取得する必要があるのは最後のレコードだけです。
/* For a single User/Currency */
Select TOP 1 *
FROM dbo.Transaction
WHERE UserID = 3 and CurrencyID = 1
ORDER By TransactionDate desc
/* For multiple records ie: to put into a view (which you might want to index) */
SELECT
C.*
FROM
(SELECT
*,
ROW_NUMBER() OVER (
PARTITION BY UserID, CurrencyID
ORDER BY TransactionDate DESC
) AS rnBalance
FROM Transaction) C
WHERE
C.rnBalance = 1
ORDER BY
C.UserID, C.CurrencyID
短所:
ユーザー/通貨のトランザクションは、正確なバランスを維持するためにシリアル化する必要があります。
-- Example of getting the current balance and locking the
-- last record for that User/Currency.
-- This lock will be freed after the Stored Procedure completes.
SELECT TOP 1 @OldBalance = OpeningBalance + TransactionAmount
FROM dbo.Transaction with (rowlock, xlock)
WHERE UserID = 3 and CurrencyID = 1
ORDER By TransactionDate DESC;
長所:
編集:現在の残高の取得とconを強調表示するためのいくつかのサンプルクエリ(ありがとう@Jack Douglas)
顧客が0未満の残高を持つことを許可しないことはビジネスルールです(ドラフトのようなものに対する手数料が銀行がほとんどのお金を稼ぐ方法であるので、それはすぐに変わります)。行がトランザクション履歴に挿入されたときのアプリケーション処理でこれを処理する必要があります。特に、当座貸越保護を利用している顧客や、手数料が請求されている顧客、負の金額の入力を許可していない顧客がいる場合があります。
これまでのところ、これを使用するところが好きですが、これが実際のプロジェクト(学校ではない)の場合は、ビジネスルールなどに多くの考えを盛り込む必要があります。銀行システムが完成したら、そして、人々がお金にアクセスできるという非常に具体的な法律があるので、そこを実行することは再設計の余地があまりありません。
これらの2つの議論を読んだ後、私はオプション2を決定しました
それらの議論も読んだので、なぜあなたが概説する他のオプションの最も賢明なものよりも [〜#〜] dri [〜#〜] ソリューションを決定したのかはわかりません:
トランザクションテーブルとバランステーブルの両方にトランザクションを適用します。ストアドプロシージャレイヤーでTRANSACTIONロジックを使用して、残高とトランザクションが常に同期していることを確認します。
トランザクションAPIを介したデータへのallへのアクセスを制限する贅沢がある場合、この種類のソリューションには非常に実用的なメリットがあります。 DRIの非常に重要な利点は失われます。つまり、データベースによって整合性が保証されますが、十分に複雑なモデルでは、DRIによって適用できないビジネスルールがいくつかありますです。
可能な場合はモデルを過度に曲げることなくビジネスルールを適用できるように、可能な場合はDRIを使用することをお勧めします。
トランザクションをアーカイブしている場合でも(たとえば、トランザクションを別の場所に移動して、それらを要約トランザクションに置き換えることにより)
このようにモデルを汚染することを検討し始めるとすぐに、DRIの利点が導入する難しさを上回っている領域に移動していると思います。たとえば、アーカイブプロセスのバグが理論上、ゴールデンルール(alwaysがトランザクションの合計に等しい)をサイレントブレイクDRIソリューションあり。
以下は、私が見るトランザクションアプローチの利点の要約です。
-編集
複雑さやリスクを追加せずにアーカイブできるようにするには、継続的に生成される(@Andrewと@Garikから借用する)別のサマリーテーブルにサマリー行を保持することを選択できます。
たとえば、要約が毎月の場合:
ニック。
主なアイデアは、残高とトランザクションのレコードを同じテーブルに格納することです。それは歴史的に起こりました。したがって、この場合、最後の要約レコードを見つけるだけでバランスをとることができます。
id user_id currency_id amount is_summary (or record_type)
----------------------------------------------------
1 3 1 10.60 0
2 3 1 10.60 1 -- summary after transaction 1
3 3 1 -55.00 0
4 3 1 -44.40 1 -- summary after transactions 1 and 3
5 3 1 -12.12 0
6 3 1 -56.52 1 -- summary after transactions 1, 3 and 5
より良い方法は、要約レコードの数を減らすことです。 1日の終わり(および/または始まり)に1つの残高レコードを持つことができます。ご存知のとおり、すべての銀行にはoperational day
開いて閉じ、この日のいくつかの要約操作を行います。毎日の残高レコードを使用することで、たとえばinterestを簡単に計算できます。次に例を示します。
user_id currency_id amount is_summary oper_date
--------------------------------------------------------------
3 1 10.60 0 01/01/2011
3 1 -55.00 0 01/01/2011
3 1 -44.40 1 01/01/2011 -- summary at the end of day (01/01/2011)
3 1 -12.12 0 01/02/2011
3 1 -56.52 1 01/02/2011 -- summary at the end of day (01/02/2011)
幸運。
要件に基づいて、オプション1が最適です。トランザクションテーブルへの挿入のみを許可するように設計しましたが。トランザクションテーブルにトリガーを設定して、リアルタイムバランステーブルを更新します。データベース権限を使用して、これらのテーブルへのアクセスを制御できます。
このアプローチでは、リアルタイムバランスがトランザクションテーブルと同期していることが保証されます。また、ストアドプロシージャ、psql、jdbcのどちらを使用しても問題ありません。必要に応じて、マイナスの残高チェックを行うことができます。パフォーマンスは問題になりません。リアルタイムバランスを取得するためのシングルトンクエリです。
アーカイブはこのアプローチには影響しません。レポートなどの必要に応じて、週次、月次、年次の要約表も作成できます。
Oracleでは、バランスを形成するために集計を行う高速で更新可能なマテリアライズドビューが含まれるトランザクションテーブルのみを使用してこれを行うことができます。トリガーはマテリアライズドビューで定義します。マテリアライズドビューが「ON COMMIT」で定義されている場合、ベーステーブルでのデータの追加/変更を効果的に防止します。トリガーは[in] validデータを検出し、トランザクションをロールバックする例外を発生させます。いい例がここにあります http://www.sqlsnippets.com/en/topic-12896.html
私はsqlserverを知りませんが、おそらく同じようなオプションがありますか?