web-dev-qa-db-ja.com

単純な銀行スキーマの作成:残高を取引履歴と同期させるにはどうすればよいですか?

単純な銀行データベースのスキーマを書いています。基本的な仕様は次のとおりです。

  • データベースは、ユーザーと通貨に対するトランザクションを格納します。
  • すべてのユーザーは通貨ごとに1つの残高を持っているので、各残高は特定のユーザーと通貨に対するすべてのトランザクションの合計です。
  • 残高がマイナスになることはありません。

銀行アプリケーションは、ストアード・プロシージャーのみを介してデータベースと通信します。

このデータベースは、1日あたり数十万の新しいトランザクションを受け入れるだけでなく、クエリのバランスをより高い桁で期待しています。 残高をすばやく提供するには、事前に集計する必要があります。同時に、残高が取引履歴と矛盾しないことを保証する必要があります。

私のオプションは:

  1. 別のbalancesテーブルを用意し、次のいずれかを実行します。

    1. transactionsテーブルとbalancesテーブルの両方にトランザクションを適用します。ストアドプロシージャレイヤーでTRANSACTIONロジックを使用して、残高とトランザクションが常に同期していることを確認します。 ( Jack でサポートされています。)

    2. トランザクションをtransactionsテーブルに適用し、balancesテーブルをトランザクション量で更新するトリガーを設定します。

    3. トランザクションをbalancesテーブルに適用し、transactionsテーブルにトランザクション量を含む新しいエントリを追加するトリガーを設定します。

    ストアドプロシージャの外部で変更が行われないようにするために、セキュリティベースのアプローチに依存する必要があります。それ以外の場合、たとえば、一部のプロセスがトランザクションをtransactionsテーブルに直接挿入し、スキーム1.3の下で、関連する残高が同期しなくなります。

  2. トランザクションを適切に集計するbalancesインデックス付きビューを用意します。残高はストレージエンジンによってトランザクションとの同期が保たれることが保証されているため、これを保証するためにセキュリティベースのアプローチに依存する必要はありません。一方、ビュー(インデックス付きビューでさえ)にCHECK制約を設定することはできないため、バランスを負でない値にすることはできません。 ( Denny でサポートされています。)

  3. 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

このように、アーカイブされたトランザクションとのバランスにより、完全で一貫したトランザクション履歴が維持されます。

60
Nick Chammas

私は会計に精通していませんが、在庫タイプの環境でいくつかの同様の問題を解決しました。トランザクションと同じ行に累計を保存します。私は制約を使用しているので、同時実行性が高くてもデータが間違っていることはありません。 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
17
A-K

考慮すべき少し異なるアプローチ(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;
    

長所:

  • 2つの別個のテーブルを維持する必要がなくなりました...
  • 残高は簡単に検証でき、残高が同期しなくなった場合は、トランザクション履歴が自己文書化されるため、バランスが崩れた時期を正確に特定できます。

編集:現在の残高の取得とconを強調表示するためのいくつかのサンプルクエリ(ありがとう@Jack Douglas)

15

顧客が0未満の残高を持つことを許可しないことはビジネスルールです(ドラフトのようなものに対する手数料が銀行がほとんどのお金を稼ぐ方法であるので、それはすぐに変わります)。行がトランザクション履歴に挿入されたときのアプリケーション処理でこれを処理する必要があります。特に、当座貸越保護を利用している顧客や、手数料が請求されている顧客、負の金額の入力を許可していない顧客がいる場合があります。

これまでのところ、これを使用するところが好きですが、これが実際のプロジェクト(学校ではない)の場合は、ビジネスルールなどに多くの考えを盛り込む必要があります。銀行システムが完成したら、そして、人々がお金にアクセスできるという非常に具体的な法律があるので、そこを実行することは再設計の余地があまりありません。

14
mrdenny

これらの2つの議論を読んだ後、私はオプション2を決定しました

それらの議論も読んだので、なぜあなたが概説する他のオプションの最も賢明なものよりも [〜#〜] dri [〜#〜] ソリューションを決定したのかはわかりません:

トランザクションテーブルとバランステーブルの両方にトランザクションを適用します。ストアドプロシージャレイヤーでTRANSACTIONロジックを使用して、残高とトランザクションが常に同期していることを確認します。

トランザクションAPIを介したデータへのallへのアクセスを制限する贅沢がある場合、この種類のソリューションには非常に実用的なメリットがあります。 DRIの非常に重要な利点は失われます。つまり、データベースによって整合性が保証されますが、十分に複雑なモデルでは、DRIによって適用できないビジネスルールがいくつかありますです。

可能な場合はモデルを過度に曲げることなくビジネスルールを適用できるように、可能な場合はDRIを使用することをお勧めします。

トランザクションをアーカイブしている場合でも(たとえば、トランザクションを別の場所に移動して、それらを要約トランザクションに置き換えることにより)

このようにモデルを汚染することを検討し始めるとすぐに、DRIの利点が導入する難しさを上回っている領域に移動していると思います。たとえば、アーカイブプロセスのバグが理論上、ゴールデンルール(alwaysがトランザクションの合計に等しい)をサイレントブレイクDRIソリューションあり

以下は、私が見るトランザクションアプローチの利点の要約です。

  • 可能な限り、とにかくこれを行うべきです。この特定の問題に対してどのソリューションを選択しても、設計の柔軟性とデータの制御が向上します。すべてのアクセスは、データベースロジックだけでなく、ビジネスロジックの観点から「トランザクション」になります。
  • モデルをきれいに保つことができます
  • はるかに広範囲で複雑なビジネスルールを "適用"できます( "適用"の概念はDRIを使用する場合よりも緩いということに注意してください)。
  • 実用的な場所であればDRIを使用して、モデルの基礎となる完全性をより堅牢にすることができます。これは、トランザクションロジックのチェックとして機能します。
  • あなたを悩ませているパフォーマンスの問題のほとんどは溶けてしまいます
  • 新しい要件の導入ははるかに簡単になる可能性があります。たとえば、異議のあるトランザクションの複雑なルールは、純粋なDRIアプローチから遠ざかる可能性があり、多くの無駄な努力を意味します。
  • 履歴データのパーティション化またはアーカイブにより、リスクと痛みが軽減されます

-編集

複雑さやリスクを追加せずにアーカイブできるようにするには、継続的に生成される(@Andrewと@Garikから借用する)別のサマリーテーブルにサマリー行を保持することを選択できます。

たとえば、要約が毎月の場合:

  • (APIを介して)トランザクションが発生するたびに、対応する更新またはサマリーテーブルへの挿入があります。
  • 要約テーブルはneverアーカイブされますが、トランザクションのアーカイブは削除、削除(またはパーティションの削除?)と同じくらい簡単になります。
  • 概要表の各行には、「期首残高」と「金額」が含まれています
  • 「期首残高」+「金額」> 0や「期首残高」> 0などのチェック制約をサマリーテーブルに適用できます。
  • 最新の集計行のロックを容易にするために、集計行を月次バッチに挿入できます(現在の月の行は常に存在します)

ニック。

主なアイデアは、残高とトランザクションのレコードを同じテーブルに格納することです。それは歴史的に起こりました。したがって、この場合、最後の要約レコードを見つけるだけでバランスをとることができます。

 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)

幸運。

6
garik

要件に基づいて、オプション1が最適です。トランザクションテーブルへの挿入のみを許可するように設計しましたが。トランザクションテーブルにトリガーを設定して、リアルタイムバランステーブルを更新します。データベース権限を使用して、これらのテーブルへのアクセスを制御できます。

このアプローチでは、リアルタイムバランスがトランザクションテーブルと同期していることが保証されます。また、ストアドプロシージャ、psql、jdbcのどちらを使用しても問題ありません。必要に応じて、マイナスの残高チェックを行うことができます。パフォーマンスは問題になりません。リアルタイムバランスを取得するためのシングルトンクエリです。

アーカイブはこのアプローチには影響しません。レポートなどの必要に応じて、週次、月次、年次の要約表も作成できます。

4
Elan Fisoc

Oracleでは、バランスを形成するために集計を行う高速で更新可能なマテリアライズドビューが含まれるトランザクションテーブルのみを使用してこれを行うことができます。トリガーはマテリアライズドビューで定義します。マテリアライズドビューが「ON COMMIT」で定義されている場合、ベーステーブルでのデータの追加/変更を効果的に防止します。トリガーは[in] validデータを検出し、トランザクションをロールバックする例外を発生させます。いい例がここにあります http://www.sqlsnippets.com/en/topic-12896.html

私はsqlserverを知りませんが、おそらく同じようなオプションがありますか?

3
ik_zelf