昨天在工作中遇到了一个有趣的小问题。这是一个关于算术和 SQL 的问题。假设您有一堆订单,并且订单可以拥有的数量有限制(在本例中为 20 个):
if object_id('tempdb..#OMAX') is not null drop table #OMAX
create table #OMAX
(
OrderId int primary key,
MaxVol decimal(15,3)
)
insert into #OMAX(OrderId, MaxVol) values (1, 20), (2, 20), (3, 20)
以下是您的订单行项目及其当前、建议的数量:
if object_id('tempdb..#OLI') is not null drop table #OLI
create table #OLI
(
OrderId int,
ProposedVolume decimal(15,3)
)
insert into #OLI(OrderId, ProposedVolume)
values
(1, 11.6),
(1, 5.4),
(2, 9.744),
(2, 16.254),
(2, 9.556),
(3, 7.1),
(3, 7.23),
(3, 7.45)
您还希望将结果四舍五入到特定的准确度,假设目前为 1.0(整数):
declare @nOrderRoundAmt decimal(15,3) = 1.0;
问题:对于当前总数大于 OMAX.MaxVol 的订单,您能否编写一条 SQL 语句来缩小 ProposedVolumes 以使订单行的新总数等于 MaxVol?它必须相等,不少于(原因:这里的商业案例是订单 2 的总建议量为 35.554,但我们说允许的最大值为 20,所以当我们减少订单时,我们需要减少它到 20,不低于,因为那是不合理的)。
复杂性:一个订单可以有 1..N 个订单项。不要认为这是一组详尽的测试数据,我怀疑还有其他棘手的情况。
在这种情况下,订单 1 应该保持不变,除了四舍五入,订单 2 和 3 应该减少并四舍五入到 20。
这是我迄今为止的最大努力:
; with OrderTotals as
(
select OrderId, sum(ProposedVolume) as TotalVolume
from #OLI
group by OrderId
)
select
OLI.*,
Ratio.Ratio,
Scaled.Vol as SVol,
ScaledAndRounded.Vol as SRVol
from
#OLI OLI
join OrderTotals OT on OLI.OrderId = OT.OrderId
join #OMAX OMAX on OLI.OrderId = OMAX.OrderId
cross apply
(
-- Don't reduce orders that are already below the max.
select
case when OMAX.MaxVol / OT.TotalVolume > 1 then 1
else OMAX.MaxVol / OT.TotalVolume
end as Ratio
) Ratio
cross apply (select OLI.ProposedVolume * Ratio.Ratio as Vol) Scaled
-- Rounds to nearest.
cross apply (select round(Scaled.Vol / @nOrderRoundAmt, 0) * @nOrderRoundAmt as Vol) ScaledAndRounded
-- Rounds down.
-- cast(Scaled.Vol / @nOrderRoundAmt as bigint) * @nOrderRoundAmt as ScaledAndRoundedDown,
这说明了两个问题:订单 2 的总数为 19,订单 3 的总数为 21。您可以通过始终向下舍入来阻止订单 3 超过 20,但是您可以得到订单总数出现的情况18点出门。
那么是否有可能在单个语句中?到目前为止,我最好的解决方案是应用上述逻辑(使用向下舍入),然后在光标中应用第二步处理以添加差异,直到我们回到总数 20。
你能证明你的解决方案适用于所有情况吗?
以下用于生成随机测试订单的代码可能很有用:
declare @OrderId int = 0, @NumLineItems int;
while @OrderId < 1000 begin
set @NumLineItems = cast(rand() * 5 as int) + 1
insert into #OLI(OrderId, ProposedVolume)
select top (@NumLineItems) @OrderId, rand(cast(newId() as varbinary)) * 15
from sys.objects
set @OrderId = @OrderId + 1
end
解决方案
如果有人对我根据 Gordon 的回答做出的最终解决方案感兴趣,就在这里。它有点冗长,返回的列比实际需要的多得多,但这有助于调试/理解。尝试将舍入度设置为 0.1 或 0.01。如果任何行项目的建议数量为 0,则该解决方案很容易出现除零错误,但它们很容易事先被过滤掉。它还可以生成一些四舍五入到零的行项目,需要事后排除。
declare @nOrderRoundAmt decimal(15,3) = 0.1; -- Degree of rounding required.
if object_id('tempdb..#Results') is not null drop table #Results
select
T.*,
row_number() over (partition by OrderId order by Remainder desc) as seqnum,
case
when NeedsAdjustment = 0 then ProposedVolumeRounded
else
(case when row_number() over (partition by OrderId order by Remainder desc) <= LeftOver
then AppliedVolInt + 1
else AppliedVolInt
end)
end * @nOrderRoundAmt as NewVolume
--into #Results
from
(
select
T.*,
floor(T.AppliedVol) as AppliedVolInt,
(T.AppliedVol - 1.000 * floor(T.AppliedVol)) as Remainder,
T.MaxVol * 1.0 - sum(floor(T.AppliedVol)) over (partition by T.OrderId) as LeftOver
from
(
select
OLI.OrderId,
OMAX.MaxVol as OrigMaxVol,
MaxVol.Vol as MaxVol,
OLI.ProposedVolume as OrigProposedVolume,
ProposedVolume.Vol as ProposedVolume,
ProposedVolumeRounded.Vol as ProposedVolumeRounded,
sum(ProposedVolume.Vol) over (partition by OLI.OrderId) as SumProposedVolume,
sum(ProposedVolumeRounded.Vol) over (partition by OLI.OrderId) as SumProposedVolumeRounded, -- Round, THEN sum.
case
-- when SumProposedVolumeRounded > MaxVol, i.e. the sum of the rounded line items would be
-- greater than the order limit, then scale, else take the original.
when sum(ProposedVolumeRounded.Vol) over (partition by OLI.OrderId) > MaxVol.Vol then 1
else 0
end as NeedsAdjustment,
case
-- when SumProposedVolumeRounded > MaxVol, i.e. the sum of the rounded line items would be
-- greater than the order limit, then scale, else take the original.
when sum(ProposedVolumeRounded.Vol) over (partition by OLI.OrderId) > MaxVol.Vol then MaxVol.Vol * (ProposedVolume.Vol / sum(ProposedVolume.Vol) over (partition by OLI.OrderId))
else ProposedVolume.Vol
end as AppliedVol
from
##OLI OLI
join ##OMax OMAX on OLI.OrderId = OMAX.OrderId
cross apply (select OLI.ProposedVolume / @nOrderRoundAmt as Vol) ProposedVolume
cross apply (select OMAX.MaxVol / @nOrderRoundAmt as Vol) MaxVol
cross apply (select round(ProposedVolume.Vol, 0) as Vol) ProposedVolumeRounded
) T
) T