这是一个 SQL 2008 版本:
CREATE PROCEDURE dbo.PaymentApply
@CustID int,
@Amount decimal(11, 2),
@AsOfDate datetime
AS
WITH Totals AS (
SELECT
T.*,
RunningTotal =
Coalesce (
(SELECT Sum(S.Buyin - Coalesce(S.CreditPaid, 0))
FROM dbo.Trans S
WHERE
T.CustID = S.CustID
AND S.Type = 'Credit'
AND S.Buyin < Coalesce(S.CreditPaid, 0)
AND (
T.Starttime > S.Starttime
OR (
T.Starttime = S.Starttime
AND T.TrID > S.TrID
)
)
),
0)
FROM
dbo.Trans T
WHERE
CustID = @CustID
AND T.Type = 'Credit'
AND T.Buyin < Coalesce(T.CreditPaid, 0)
)
UPDATE T
SET
T.EndTime = P.EndTime,
T.CreditPaid = Coalesce(T.CreditPaid, 0) + P.CreditPaid
FROM
Totals T
CROSS APPLY (
SELECT TOP 1
V.*
FROM
(VALUES
(T.Buyin - Coalesce(T.CreditPaid, 0), @AsOfDate),
(@Amount - RunningTotal, NULL)
) V (CreditPaid, EndTime)
ORDER BY
V.CreditPaid,
V.EndTime DESC
) P
WHERE
T.RunningTotal <= @Amount
AND @Amount > 0;
;
在 SQL Fiddle 上查看现场演示
或者,对于使用 SQL 2012 的任何人,您可以使用新的窗口函数将 CTE 的内容替换为性能更好、更简单的查询:
SELECT
*,
RunningTotal =
Sum(Buyin - Coalesce(CreditPaid, 0)) OVER(
ORDER BY StartTime
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) - Buyin
FROM dbo.Trans
WHERE
CustID = @CustID
AND Type = 'Credit'
AND Buyin - Coalesce(CreditPaid, 0) > 0
在 SQL Fiddle 上查看现场演示
以下是它们的工作方式:
CreditPaid
我们计算金额小于金额的所有先前行的运行总计Buyin
。请注意,这不包括当前行。
- 由此,我们可以确定支付的哪一部分将应用于每一行以及哪些行将参与支付。如果前面所有行的所有积分的总和都高于付款,则该行将不包括在内,因此
T.RunningTotal <= @Amount
。那是因为此时所有先前的行都将完全消耗付款,因此我们可以停止应用它。
- 对于我们将应用付款的每一行,我们希望尽可能多地支付,但我们必须注意我们可能没有支付全部金额的最后一行(如示例中的第三个贷项) )。因此,我们将支付以下两个金额中的一个:全部信用金额(有更多行来接收付款)或仅剩余可能少于该行的全部信用的部分(这是最后一行)。我们通过取 1) 全部剩余
Buyin - CreditPaid
金额或 2) 全部剩余金额中的较小者来实现这一点@Amount - RunningTotalOfPriorRows
。我可以将其作为CASE
表达式来完成,但我喜欢使用该Min
函数,特别是因为我们必须执行两个CASE
表达式来确定是否也更新EndTime
列(根据您的要求)。
- SQL 2012 版本只计算与 2008 版本相同的东西:
Buyin - CreditPaid
所有先前行的总和,使用窗口函数而不是相关子查询。
- 最后,我们对小于要应用的金额的所有行执行更新
RunningTotal
(因为如果它等于金额,则当前行将没有付款)。
现在,您应该考虑一些更大的考虑因素。
我喜欢你的一些计划——我不相信,正如一些评论者所说,你应该忽略个别交易。我认为处理个人交易可能非常重要。这很像医院如何为每位患者(MRN)提供一个病历编号,但要开设一个新帐户/文件/访问每次患者进行服务时。每个帐户都被单独处理,这有很多原因,包括——这对你来说似乎也很重要——客户需要了解究竟是什么构成了总数。看到总数加起来可能会令人震惊,但是当将其分解为个别日期的个别交易时,这对人们来说更有意义,他们可以开始确切地了解他们是如何花的钱比他们记得的更多时间。“你欠我 600 美元”比“你的 100 美元、300 美元和 200 美元的交易仍未支付”更难面对。:)
所以,这里有一些重要的考虑。
如果您遵循交易或基于余额的帐户从 0 开始作为一种“锚”的理论,并且要找到当前余额,您只需将所有交易相加:嗯,这确实满足关系理论,但在实践中它是完全行不通的,因为它没有提供一种快速、准确的方法来获得当前的余额。必须将当前余额保存为离散值。如果你是一家银行,你怎么知道你有多少钱,而不是每次加起来可能有几十年的交易历史?相反,最好将当前余额视为“锚”(而不是 0)并将交易视为倒退及时。此外,记录定期余额也没有坏处。银行通过将期间关闭到报表中来做到这一点,在每个报表关闭日期都有定义的余额。没有必要一直回到零,因为您不太关心历史旧的、未锚定的终结处的平衡。你知道,最终每个账户都是从 0 开始的。大不了!
考虑到这些想法,对您来说拥有一张简单地说明客户总账户余额的表格非常重要。您还需要一个地方来记录他的付款、退款、取消等。这应该与帐户(在您的情况下为交易)本身分开,因为付款交易和信用交易之间没有一对一的对应关系。在您当前的计划中,您已经部分支付了没有记录日期的交易——这是系统中的一个巨大差距,它将回来咬你。如果客户每天支付 10 美元换取 200 美元的信用额度,为期 20 天怎么办?其中 19 笔付款显示没有付款日期。
那么,我建议您创建一个存储过程 (SP),首先将付款应用于总计,然后创建另一个以按需方式将付款“重写”到交易中的存储过程。想想如果信用卡公司“重新评估”您的帐户,他们必须做什么。也许他们对不正确的信息采取了行动,并在某个日期提高了您的利率。(这实际上发生在我身上。我向他们证明,他们回应的收款活动不是我的错——在我向他们展示他们的一名员工错误地更改了我的邮寄地址后,它已被原公司撤回,并且我从来没有收到可以支付的账单。所以他们必须能够追溯地重新运行我账户上的所有购买/借记/利率计算,根据正确的利率重新计算原始更改日期之后的所有内容。)考虑一下,您会发现只要您正确设计系统,就可以这样操作。你的 SP 被赋予一个日期范围或允许它工作的事务集,然后“重写”历史,就好像旧的历史从未存在过一样。
但是,您实际上并不想破坏历史记录,因此,在某个时间点,您对客户账户余额的最佳了解与您当前对客户账户余额的最佳了解不同,这使情况变得更加复杂。该时间段的账户余额——两者都是真实数据,需要保留。
假设您发现您的系统偶尔会错误地将信用交易加倍。当您修复客户数据时,您需要能够看到他们有问题的事实,即使他们现在没有问题。这是通过使用额外的日期列EffectiveDate
和ExpirationDate
-- 或任何你想调用它们来完成的。然后,这些需要成为聚集索引的一部分,并在每个查询中使用以始终获取当前值。我强烈建议使用9999-12-31
而不是NULL
作为ExpirationDate
当前行的值——这将对查询当前数据时的性能产生巨大的积极影响。我还建议将 放在ExpirationDate
聚集索引中的第一列(或至少在EffectiveDate
列),因为历史总是可能比未来有更多的记录,所以它会比EffectiveDate
第一次更有选择性(想一想:所有过去的知识都会有EffectiveDate =< GetDate()
,但只有当前或未来的数据会有ExpirationDate > GetDate()
)。把重点放在家里:你不要删除。您通过将一列设置为知识过时的日期来使旧行过期,然后插入代表新知识的新行,其中一列显示您学习此信息的日期,并且在其他日期列。
最后是几点:
- 该
CreditPaid
列NOT NULL
的默认值为0
. 我不得不扔一堆Coalesce
s 来处理NULL
s。
- 您需要以某种方式处理多付款项。通过阻止它们,或者通过存储多付的部分值并在以后应用它。您可以将 UPDATE 语句的结果输出到表中,然后从中选择 Sum 并使 SP 返回任何未使用的付款值。有很多方法可以处理这个问题。如果您按照我的建议构建“重新评级”SP,那么这不会有太大问题,因为您可以在收到新交易后重新运行它(然后立即(重新)应用任何开放期间的所有付款)。
在这一点上我不能再讲太多了,但我希望这些想法对你有所帮助。您的设计是一个良好的开端,但它需要一些工作才能使其达到可以作为企业质量系统良好运行的程度。
更新
我更正了 2008 版本中的一个小故障(将外部查询中的条件添加到子查询中)。
这是我的最后一次编辑(全部:请不要再次编辑此答案,否则它将转换为社区 wiki)。
如果您确实采用了一种方案,其中行被标记为它们被理解为真实的日期(EffectiveDate
和ExpirationDate
),您可以通过创建仅从表中选择活动行的内联表函数来简化系统中的编码WHERE EffectiveDate <= GetDate() AND GetDate() < ExpirationDate
。请特别注意您正在使用的比较运算符(例如<=
vs ),并使用开头包含但结尾不包含<
的日期范围。如果您不确定这意味着什么,请在继续之前查看这些术语并理解它们。您希望将来能够更改日期数据类型的分辨率,而不会中断任何查询。如果您使用包容性结束日期,这是不可能的。网上有很多帖子讨论如何在 SQL 中正确查询日期。
像这样的东西:
CREATE FUNCTION dbo.TransCurrent
RETURNS TABLE
AS
RETURN (
SELECT *
FROM dbo.Trans
WHERE
EffectiveDate <= GetDate()
AND GetDate() < ExpirationDate --make clustered index have this first!
);
不要将此与多语句表值返回函数混淆。那将不会表现良好。这里的这种函数类型将表现良好,因为它可以内联到查询中,基本上引擎采用函数正在执行的逻辑意图,并完全处理函数调用。使用任何其他类型的功能都可以解决这个问题,并且随着您的桌子变大,您的表现将进入底池。