Nic*_*mas 70 sql-server-2008 database-design sql-server aggregate materialized-view
我正在为一个简单的银行数据库编写架构。以下是基本规格:
银行应用程序将专门通过存储过程与其数据库通信。
我希望这个数据库每天接受数十万个新事务,以及更高数量级的平衡查询。为了非常快速地提供余额,我需要预先汇总它们。同时,我需要保证余额永远不会与其交易历史相矛盾。
我的选择是:
有一个单独的balances表并执行以下操作之一:
将事务应用于transactions和balances表。TRANSACTION在我的存储过程层使用逻辑来确保余额和交易始终同步。(由杰克支持。)
将交易应用到transactions表并有一个触发器,balances用交易金额为我更新表。
将交易应用到balances表中,并有一个触发器transactions为我在表中添加一个新条目,其中包含交易金额。
我必须依靠基于安全的方法来确保在存储过程之外不能进行任何更改。否则,例如,某些进程可以直接将事务插入transactions表中,并且在该方案1.3下相关余额将不同步。
有一个balances索引视图,可以适当地聚合事务。存储引擎保证余额与其交易保持同步,因此我不需要依赖基于安全的方法来保证这一点。另一方面,我不能再强制余额为非负数,因为视图——甚至索引视图——不能有CHECK约束。(由丹尼支持。)
只有一个transactions表,但有一个额外的列来存储该交易执行后立即生效的余额。因此,用户和货币的最新交易记录也包含其当前余额。(下面由Andrew建议;由garik提出的变体。)
当我第一次解决这个问题时,我阅读了这 两个讨论并决定了 option 2。作为参考,您可以在此处查看它的基本实现。
您是否设计或管理过这样的具有高负载配置文件的数据库?你对这个问题的解决方案是什么?
你认为我做出了正确的设计选择吗?有什么我应该记住的吗?
例如,我知道对transactions表的架构更改需要我重建balances视图。即使我正在归档事务以保持数据库较小(例如,将它们移到其他地方并用摘要事务替换它们),每次架构更新时都必须重建数千万个事务的视图,这可能意味着每次部署的停机时间会显着增加。
如果索引视图是要走的路,我如何保证没有余额为负?
归档交易:
让我详细说明一下存档交易和我上面提到的“摘要交易”。首先,在像这样的高负载系统中,定期存档将是必要的。我想保持余额与其交易历史之间的一致性,同时允许将旧交易转移到其他地方。为此,我将用每个用户和货币的金额摘要替换每批存档交易。
因此,例如,此交易列表:
user_id currency_id amount is_summary
------------------------------------------------
3 1 10.60 0
3 1 -55.00 0
3 1 -12.12 0
Run Code Online (Sandbox Code Playgroud)
已存档并替换为:
user_id currency_id amount is_summary
------------------------------------------------
3 1 -56.52 1
Run Code Online (Sandbox Code Playgroud)
通过这种方式,具有存档交易的余额可以保持完整且一致的交易历史记录。
And*_*ton 17
要考虑的一种稍微不同的方法(类似于您的第二个选项)是只有事务表,其定义为:
CREATE TABLE Transaction (
UserID INT
, CurrencyID INT
, TransactionDate DATETIME
, OpeningBalance MONEY
, TransactionAmount MONEY
);
Run Code Online (Sandbox Code Playgroud)
您可能还需要交易 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
Run Code Online (Sandbox Code Playgroud)
缺点:
用户/货币的交易需要序列化以保持准确的平衡。
-- 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;
Run Code Online (Sandbox Code Playgroud)优点:
编辑:关于检索当前余额并突出显示骗局的一些示例查询(感谢@Jack Douglas)
A-K*_*A-K 17
我不熟悉会计,但我在库存类型的环境中解决了一些类似的问题。我将运行总计与事务存储在同一行中。我正在使用约束,因此即使在高并发下我的数据也永远不会出错。我在 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
Run Code Online (Sandbox Code Playgroud)
mrd*_*nny 14
不允许客户的余额低于 0 是一项商业规则(这会很快发生变化,因为像透支这样的费用是银行赚取大部分资金的方式)。当行被插入到事务历史中时,您需要在应用程序处理中处理这个问题。特别是因为您最终可能会遇到一些客户有透支保护和一些收取费用以及一些不允许输入负金额的情况。
到目前为止,我喜欢你的做法,但如果这是一个实际项目(而不是学校),则需要在业务规则等方面深思熟虑。一旦你建立了一个银行系统并且运行没有很大的重新设计空间,因为有关于人们可以使用他们的钱的非常具体的法律。
Jac*_*las 13
在阅读了这两个讨论后,我决定了选项 2
也阅读了这些讨论后,我不确定您为什么选择DRI解决方案而不是您概述的最明智的其他选项:
将交易应用于交易和余额表。在我的存储过程层中使用 TRANSACTION 逻辑来确保余额和交易始终同步。
如果您可以通过事务 API限制对数据的所有访问,这种解决方案具有巨大的实际好处。你失去了 DRI 的一个非常重要的好处,即完整性由数据库保证,但在任何足够复杂的模型中,都会有一些 DRI 无法执行的业务规则。
我建议在可能的情况下使用 DRI 来强制执行业务规则,而不会过度弯曲您的模型以使其成为可能:
即使我正在归档交易(例如将它们移到其他地方并用汇总交易替换它们)
一旦你开始考虑像这样污染你的模型,我认为你正在进入一个 DRI 的好处被你引入的困难所抵消的领域。例如,考虑到归档过程中的错误在理论上可能会导致您的黄金法则(余额始终等于交易总和)在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
Run Code Online (Sandbox Code Playgroud)
更好的变体是减少摘要记录的数量。我们可以在一天结束(和/或开始)有一个余额记录。如您所知,每家银行都operational day必须打开而不是关闭它才能为这一天做一些总结性操作。它使我们可以通过使用每日余额记录轻松计算利息,例如:
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)
Run Code Online (Sandbox Code Playgroud)
运气。
| 归档时间: |
|
| 查看次数: |
68002 次 |
| 最近记录: |