【问题标题】:SQL accumulating valueSQL累计值
【发布时间】:2012-07-23 22:46:49
【问题描述】:

我正在编写一个 SQLServer 2008 存储过程,它采用一个付款表并尝试根据相关表中描述的一组规则(基本上是一组存储桶)分配这些付款。然而,目前让我头疼的是分配(将支付值放入存储桶)。

假设表 Payments 包含要支付的值,而表 Buckets 是关于应该在每个桶中放入多少,直到要支付的初始值用完(达到 0)。

使用下表作为示例(实际用例有点做作,因为有一些复杂的标准可以选择适合每次付款的存储桶):

PaymentId     Value                 BucketId       MaxAmount
--------------------------          --------------------------------
1             16.5                  1              5.5
2             7.0                   2              10
                                    3              8.3

对于付款 1:5.5 单位(该桶的最大值)应放入桶 1,10 单位应放入桶 2(11.5 是桶 1 的余数,但仍超过桶 2 的最大值)和 1 单位(16.5 - 5.5 - 10) 应放入存储桶 3。重复所有付款。

这很容易在任何命令式语言中实现,甚至可能在带有 for/while 循环的 SQL 中实现,但我试图意识到是否有更好的方法(即使它是不可移植的并且特定于 SQLServer 2005+)。

我进行了一些研究(主要是递归 CTE),但没有想到真正出色的东西。我敢肯定有很多 StackOverflowers 使用 SQL-fu 可以从他们的脑海中回答这个问题,所以我想把它放在那里看看......

非常感谢您的帮助。

【问题讨论】:

  • 您的存储桶表中是否有任何表示订单的内容?否则你的结果几乎是不确定的。另外,您的数据类型是什么,您是否需要担心随机舍入问题?
  • @X-Zero - 桶表中没有顺序。只要数量不超过每个桶的最大值,并且将总值分配给一个或多个桶,就可以将任何付款分配给桶。四舍五入问题绝对重要。数据类型为十进制(18,6),但舍入到小数点后第三位。
  • 您可能想阅读 SQL CLR 并查看它是否比纯 Transact-SQL 解决方案更容易访问。
  • 好的,如果你有一个 DECIMAL 类型,你应该没问题(尤其是你没有乘法/除法)。如果我在我的 DB2 版本中这样做,我需要使用递归 CTE;但是,如果 2008 年有一些额外的 OLAP 功能(我认为是LAG/LEAD),您应该能够在没有它们的情况下做到这一点。
  • 你可能会发现一些有用的想法here

标签: sql sql-server sql-server-2008


【解决方案1】:

将您的存储桶表放入一个临时表中,然后有一个称为运行总计的额外列。这将具有此连接之前的运行总计,然后交叉连接支付和临时存储桶表,并指定支付 感谢他,我使用 Mafu Josh 的 DDL 创建了以下查询。我希望 OP 应该始终发布这些内容,以使其他人的生活更轻松。

看起来buckte表很小。但是如果它非常大。然后生成运行总计使用变量更新。这比下面的方法更有效。对我来说这听起来表或多或少是静态表,因此您可以将运行总计作为表本身的一部分。

   DECLARE @Buckets TABLE ( 
    BucketId INT, 
    MaxAmount DECIMAL(18,6) 
) 

INSERT INTO @Buckets VALUES (1, 5.5) 
INSERT INTO @Buckets VALUES (2, 10) 
INSERT INTO @Buckets VALUES (3, 8.3) 

DECLARE @Payments TABLE ( 
    PaymentId INT, 
    Value DECIMAL(18,6) 
) 

INSERT INTO @Payments VALUES (1,16.5) 
INSERT INTO @Payments VALUES (2,7.0) 
INSERT INTO @Payments VALUES (3,23.8) 

DECLARE @tempBuckets TABLE ( 
    BucketId INT, 
    MaxAmount DECIMAL(18,6) ,
    currentruntotal decimal(18,6)
) 
insert into @tempBuckets select bucketid,maxamount ,(select SUM(maxamount) from @Buckets bin where b.bucketid >=bin.bucketid)
--,isnull((select SUM(maxamount) from @Buckets bin where b.bucketid > bin.bucketid),0)
from @Buckets b

select * from @tempBuckets
select PaymentId,Value,BucketId,
case when p.Value >= tb.currentruntotal then tb.MaxAmount else p.Value - tb.currentruntotal + tb.MaxAmount end as bucketamount
from @Payments p inner join @tempBuckets tb on  (p.Value >= tb.currentruntotal or p.Value between tb.currentruntotal - tb.MaxAmount and tb.currentruntotal )
order by PaymentId
go

【讨论】:

  • +1 表示不使用游标并允许 SQL Server 实际优化不同的步骤。
  • +1 用于清理 Mafu Josh 的答案,但由于该查询实际上是他的,我将接受他的答案(不过,添加一条引用您的答案的评论)。
  • 没有问题...但是我建议如果存储桶表很小并且没有太大变化,请保留运行总计列,然后查询将只是在 2 个表之间连接。
【解决方案2】:

我不使用游标的尝试:

DECLARE @Buckets TABLE (
    BucketId INT,
    MaxAmount DECIMAL(18,6)
)

INSERT INTO @Buckets VALUES (1, 5.5)
INSERT INTO @Buckets VALUES (2, 10)
INSERT INTO @Buckets VALUES (3, 8.3)

DECLARE @Payments TABLE (
    PaymentId INT,
    Value DECIMAL(18,6)
)

INSERT INTO @Payments VALUES (1,16.5)
INSERT INTO @Payments VALUES (2,7.0)

SELECT
  P1.PaymentId
, P1.Value as TotalPayment
, B4.BucketId
, B4.MaxAmount
, CASE WHEN B3.BucketId = B4.BucketId THEN P1.Value - MaxAmountRunningTotalOfPreviousBuckets ELSE B4.MaxAmount END AS BucketPaymentAmount
FROM @Payments P1
INNER JOIN (
    SELECT
      B2.BucketId
    , B2.MaxAmount as BucketMaxAmount
    , SUM(B1.MaxAmount) - B2.MaxAmount as MaxAmountRunningTotalOfPreviousBuckets
    FROM @Buckets B1
    INNER JOIN @Buckets B2
      ON B1.BucketId <= B2.BucketId
    GROUP BY B2.BucketId, B2.MaxAmount
  ) AS B3
  ON P1.Value > B3.MaxAmountRunningTotalOfPreviousBuckets AND P1.Value <= (B3.MaxAmountRunningTotalOfPreviousBuckets + BucketMaxAmount)
INNER JOIN @Buckets B4
  ON B4.BucketId <= B3.BucketId
ORDER BY P1.PaymentId, B3.BucketId

【讨论】:

  • +1 表示案例而不使用慢速光标。 3 个答案,一个仍然停留在 DBase 时代......你做得很好。我希望我有一个 +2,你甚至不使用临时表。
  • 太棒了!这正是我拍摄的目的。不过,很抱歉最初没有发布 DDL。桶表通常很小(它是一个更大的表的一个小子集)并且使用 table 变量实际上符合我的设置。我仍然需要检查它的性能,但现在,非常感谢!
  • @Gulli Meel 的答案与您的基本相同,尽管阅读起来更简单一些(性能方面,我必须检查一下)。尽管如此,还是接受您的回答,因为这是第一个可以解决问题的答案。
【解决方案3】:

这是适合您的递归 CTE 方法:

WITH BucketsRanked AS (
  SELECT *, rnk = ROW_NUMBER() OVER (ORDER BY BucketId) FROM Buckets
)
, PaymentsRanked AS (
  SELECT *, rnk = ROW_NUMBER() OVER (ORDER BY PaymentId) FROM Payments
)
, PaymentsDistributed AS (
  SELECT
    b.BucketId,
    p.PaymentId,
    Bucket        = b.MaxAmount,
    Payment       = p.Value,
    BucketRnk     = b.rnk,
    PaymentRnk    = p.rnk,
    BucketPayment = CASE
                      WHEN p.Value > b.MaxAmount
                      THEN b.MaxAmount
                      ELSE p.Value
                    END,
    CarryOver     = p.Value - b.MaxAmount
  FROM
    BucketsRanked b,
    PaymentsRanked p
  WHERE b.rnk = 1 AND p.rnk = 1
  UNION ALL
  SELECT
    b.BucketId,
    p.PaymentId,
    Bucket        = b.MaxAmount,
    Payment       = p.Value,
    BucketRnk     = b.rnk,
    PaymentRnk    = p.rnk,
    BucketPayment = CASE
                      WHEN x.PaymentValue > x.BucketValue
                      THEN x.BucketValue
                      ELSE x.PaymentValue
                    END,
    CarryOver     = x.PaymentValue - x.BucketValue
  FROM PaymentsDistributed d
    INNER JOIN BucketsRanked  b
      ON b.rnk = d.BucketRnk  + CASE SIGN(CarryOver) WHEN -1 THEN 0 ELSE 1 END
    INNER JOIN PaymentsRanked p
      ON p.rnk = d.PaymentRnk + CASE SIGN(CarryOver) WHEN +1 THEN 0 ELSE 1 END
    CROSS APPLY (
      SELECT
        CONVERT(
          decimal(18,6),
          CASE SIGN(CarryOver) WHEN -1 THEN -d.CarryOver ELSE b.MaxAmount END
        ),
        CONVERT(
          decimal(18,6),
          CASE SIGN(CarryOver) WHEN +1 THEN +d.CarryOver ELSE p.Value     END
        )
    ) x (BucketValue, PaymentValue)
)
SELECT
  BucketId,
  PaymentId,
  Bucket,
  Payment,
  BucketPayment
FROM PaymentsDistributed
;

基本上,此查询获取第一笔付款和第一个桶,找出哪个少,并产生第一个 BucketPayment 项目。支付值和桶容量的差值会被记住,以备下次迭代使用。

在下一次迭代中,差额(取决于其符号)被用作存储桶金额或付款。此外,根据差异的符号,查询从Payments 表中获取下一笔付款,或者从Buckets 中获取下一个桶。 (但如果差值为 0,则查询实际上检索下一笔付款和下一个桶。)然后将与第一次迭代相同的逻辑应用于新的桶金额和付款值。

迭代一直持续到没有更多存储桶或没有更多付款为止。或者直到达到默认的 MAXRECURSION 值 100,在这种情况下,您可能需要追加

OPTION (MAXRECURSION <i>n</i>)

到上述查询,其中 n 必须是最大为 32767 的非负整数,指定最大迭代次数(递归)。 (请注意,0 实际上代表 unlimited。)

你可以试试这个查询at SQL Fiddle

【讨论】:

  • 我将不得不更详细地研究您的解决方案,但它似乎试图将所有付款的总和分配到可用的存储桶空间中。但是,需要为每次付款填充存储桶(对于上面的付款 2,存储桶存储仍然是所有存储桶的总和)。抱歉,如果这还不够清楚。不过,很好地解决了这个问题。让我们看看我是否可以根据我的要求调整它(这样我就可以将它的性能与@Gulli Meel 的解决方案进行比较)。
  • 您说得对,这确实是我理解您的问题的方式(将所有付款的总和分配到所有存储桶中)。抱歉,我无法提供更多帮助。至于我,我很高兴解决我读到你的问题,所以无论如何感谢你的问题。 :)
  • 很高兴您没有发现这是在浪费时间。 ;-)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-27
  • 2017-08-06
  • 1970-01-01
  • 2018-07-08
  • 1970-01-01
相关资源
最近更新 更多