昨日、職場で興味深い小さな問題に遭遇しました。これは、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 より大きい注文について、注文明細の新しい合計が MaxVol と等しくなるように ProposedVolumes を縮小する SQL ステートメントを記述できますか? (理由: ここでのビジネス ケースは、注文 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 つの問題を示しています。注文 2 の合計は 19 で、注文 3 の合計は 21 です。常に切り捨てを行うことで、注文 3 が 20 を超えないようにすることができますが、注文の合計が発生する場合があります。 18時に出ます。
それで、それは単一のステートメントで可能ですか?これまでの私の最善の解決策は、上記のロジックを (切り捨てを使用して) 適用し、カーソル内で処理の 2 番目のステップを適用して、合計が 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
解決
ゴードンの答えに基づいて私が作成した最終的な解決策に誰かが興味を持っている場合は、ここにあります. これは少し冗長で、実際に必要な数よりもはるかに多くの列を返しますが、デバッグや理解に役立ちます。丸めの度合いを 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