I am writing the schema for a simple bank database. Here are the basic specifications:
- The database will store transactions against a user and currency.
- Every user has one balance per currency, so each balance is simply the sum of all transactions against a given user and currency.
- A balance cannot be negative.
The bank application will communicate with its database exclusively through stored procedures.
I expect this database to accept hundreds of thousands of new transactions per day, as well as balance queries on a higher order of magnitude. To serve up balances very quickly I need to pre-aggregate them. At the same time, I need to guarantee that a balance never contradicts its transaction history.
My options are:
Have a separate
balances
table and do one of the following:Apply transactions to both the
transactions
andbalances
tables. UseTRANSACTION
logic in my stored procedure layer to ensure that balances and transactions are always in sync. (Supported by Jack.)Apply transactions to the
transactions
table and have a trigger that updates thebalances
table for me with the transaction amount.Apply transactions to the
balances
table and have a trigger that adds a new entry in thetransactions
table for me with the transaction amount.
I have to rely on security-based approaches to make sure no changes can be made outside of the stored procedures. Otherwise, for example, some process could directly insert a transaction into the
transactions
table and under scheme1.3
the relevant balance would be out of sync.Have a
balances
indexed view that aggregates the transactions appropriately. Balances are guaranteed by the storage engine to stay in sync with their transactions, so I don't need to rely on security-based approaches to guarantee this. On the other hand, I cannot enforce balances be non-negative anymore since views -- even indexed views -- cannot haveCHECK
constraints. (Supported by Denny.)Have just a
transactions
table but with an additional column to store the balance effective right after that transaction executed. Thus, the latest transaction record for a user and currency also contains their current balance. (Suggested below by Andrew; variant proposed by garik.)
When I first tackled this problem, I read these two discussions and decided on option 2
. For reference, you can see a bare-bones implementation of it here.
Have you designed or managed a database like this with a high load profile? What was your solution to this problem?
Do you think I've made the right design choice? Is there anything I should keep in mind?
For example, I know schema changes to the
transactions
table will require I rebuild thebalances
view. Even if I am archiving transactions to keep the database small (e.g. by moving them somewhere else and replacing them with summary transactions), having to rebuild the view off tens of millions of transactions with every schema update will probably mean significantly more downtime per deployment.If the indexed view is the way to go, how can I guarantee that no balance is negative?
Archiving transactions:
Let me elaborate a bit on archiving transactions and the "summary transactions" I mentioned above. First, regular archiving will be a necessity in a high-load system like this. I want to maintain consistency between balances and their transaction histories while allowing old transactions to be moved somewhere else. To do this I'll replace every batch of archived transactions with a summary of their amounts per user and currency.
So, for example, this list of transactions:
user_id currency_id amount is_summary
------------------------------------------------
3 1 10.60 0
3 1 -55.00 0
3 1 -12.12 0
is archived away and replaced with this:
user_id currency_id amount is_summary
------------------------------------------------
3 1 -56.52 1
In this way, a balance with archived transactions maintains a complete and consistent transaction history.