SQL累积值

时间:2012-07-23 22:46:50

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

我正在编写一个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一起回答它们的问题,所以我想把它放在那里看看......

非常感谢你的帮助。

3 个答案:

答案 0 :(得分:5)

将您的存储桶表放入临时表,然后有一个名为running total的额外列。这将有运行总计直到此加入,然后交叉加入付款和tempbucket表并指定付款< =运行totalin tempbucket表的条件。这应该可以解决您的问题。 我已经使用Mafu Josh的DDL来创建以下查询,因此感谢他 。我希望OP应该总是发布这些东西,以使其他人的生活更轻松。

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

   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 :(得分:3)

我没有使用游标的尝试:

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

答案 2 :(得分:2)

这是针对您的递归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 n)

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

您可以尝试此查询at SQL Fiddle

相关问题