Mit*_*hir 31 rdbms database-design
我们正在开发一个预付卡平台,该平台主要保存有关卡及其余额、付款等的数据。
到目前为止,我们有一个 Card 实体,它有一个 Account 实体的集合,每个 Account 都有一个 Amount,它在每次存款/取款时更新。
现在团队中有一场辩论;有人告诉我们,这违反了Codd 的 12 条规则,并且在每次付款时更新其价值很麻烦。
这真的有问题吗?
如果是,我们如何解决这个问题?
db2*_*db2 30
是的,这是非规范化的,但有时出于性能原因,非规范化设计会胜出。
但是,出于安全原因,我可能会以不同的方式处理它。(免责声明:我目前没有,也从未在金融领域工作过。我只是把它扔在那里。)
在卡片上有一张过帐余额表。这将为每个帐户插入一行,指示每个期间(日、周、月或任何适当的)结束时的过帐余额。按帐号和日期索引此表。
使用另一个表来保存待处理的事务,这些事务是即时插入的。在每个期间结束时,运行一个例程,将未过帐的交易记录添加到帐户的最后一个期末余额中以计算新余额。要么将挂起的交易标记为已发布,要么查看日期以确定哪些仍处于挂起状态。
这样,您就有了一种按需计算卡余额的方法,而无需汇总所有帐户历史记录,并且通过将余额重新计算放在专用的过帐例程中,可以确保此重新计算的交易安全性仅限于一个地方(并且还限制余额表的安全性,因此只有过帐例程可以写入)。
然后根据审计、客户服务和性能要求保留尽可能多的历史数据。
Chr*_*ers 17
另一方面,我们在会计软件中经常遇到一个问题。释义:
我真的需要汇总十年的数据来找出支票账户中有多少钱吗?
答案当然是否定的。这里有几种方法。一种是存储计算值。我不推荐这种方法,因为导致错误值的软件错误很难追踪,所以我会避免这种方法。
更好的方法是我所说的日志快照聚合方法。在这种方法中,我们的付款和使用是插入,我们从不更新这些值。我们定期聚合一段时间内的数据并插入一个计算的快照记录,该记录表示快照生效时的数据(通常是出现之前的一段时间)。
现在这并没有违反 Codd 的规则,因为随着时间的推移,快照可能不太完全依赖于插入的支付/使用数据。如果我们有工作快照,我们可以决定清除 10 年前的数据,而不会影响我们按需计算当前余额的能力。
出于性能原因,在大多数情况下,我们必须存储当前余额 - 否则动态计算最终可能会变得非常缓慢。
我们确实在我们的系统中存储了预先计算的运行总数。为了保证数字总是正确的,我们使用约束。以下解决方案已从我的博客中复制。它描述了一个清单,本质上是相同的问题:
无论您是使用游标还是三角形连接,计算运行总计都非常缓慢。非规范化非常诱人,将运行总计存储在列中,特别是如果您经常选择它。但是,像往常一样,在进行非规范化时,您需要保证非规范化数据的完整性。幸运的是,您可以通过约束保证运行总计的完整性——只要您的所有约束都是可信的,您的所有运行总计都是正确的。此外,通过这种方式,您可以轻松确保当前余额(运行总计)永远不会为负 - 通过其他方法执行也可能非常缓慢。以下脚本演示了该技术。
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
Run Code Online (Sandbox Code Playgroud)
这个问题问得好。
假设您有一个存储每个借记/贷记的交易表,那么您的设计没有任何问题。事实上,我曾使用过以这种方式工作的预付费电信系统。
您需要做的主要事情是确保SELECT ... FOR UPDATE您INSERT在借记/贷记时进行余额处理。如果出现问题,这将保证正确的余额(因为整个事务将被回滚)。
正如其他人指出的那样,您需要特定时间段的余额快照,以验证给定期间内的所有交易总额与期间开始/结束余额是否正确。编写一个在期间结束(月/周/日)午夜运行的批处理作业来执行此操作。