【问题标题】:Update records and set values更新记录和设置值
【发布时间】:2013-05-20 14:19:58
【问题描述】:

我有一个名为 Transaction 的表,其中包含一些列:[TransactionID, Type(credit or debit), Amount, Cashout, CreditPaid, EndTime]

客户可以获得大量信用,这些交易存储在交易表中。如果客户在月底支付了涵盖部分或全部信用交易的金额,我希望更新这些交易。如果总付款涵盖某些交易,则应更新交易。

例如,客户支付 300。如果交易“Amount”为 300,“Type”为 credit,则“CreditPaid”金额应为 300。(这是一个简单的更新语句)但是...

如果有两笔交易,即一笔 300 笔和另一笔 400 笔,且均为贷项且每月付款金额为 600,则最早的一笔交易应全额支付 300 笔,下一笔交易为 300 笔,剩余 100 笔未结。

任何想法如何做到这一点?

TrID  Buyin Type    Cashout CustID  StartTime  EndTime  AddedBy CreditPaid
72    200   Credit    0 132 2013-05-21 NULL     NULL    NULL
73    300   Credit    0 132 2013-05-22 NULL     NULL    NULL
75    400   Credit    0 132 2013-05-23 NULL     NULL    NULL

客户支付 600 美元后的期望结果

TrID  Buyin Type    Cashout CustID  StartTime  EndTime  AddedBy CreditPaid
72    200   Credit     0    132 2013-05-21 2013-05-24   NULL    200
73    300   Credit     0    132 2013-05-22 2013-05-24   NULL    300
75    400   Credit     0    132 2013-05-23 NULL     NULL    100

【问题讨论】:

  • 您确实需要详细说明示例数据和您的预期结果。
  • 虽然这听起来像是一个完全不同的问题,但这里有一个非常有用的例子可能会被改编:stackoverflow.com/questions/10327741/…
  • EndTime 是干什么用的?如果这记录了还清信贷的时间,那么如果分期付款会发生什么?就个人而言,我想我想要更多关于付款时间的历史记录,这意味着另一张桌子。
  • 想一想:当客户支付 300 美元并欠 700 美元(300 + 400)时,他们是还清了 300 美元,还是只支付了欠款总额?在大多数情况下,他们是根据欠款总额支付的,因此支付的金额不会映射到个别交易,而只是映射到总额。
  • 您的建议真的很有帮助。你觉得我应该怎么处理? ClockworkMuse 提到了另一张桌子。我的想法是,我想让用户尽可能方便,因为他们不会费心去完成所有交易并单独支付每一笔交易。因此,通过向系统中添加一笔大笔款项并自动更新所有信用交易,我认为这将是最好的解决方案。 * EndTime 是未偿债务的支付日期。

标签: sql sql-server


【解决方案1】:

这是一个 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;
;

See a Live Demo at 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

See a Live Demo at SQL Fiddle

它们的工作原理如下:

  1. 我们计算CreditPaid 数量小于Buyin 数量的所有先前行的总和。请注意,这不包括当前行。
  2. 据此,我们可以确定支付的哪一部分将应用于每一行以及哪些行将参与支付。如果之前所有行的所有信用总和高于付款,则不会包括此行,因此T.RunningTotal &lt;= @Amount。那是因为之前所有的行都将在此时完全消耗付款,所以我们可以停止应用它。
  3. 对于我们将应用付款的每一行,我们希望尽可能多地付款,但我们必须注意最后一行我们可能没有支付全部金额(如第三个信用证的情况)在示例中)。因此,我们将支付以下两个金额之一:全部信用金额(有更多行来接收付款)或仅剩余部分可能少于该行的全部信用(这是最后一行)。我们通过取 1) 全部剩余 Buyin - CreditPaid 金额或 2) 全部金额 @Amount - RunningTotalOfPriorRows 中的较小值来实现这一点。我本可以将其作为 CASE 表达式来完成,但我喜欢使用 Min 函数,特别是因为我们必须执行两个 CASE 表达式来确定是否也更新 EndTime 列(根据您的要求)。
  4. SQL 2012 版本只计算与 2008 版本相同的东西:使用窗口函数而不是相关子查询,计算所有先前行的 Buyin - CreditPaid 的总和。
  5. 最后,我们对 RunningTotal 小于要应用的金额的所有行执行更新(因为如果等于金额,则当前行将没有任何付款)。

现在,您应该考虑一些更大的考虑因素。

我喜欢你的一些方案——我不相信,正如一些评论者所说,你应该忽略个别交易。我认为处理单个交易可能非常很重要。这很像医院为每位患者 (MRN) 设置一个病历编号,但每次患者打开一个新的帐户 / 文件 / 访问执行了一项服务。每个帐户都被单独处理,这有很多原因,包括——这对你来说似乎也很重要——客户需要了解究竟是什么构成了总数。看到总数加起来可能会令人震惊,但是当将其分解为个别日期的个别交易时,这对人们来说更有意义,他们可以开始确切地了解他们是如何花的钱比他们记得的更多时间。 “你欠我 600 美元”比“你的 100 美元、300 美元和 200 美元的交易仍未支付”更难面对。 :)

所以,这里有一些重要的考虑因素。

如果您遵循交易或基于余额的账户从 0 开始作为一种“锚”的理论,并且要找到当前余额,您只需将所有交易相加:嗯,这确实满足关系理论,但在实践中它是完全行不通的,因为它没有提供一种快速、准确的方法来获得当前的平衡。必须将当前余额保存为离散值。如果你是一家银行,你怎么知道你有多少钱,而不是每次加起来可能有几十年的交易历史?相反,最好将当前余额视为“锚”(而不是 0),并将交易视为及时向后。此外,记录定期余额也没有坏处。银行通过将期间关闭到报表中来做到这一点,在每个报表关闭日期都有定义的余额。没有必要一直回到零,因为您不太关心历史旧的、未锚定的终结处的平衡。您知道,最终每个帐户都是从 0 开始的。很重要!

考虑到这些想法, 对您而言,有一个表格很重要,其中简单地说明了客户的总帐户余额。您还需要一个地方来记录他的付款、退款、取消等。这应该与帐户(在您的情况下为交易)本身分开,因为付款交易和信用交易之间没有一对一的对应关系。在您当前的计划中,您已经部分支付了没有记录日期的交易——这是系统中的一个巨大差距,它将回来咬你。如果客户每天支付 10 美元换取 200 美元的信用额度,为期 20 天怎么办?其中 19 笔付款显示没有付款日期。

那么,我建议您创建一个存储过程 (SP),首先将付款应用于总计,然后再创建另一个以按需方式将付款“重写”到交易中的存储过程。想想如果信用卡公司“重新评估”您的帐户,他们必须做什么。也许他们对不正确的信息采取了行动,并在某个日期提高了您的利率。 (这实际上发生在我身上。我向他们证明,他们回应的收款活动不是我的错——在我向他们展示他们的一名员工错误地更改了我的邮寄地址后,它已被原公司撤回,并且我从来没有收到可以支付的账单。所以他们必须能够追溯地重新运行我账户上的所有购买/借记/利率计算,以便根据正确的利率重新计算原始更改日期之后的所有内容.) 想一想,你会发现只要你正确设计你的系统,这种方式是很有可能的。为您的 SP 指定一个日期范围或允许其工作的事务集,然后“重写”历史记录,就好像旧的历史记录从未存在过一样。

但是,您实际上并不想破坏历史记录,因此,在某个时间点,您对一段时间内客户账户余额的最佳了解是与您当前对该时间段的帐户余额的最佳了解不同 - 两者都是真实数据,需要保留。

假设您发现您的系统偶尔会错误地将信用交易加倍。当您修复客户数据时,您需要能够看到他们遇到问题的事实,即使他们现在没有问题。这是通过使用额外的日期列EffectiveDateExpirationDate 来完成的——或者任何你想称呼它们的东西。然后,这些需要成为聚集索引的一部分,并在每个查询中使用以始终获取当前值。我强烈建议使用9999-12-31 而不是NULL 作为当前行的ExpirationDate 值——这将对查询当前数据时的性能产生巨大的积极影响。我还建议将ExpirationDate 作为聚集索引中的第一列(或者至少在EffectiveDate 列之前),因为历史总是可能比未来有更多的记录,所以它比@ 更具选择性987654343@ 是第一个(想一想:所有过去的知识都会有EffectiveDate =&lt; GetDate(),但只有当前或未来的数据会有ExpirationDate &gt; GetDate())。把重点放在家里:你不要删除。您可以通过将一列设置为知识过时的日期来使旧行过期,然后插入表示新知识的新行,其中一列显示您学习此信息的日期,并且在其他日期列。

最后是几个单点:

  • CreditPaid 列应为NOT NULL,默认为0。我不得不投入一堆Coalesces 来处理NULLs。
  • 您需要以某种方式处理多付款项。通过阻止它们,或者通过存储多付的部分值并在以后应用它。您可以将 UPDATE 语句的结果输​​出到表中,然后从中选择 Sum 并使 SP 返回任何未使用的付款值。有很多方法可以处理这个问题。如果您按照我的建议构建“重新评级”SP,那么这不会有太大问题,因为您可以在收到新交易后重新运行它(然后立即(重新)应用任何开放期间的所有付款)。

在这一点上我不能说太多,但我希望这些想法对你有所帮助。您的设计是一个良好的开端,但需要做一些工作才能使其达到作为企业质量系统良好运行的地步。

更新

我更正了 2008 版本中的一个小故障(将外部查询中的条件添加到子查询中)。

这是我的最后一次编辑(全部:请不要再次编辑此答案,否则它将被转换为社区 wiki)。

如果您确实采用了一种方案,其中行被标记为它们被认为是真实的日期(EffectiveDateExpirationDate),您可以通过创建内联表函数来简化系统中的编码选择仅来自表 WHERE EffectiveDate &lt;= GetDate() AND GetDate() &lt; ExpirationDate 的活动行。请注意您正在使用的比较运算符(例如,&lt;=&lt;),并在开始时使用包含不包含的日期范围结束。如果您不确定这意味着什么,请在继续之前查看这些术语并理解它们。您希望将来能够更改日期数据类型的分辨率,而不会中断任何查询。如果您使用包含的结束日期,这是不可能的。网上有很多帖子在讨论如何在 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!
);

不要将此与多语句表值返回函数混淆。那将不会表现良好。这里的这种函数类型将表现良好,因为它可以内联到查询中,基本上引擎采用函数正在执行的逻辑意图,并完全处理函数调用。使用任何其他类型的函数都可以解决这个问题,并且随着您的桌子变大,您的表现将进入底池。

【讨论】:

  • 不真实的答案,在小提琴上效果很好,但我需要 2008 版本。奇怪的是有些人有多聪明!
  • 我会尽量记住回家后给你一个更新的版本!
  • 您好 ErikE,我尝试替换它并运行它,但出现错误。我相信我没有更换正确的积木。你能把代码贴在这里吗?我是SP的初学者。谢谢!
  • 很抱歉——有几个语法错误。只是缺少一个括号和一个错误的别名。对于那个很抱歉。我已经更新了一个工作版本。但是还有一些更重要的问题我还没有机会讨论。我会尽可能地做到这一点!
  • 天啊,你真是太棒了!这么聪明!我非常佩服像你这样的人。我认为没有什么问题是你解决不了的。正如您所说,最好的解决方案是分别处理每笔交易。但是当人们没有足够的知识或者不太关心将数字插入系统时,我认为这将是他们处理优秀学分的最佳解决方案。当然,应该存在一个日志来跟踪每个事务编辑。当然,非常欢迎任何进一步的建议!再次感谢!!!
猜你喜欢
  • 2021-10-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-06-22
  • 1970-01-01
相关资源
最近更新 更多