2

昨日、職場で興味深い小さな問題に遭遇しました。これは、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
4

2 に答える 2

2

これは、結果を整数 (または、整数の固定倍数) にしようとするパーティショニングの問題です。戦略は、すべてを整数として計算し、剰余を見つけて、剰余を項目間で配分することです。

計算の概要は次のとおりです。

  1. 注文の各エントリの新しいボリュームを浮動小数点数として計算します
  2. このボリュームの分数から整数部分を分離します。
  3. 最大ボリュームから整数比率の合計を差し引いて計算します。差額は、補わなければならない金額です。
  4. 分数を最大から最小まで列挙します。
  5. 最終的な金額は、整数の金額に 1 または 0 を加えたものとして計算します。列挙が補う金額以下の場合は 1 を使用します。その他は 0 です。

次の SQL はこれを行います。

select t.*, row_number() over (partition by orderid order by remainder desc) as seqnum,
       (case when row_number() over (partition by orderid order by remainder desc) <= LeftOver
             then AppliedVolInt + 1
             else AppliedVolInt
        end) as NewVolume
from (select t.*, floor(AppliedVol) as AppliedVolInt,
             (AppliedVol - 1.000*floor(AppliedVol)) as Remainder,
             maxvol*1.0 - sum(floor(AppliedVol)) over (partition by orderid) as LeftOver
      from (select oli.orderid, oli.ProposedVolume, omax.MaxVol,
                   sum(proposedVolume) over (partition by oli.orderid) as sumProposed,
                   omax.maxvol * (oli.ProposedVolume / sum(proposedVolume) over (partition by oli.orderid)) as AppliedVol
            from #OLI oli join
                 #OMax omax
                 on oli.orderid = omax.orderid
           ) t
     ) t

整数がない場合、算術演算は少し複雑になります ((4) から (5) までの列挙を使用するため)。私の推奨事項は、すべての数値を定数で乗算し、それを整数問題または乗算に変換することです。 (4)の列挙を係数で。

そして、はい、私はあなたのテスト データでこれをテストしました。論理的に機能するだけでなく、実際に機能します。

于 2012-12-01T17:33:35.157 に答える
1

アプローチ

考えられるアプローチの 1 つは、

  1. 「最も丸められた」提案されたボリュームを取得します(私の例では、丸められたスケーリングされたボリュームと丸められていないスケーリングされたボリュームの最小の違い)。
  2. 計算された合計ボリュームが 20 に等しくなく、比率が 1 でない場合は、そのボリュームを 1 で修正します。

更新:これは単なる例であるため、実装にはいくつかの問題があります

  1. RRによって分割され、によってソートされRRSた提案されたボリュームのランクに参加する必要があります。結合は本番環境には適していません。orderIdSDiffMaxSDiff修繕
  2. 修正後のボリュームがないことを確認する必要があります (これを行う最善の方法は、おそらく注文を計算から0除外することです)。SRVol = 1SDif < 0修繕
  3. の比率を計算し、さらにその値を使用する代わりにその値を使用すると、結果がより正確になります。SDiffSVolSDiff修繕

これらについてサポートが必要な場合はお知らせください。

解決

; with OrderTotals as
    (
    select OrderId, sum(ProposedVolume) as TotalVolume
    from #OLI
    group by OrderId
    ),
 RawRounded as
 (
    select
        OLI.*, 
        Ratio.Ratio,
        Scaled.Vol as SVol,
        ScaledAndRounded.Vol as SRVol,
        (Scaled.Vol - ScaledAndRounded.Vol) / Scaled.Vol as SDiff,
        row_number() over (partition by OLI.OrderId order by (Scaled.Vol - ScaledAndRounded.Vol) / Scaled.Vol desc) as SDiffRank,
        TotalVolume
    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
),
RawRoundedSum AS
(
    select 
        OrderId,
        MIN(SDiff) AS SDiffMin,
        MIN(SDiffRank) AS SDiffRankMin,
        SUM(SRVol) AS SRVolSum
    from RawRounded
    where 
        Ratio <> 1
    group by OrderId
    having SUM(SRVol) <> 20
)
select 
    RR.OrderId,
    RR.ProposedVolume,
    case 
        when RRS.SDiffMin is null then RR.SRVol 
        else round(RRS.SDiffMin / @nOrderRoundAmt + case when RRS.SDiffMin < 0 then - 0.5 else 0.5 end, 0) * @nOrderRoundAmt + RR.SRVol
    end SRVolFinal
from 
    RawRounded RR
    left join RawRoundedSum RRS 
        on RR.OrderId = RRS.OrderId and RR.SDiffRank = RRS.SDiffRankMin

結果

OrderId     ProposedVolume                          SRVolFinal
----------- --------------------------------------- ---------------------------------------
1           11.600                                  12.000000
1           5.400                                   5.000000
2           9.744                                   5.000000
2           16.254                                  10.000000
2           9.556                                   5.000000
3           7.100                                   6.000000
3           7.230                                   7.000000
3           7.450                                   7.000000
于 2012-12-01T13:42:10.180 に答える