3

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

非常感谢你的帮助。

4

3 回答 3

6

将您的存储桶表放入临时表中,然后有一个称为运行总计的额外列。这将具有此连接之前的运行总计,然后交叉连接支付和临时存储桶表,并指定支付 <= 运行总计在临时存储桶表中的条件。这应该可以解决您的问题。感谢他,我使用 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
于 2012-07-24T04:30:48.543 回答
4

我不使用游标的尝试:

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
于 2012-07-23T23:53:24.010 回答
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这实际上代表无限。)

您可以在 SQL Fiddle尝试此查询。

于 2012-07-24T07:20:02.373 回答