确保始终选择最新的SQL记录进行计算

时间:2017-07-05 10:11:23

标签: sql sql-server sql-server-2012 transactions batch-processing

我们有一个存储在数据库表中的客户所做交易的财务日志,该表包含以下信息:

  • 交易ID
  • 客户ID
  • 交易日期/时间
  • 金额
  • 余额

新交易的余额是通过将最新的先前交易(使用交易的日期/时间)与相同的客户ID进行计算,并将其与新交易的金额相加。

由于这是一个事务日志,因此用户无法修改任何细节。

我们正在使用SQL Server的GETDATE来获取客户的当前余额。这是否足以确保我们始终选择客户的最新交易来进行余额计算?对我们来说这很重要,因为如果我们选择了不正确的交易,那么客户就可以做到这一点。事务日志不会加起来。

表的外观示例(图像,因为它看起来不像我可以使用markdown来创建表):

Example table of transaction logs

我们的系统还有用于批量添加事务的屏幕,这里我们将事务作为SQL批处理插入。在这里,为客户输入的两个或多个交易可能具有相同的日期/时间,从而导致后续交易的余额计算出现问题吗?

2 个答案:

答案 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_XRefFK_Ledger_XRef)的外键的一部分来确保先前的余额是正确的。

我们还有一些检查限制,以确保我们建立的历史记录是唯一且明确的 - 每个客户的交易必须从1向上编号,没有间隙。

创建该结构后,我们发现它比我们想要向用户公开的要复杂得多 - 所以我们创建了一个视图:

create view dbo.Ledger
with schemabinding
as
    select
        CustomerID,
        ISNULL(TransactionID,0) as TransactionID,
        OccurredAt,
        TransactionAmount,
        Balance
    from
        dbo._Ledger

现在只显示用户将关注的列,并隐藏它们的复杂性。当用户插入此视图时,他们只会填充CustomerIDOccurredAtTransactionAmount - 计算其他列(TransactionIDBalance

但是,插入此视图并不简单,因此我们还需要提供触发器:

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