舍入订单量达到限制总额

时间:2012-12-01 12:43:46

标签: sql sql-server

昨天遇到了一个有趣的小问题。这是一个关于算术的问题,与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

2 个答案:

答案 0 :(得分:2)

这是一个分区问题,你试图让结果为整数(或等价,整数的一些固定倍数)。策略是将所有内容计算为整数,找到余数,然后在项目中分配剩余部分。

以下是计算概述:

  1. 将新卷计算为订单中每个条目的浮点数
  2. 将整数部分与此卷中的分数分开。
  3. 计算最大体积减去整数比例的总和。差异是你必须弥补的数额。
  4. 枚举从最大到最小的分数。
  5. 计算最终金额为整数金额加1或0.当枚举小于或等于要弥补的金额时,请使用1。 0为其他人。
  6. 以下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)中的枚举乘以因子。

    而且,是的,我已经在您的测试数据上对此进行了测试。它不仅在逻辑上有效,而且在实践中。

答案 1 :(得分:1)

<强>方法

一种可能的方法是

  1. 获取“最全圆”的建议音量(在我的示例中,圆形和非圆形缩放音量之间的最小差异);
  2. 如果总计算量不等于20且比率不是1,则将该体积校正为1.
  3. 更新:由于这只是一个例子,实施中存在一些问题

    1. RRRRS应加入由orderId分区并按SDiffMax分类的建议卷的等级。 SDiff加入适合制作; FIXED
    2. 更正后应确保没有量0(最好这样做可能是从计算中排除SRVol = 1SDif < 0的订单; < / del> FIXED
    3. 最好使用相对差异而不是算术,如计算SDiffSVol的比率和进一步使用该值代替SDiff会使结果更加准确。 FIXED
    4. 如果您需要任何帮助,请与我们联系。

      <强>解决方案

      ; 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