我正在编写一个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一起回答它们的问题,所以我想把它放在那里看看......
非常感谢你的帮助。
答案 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。