我们有一个存储在数据库表中的客户所做交易的财务日志,该表包含以下信息:
新交易的余额是通过将最新的先前交易(使用交易的日期/时间)与相同的客户ID进行计算,并将其与新交易的金额相加。
由于这是一个事务日志,因此用户无法修改任何细节。
我们正在使用SQL Server的GETDATE来获取客户的当前余额。这是否足以确保我们始终选择客户的最新交易来进行余额计算?对我们来说这很重要,因为如果我们选择了不正确的交易,那么客户就可以做到这一点。事务日志不会加起来。
表的外观示例(图像,因为它看起来不像我可以使用markdown来创建表):
我们的系统还有用于批量添加事务的屏幕,这里我们将事务作为SQL批处理插入。在这里,为客户输入的两个或多个交易可能具有相同的日期/时间,从而导致后续交易的余额计算出现问题吗?
答案 0 :(得分:0)
对于这类问题,我的出发点始终是尝试创建无法存储错误数据的表结构。以此为出发点,您可以在其周围添加其他结构,以使其更有用。
所以,如果我们已经决定了一个总计是一个好主意(我通常建议反对这一点,如评论中所示,并且只是计算它需求),我们需要创建一个促进这个的表结构:
create table dbo._Ledger (
CustomerID int not null,
TransactionID int not null,
OccurredAt datetime2 not null,
TransactionAmount decimal(22,5) not null,
PreviousTransactionID as CASE WHEN TransactionID > 1
THEN TransactionID - 1 END persisted,
PreviousBalance decimal(22,5) null,
Balance as CONVERT(decimal(22,5),
ISNULL(PreviousBalance,0.0) + TransactionAmount) persisted,
constraint PK_Ledger PRIMARY KEY (CustomerID,TransactionID),
constraint CK_Ledger_IDs CHECK (TransactionID > 0),
constraint CK_Ledger_Continuous CHECK (TransactionID=1 and PreviousBalance is null or
TransactionID > 1 and PreviousBalance is not null),
constraint UQ_Ledger_XRef UNIQUE (CustomerID,TransactionID,Balance),
constraint FK_Ledger_XRef
FOREIGN KEY (CustomerID,PreviousTransactionID,PreviousBalance)
references dbo._Ledger(CustomerID,TransactionID,Balance)
)
通过上述结构,我们所拥有的是一个为每个客户构建分类帐的表。 Balance
列是计算列 - 通过将交易金额添加到先前余额来计算。并且我们通过将其作为引用上一行(UQ_Ledger_XRef
和FK_Ledger_XRef
)的外键的一部分来确保先前的余额是正确的。
我们还有一些检查限制,以确保我们建立的历史记录是唯一且明确的 - 每个客户的交易必须从1
向上编号,没有间隙。
创建该结构后,我们发现它比我们想要向用户公开的要复杂得多 - 所以我们创建了一个视图:
create view dbo.Ledger
with schemabinding
as
select
CustomerID,
ISNULL(TransactionID,0) as TransactionID,
OccurredAt,
TransactionAmount,
Balance
from
dbo._Ledger
现在只显示用户将关注的列,并隐藏它们的复杂性。当用户插入此视图时,他们只会填充CustomerID
,OccurredAt
和TransactionAmount
- 计算其他列(TransactionID
和Balance
)
但是,插入此视图并不简单,因此我们还需要提供触发器:
create trigger Ledger_I
on dbo.Ledger
instead of insert
as
;With Ordered as (
select
*,
ROW_NUMBER() OVER (PARTITION BY CustomerID order by OccurredAt) as rn,
SUM(TransactionAmount) OVER (PARTITION BY CustomerID
order by OccurredAt
rows between unbounded preceding and current row)
- TransactionAmount as Running
from
inserted
), Historic as (
select
*,
ROW_NUMBER() OVER (PARTITION BY CustomerID Order by TransactionID desc) as rn
from
dbo._Ledger
where
CustomerID in (select CustomerID from inserted)
)
insert into dbo._Ledger
(CustomerID,TransactionID,OccurredAt,TransactionAmount,PreviousBalance)
select o.CustomerID,
o.rn + COALESCE(h.TransactionID,0),
o.OccurredAt,o.TransactionAmount,
CASE WHEN o.rn > 1 or h.Balance is not null THEN h.Balance + o.Running END
from
Ordered o
left join
Historic h
on
o.CustomerID = h.CustomerID and
h.rn = 1
希望您可以看到公用表表达式如何协同工作以分配正确的TransactionID
并填充正确的PreviousBalance
金额。
(顺便说一下,表格上的_
前缀只是我对用户的约定,我并不打算让用户直接访问。对它没有特别的意义)< / p>
答案 1 :(得分:0)
我认为您可以查询以下窗口函数:
Select *, Balance = sum(Amount) over( partition by CustomerId order by TransactionDate)
from #Transaction