查询挑战:根据度量而非行数创建均匀大小的存储桶


12

我将以尽可能均匀地向固定数量的卡车装载订单来描述问题。

输入:

@TruckCount - the number of empty trucks to fill

一套:

OrderId, 
OrderDetailId, 
OrderDetailSize, 
TruckId (initially null)

Orders由一个或多个组成OrderDetails

这里的挑战是TruckId为每个记录分配一个。

一个订单不能跨卡车分割。

卡车应尽可能均匀地*装载,以表示sum(OrderDetailSize)

*平均:最小载重卡车和最大载重卡车之间可实现的最小增量。根据此定义,1,2,3比1,1,4更均匀地分布。如果有帮助,请假装您是统计算法,甚至可以创建高度直方图。

没有考虑最大卡车负载。这些是魔术弹力卡车。但是卡车的数量是固定的。

有一个明显的解决方案是迭代-循环分配订单。

但是可以按照基于集合的逻辑来完成吗?

我的主要兴趣是针对SQL Server 2014或更高版本。但是针对其他平台的基于集合的解决方案也可能很有趣。

这感觉就像Itzik Ben-Gan的领土:)

我的实际应用程序将处理工作负载分配到多个存储桶中,以匹配逻辑CPU的数量。因此,每个存储桶没有最大大小。统计信息具体更新。我只是认为将问题抽象到卡车上作为解决挑战的一种方式会更有趣。

CREATE TABLE #OrderDetail (
OrderId int NOT NULL,
OrderDetailId int NOT NULL PRIMARY KEY,
OrderDetailSize tinyint NOT NULL,
TruckId tinyint NULL)

-- Sample Data

INSERT #OrderDetail (OrderId, OrderDetailId, OrderDetailSize)
VALUES
(1  ,100    ,75 ),
(2  ,101    ,5  ),
(2  ,102    ,5  ),
(2  ,103    ,5  ),
(2  ,104    ,5  ),
(2  ,105    ,5  ),
(3  ,106    ,100),
(4  ,107    ,1  ),
(5  ,108    ,11 ),
(6  ,109    ,21 ),
(7  ,110    ,49 ),
(8  ,111    ,25 ),
(8  ,112    ,25 ),
(9  ,113    ,40 ),
(10 ,114    ,49 ),
(11 ,115    ,10 ),
(11 ,116    ,10 ),
(12 ,117    ,15 ),
(13 ,118    ,18 ),
(14 ,119    ,26 )
--> YOUR SOLUTION HERE

-- After assigning Trucks, Measure delta between most and least loaded trucks.
-- Zero is perfect score, however the challenge is a set based solution that will scale, and produce good results, rather
-- than iterative solution that will produce perfect results by exploring every possibility.

SELECT max(TruckOrderDetailSize) - MIN(TruckOrderDetailSize) AS TruckMinMaxDelta
FROM 
(SELECT SUM(OrderDetailSize) AS TruckOrderDetailSize FROM #OrderDetail GROUP BY TruckId) AS Truck


DROP TABLE #OrderDetail

7
这似乎是经典的装箱问题
Dan Guzman '18

1

对于给定的OrderId,所有OrderDetailSize值是否相等?或者这仅仅是样本数据中的共同点?
youcantryreachingme 18/09/17

@youcantryreachingme啊,好地方……不,这只是示例数据中的巧合。
保罗·福尔摩斯

Answers:


5

我的第一个念头是

select
    <best solution>
from
    <all possible combinations>

问题中定义了“最佳解决方案”部分-载重量最大和载重量最小的卡车之间的最小差异。另一部分-所有组合-使我停下来思考。

考虑一下我们有三个订单A,B和C以及三辆卡车的情况。可能性是

Truck 1 Truck 2 Truck 3
------- ------- -------
A       B       C
A       C       B
B       A       C
B       C       A
C       A       B
C       B       A
AB      C       -
AB      -       C
C       AB      -
-       AB      C
C       -       AB
-       C       AB
AC      B       -
AC      -       B
B       AC      -
-       AC      B
B       -       AC
-       B       AC
BC      A       -
BC      -       A
A       BC      -
-       BC      A
A       -       BC
-       A       BC
ABC     -       -
-       ABC     -
-       -       ABC

Table A: all permutations.

其中许多是对称的。例如,前六行仅在每个订单下达哪个卡车上有所不同。由于卡车是可替代的,因此这些安排器将产生相同的结果。我现在暂时不理会。

存在用于产生排列和组合的已知查询。但是,这些将在单个存储桶中产生安排。对于这个问题,我需要跨多个存储桶进行安排。

查看标准“所有组合”查询的输出

;with Numbers as
(
    select n = 1
    union
    select 2
    union
    select 3
)
select
    a.n,
    b.n,
    c.n
from Numbers as a
cross join Numbers as b
cross join Numbers as c
order by 1, 2, 3;


  n   n   n
--- --- ---
  1   1   1
  1   1   2
  1   1   3
  1   2   1
 <snip>
  3   2   3
  3   3   1
  3   3   2
  3   3   3

Table B: cross join of three values.

我注意到结果形成了与表A相同的模式。通过认知上的飞跃,将每视为订单1,将表示哪个卡车将持有该订单,并将表示为卡车中的订单排列。然后查询变为

select
    Arrangement             = ROW_NUMBER() over(order by (select null)),
    First_order_goes_in     = a.TruckNumber,
    Second_order_goes_in    = b.TruckNumber,
    Third_order_goes_in     = c.TruckNumber
from Trucks a   -- aka Numbers in Table B
cross join Trucks b
cross join Trucks c

Arrangement First_order_goes_in Second_order_goes_in Third_order_goes_in
----------- ------------------- -------------------- -------------------
          1                   1                    1                   1
          2                   1                    1                   2
          3                   1                    1                   3
          4                   1                    2                   1
  <snip>

Query C: Orders in trucks.

将其扩展为涵盖示例数据中的十四个Order,并简化名称,我们得到以下信息:

;with Trucks as
(
    select * 
    from (values (1), (2), (3)) as T(TruckNumber)
)
select
    arrangement = ROW_NUMBER() over(order by (select null)),
    First       = a.TruckNumber,
    Second      = b.TruckNumber,
    Third       = c.TruckNumber,
    Fourth      = d.TruckNumber,
    Fifth       = e.TruckNumber,
    Sixth       = f.TruckNumber,
    Seventh     = g.TruckNumber,
    Eigth       = h.TruckNumber,
    Ninth       = i.TruckNumber,
    Tenth       = j.TruckNumber,
    Eleventh    = k.TruckNumber,
    Twelth      = l.TruckNumber,
    Thirteenth  = m.TruckNumber,
    Fourteenth  = n.TruckNumber
into #Arrangements
from Trucks a
cross join Trucks b
cross join Trucks c
cross join Trucks d
cross join Trucks e
cross join Trucks f
cross join Trucks g
cross join Trucks h
cross join Trucks i
cross join Trucks j
cross join Trucks k
cross join Trucks l
cross join Trucks m
cross join Trucks n;

Query D: Orders spread over trucks.

为了方便起见,我选择将中间结果保存在临时表中。

如果先删除数据,则后续步骤将更加容易。

select
    Arrangement,
    TruckNumber,
    ItemNumber  = case NewColumn
                    when 'First'        then 1
                    when 'Second'       then 2
                    when 'Third'        then 3
                    when 'Fourth'       then 4
                    when 'Fifth'        then 5
                    when 'Sixth'        then 6
                    when 'Seventh'      then 7
                    when 'Eigth'        then 8
                    when 'Ninth'        then 9
                    when 'Tenth'        then 10
                    when 'Eleventh'     then 11
                    when 'Twelth'       then 12
                    when 'Thirteenth'   then 13
                    when 'Fourteenth'   then 14
                    else -1
                end
into #FilledTrucks
from #Arrangements
unpivot
(
    TruckNumber
    for NewColumn IN 
    (
        First,
        Second,
        Third,
        Fourth,
        Fifth,
        Sixth,
        Seventh,
        Eigth,
        Ninth,
        Tenth,
        Eleventh,
        Twelth,
        Thirteenth,
        Fourteenth
    )
) as q;

Query E: Filled trucks, unpivoted.

可以通过加入“订单”表来引入权重。

select
    ft.arrangement,
    ft.TruckNumber,
    TruckWeight = sum(i.Size)
into #TruckWeights
from #FilledTrucks as ft
inner join #Order as i
    on i.OrderId = ft.ItemNumber
group by
    ft.arrangement,
    ft.TruckNumber;

Query F: truck weights

现在,可以通过找到在最大负载和最小负载的卡车之间具有最小差异的布置来回答该问题。

select
    Arrangement,
    LightestTruck   = MIN(TruckWeight),
    HeaviestTruck   = MAX(TruckWeight),
    Delta           = MAX(TruckWeight) - MIN(TruckWeight)
from #TruckWeights
group by
    arrangement
order by
    4 ASC;

Query G: most balanced arrangements

讨论区

这有很多问题。首先,它是蛮力算法。工作表中的行数与卡车和订单数成指数关系。#Arrangements中的行数为(卡车数)^(订单数)。这将无法很好地扩展。

其次,SQL查询中嵌入了订单数。解决此问题的唯一方法是使用动态SQL,它有其自身的问题。如果订单数量成千上万,则可能会出现生成的SQL太长的情况。

第三是安排上的冗余。这使中间表膨胀,极大地增加了运行时间。

第四,#Arrangements中的许多行将一辆或多辆卡车空着。这可能不是最佳配置。创建时很容易过滤掉这些行。我选择不这样做是为了使代码更简单,更集中。

从好的方面来说,如果您的企业开始装运填充的氦气球,这将减轻负重!

思想

如果有办法直接从卡车和订单清单中填充#FilledTrucks,我认为这些问题中最糟糕的是可以解决的。可悲的是,我的想象力跌落了那个障碍。我希望将来的一些贡献者能够提供我所无法企及的东西。




1您说订单的所有物品都必须在同一辆卡车上。这意味着分配的原子是Order,而不是OrderDetail。我是从您的测试数据生成这些的,因此:

select
    OrderId,
    Size = sum(OrderDetailSize)
into #Order
from #OrderDetail
group by OrderId;

但是,无论我们将问题标记为“订单”还是“ OrderDetail”都没有关系,解决方案保持不变。


4

查看您的实际需求(我假设是要在一组cpus上平衡您的工作负载)...

您是否有理由需要将进程预先分配给特定的存储桶/ CPU?[试图了解您的实际要求]

对于您的“统计信息更新”示例,您如何知道特定操作将花费多长时间?如果给定的操作遇到意外的延迟(例如,表/索引超出计划/过度碎片,长时间运行的用户txn阻止了“统计信息更新”操作)怎么办?


为了达到负载平衡的目的,我通常会生成任务列表(例如,要更新统计信息的表的列表),并将该列表放置在(临时/临时)表中。

可以根据您的要求修改表的结构,例如:

create table tasks
(id        int             -- auto-increment?

,target    varchar(1000)   -- 'schema.table' to have stats updated, or perhaps ...
,command   varchar(1000)   -- actual command to be run, eg, 'update stats schema.table ... <options>'

,priority  int             -- provide means of ordering operations, eg, maybe you know some tasks will run really long so you want to kick them off first
,thread    int             -- identifier for parent process?
,start     datetime        -- default to NULL
,end       datetime        -- default to NULL
)

接下来,我启动X个并发进程来执行实际的“统计信息更新”操作,每个进程执行以下操作:

  • tasks表上放置排他锁(确保没有一个以上的进程承担任何任务;应该是比较短暂的锁)
  • 查找“第一”行,其中start = NULL“第一”由您确定,例如,按“ priority?” 排序)
  • 更新行集 start = getdate(), thread = <process_number>
  • 提交更新(并释放排他锁)
  • 记下idtarget/command
  • target(或者,运行command)和完成时执行所需的操作...
  • 更新tasksend = getdate() where id = <id>
  • 重复以上操作,直到没有更多的任务要执行

通过以上设计,我现在获得了动态(主要是)平衡的操作。

笔记:

  • 我尝试提供某种优先级排序方法,以便我可以启动长期运行的任务。当几个进程正在处理运行时间较长的任务时,其他进程可以浏览运行时间较短的任务列表
  • 如果某个进程遇到了计划外的延迟(例如,长时间运行,阻塞了用户txn),则其他进程可以通过继续从 tasks
  • tasks表格的设计应提供其他好处,例如可以存档以供将来参考的运行时间历史记录,可以用于修改优先级,提供当前操作状态的运行时间历史记录等
  • 尽管“独占锁定” tasks似乎有点过分,但请记住,我们必须为可能在同一确切时间尝试获得新任务的2个(或更多)进程的潜在问题进行计划,因此我们需要保证一个任务仅分配给一个进程(是的,您可以使用组合的“ update / select”语句获得相同的结果-取决于RDBMS的SQL语言功能);获得新“任务”的步骤应该很快,即“排他锁”应该是短命的,实际上,进程将以tasks相当随机的方式命中,因此无论如何几乎不会阻塞

就个人而言,我发现此tasks表驱动的过程要容易实现和维护……与(通常)尝试预分配任务/过程映射的复杂过程(ymmv)相反。


显然,以您为例,您不能让卡车返回下一个订单的分销/仓库,因此您需要将订单预先分配给各种卡车(请记住,UPS / Fedex / etc也必须根据交货路线进行分配,以减少交货时间和气体使用量。

但是,在您的实际示例(“统计信息更新”)中,没有理由不能动态地完成任务/流程分配,从而确保更好地平衡工作负载的机会(跨CPU并减少总体运行时间) 。

注意:我通常会看到(IT)人员在实际运行任务之前试图预先分配任务(作为负载平衡的一种形式),在每种情况下,他/她最终不得不不断调整预分配过程以进行考虑到不断变化的任务问题(例如,表/索引中的碎片级别,并发用户活动等)。


首先,如果我们将“ order”视为表,并将“ orderdetail”视为表上的特定统计信息,则不进行拆分的原因是为了避免在竞争的存储桶之间进行锁定等待。Traceflag 7471旨在消除此问题,但是在我的测试中,我仍然遇到锁定问题。
保罗·福尔摩斯

我本来希望做一个非常轻量级的解决方案。将存储桶创建为单个多语句SQL块,然后使用自毁SQL代理作业“解雇”每个存储桶。即没有队列管理工作。但是,随后我发现我无法轻松地衡量每个统计信息的工作量-行数没有减少。鉴于行数并不是线性映射到一个表的IO量,或者实际上是静态的,是另一个表的IO,所以这并不奇怪。因此,是的,对于此应用程序,它确实可以通过添加您建议的一些主动队列管理来实现自我平衡。
保罗·福尔摩斯

对于您的第一条评论...是的,对于命令的粒度仍然存在(显而易见的)决定...和并发问题,例如:某些命令可以并行运行并受益于它们的组合磁盘读取等优点,但是我仍然发现(有点轻)动态队列管理比预先分配存储桶效率高:-)您有一组很好的答案/想法可以使用...应该不会太难提出一个提供以下解决方案的解决方案一些不错的负载平衡。
markp-fuso 18-09-18

1

根据需要创建并填充编号表。这仅是一次创建。

 create table tblnumber(number int not null)

    insert into tblnumber (number)
    select ROW_NUMBER()over(order by a.number) from master..spt_values a
    , master..spt_values b

    CREATE unique clustered index CI_num on tblnumber(number)

创建卡车表

CREATE TABLE #PaulWhiteTruck (
Truckid int NOT NULL)

insert into #PaulWhiteTruck
values(113),(203),(303)

declare @PaulTruckCount int
Select @PaulTruckCount= count(*) from #PaulWhiteTruck

CREATE TABLE #OrderDetail (
id int identity(1,1),
OrderId int NOT NULL,
OrderDetailId int NOT NULL PRIMARY KEY,
OrderDetailSize int NOT NULL,
TruckId int NULL
)

INSERT
#OrderDetail (OrderId, OrderDetailId, OrderDetailSize)
VALUES
(
1 ,100 ,75 ),(2 ,101 ,5 ),
(2 ,102 ,5 ),(2 ,103 ,5 ),
(2 ,104 ,5 ),(2 ,105 ,5 ),
(3 ,106 ,100),(4 ,107 ,1 ),
(5 ,108 ,11 ),(6 ,109 ,21 ),
(7 ,110 ,49 ),(8 ,111 ,25 ),
(8 ,112 ,25 ),(9 ,113 ,40 ),
(10 ,114 ,49 ),(11 ,115 ,10 ),
(11 ,116 ,10 ),(12 ,117 ,15 ),
(13 ,118 ,18 ),(14 ,119 ,26 )

我已经创建了一张OrderSummary桌子

create table #orderSummary(id int identity(1,1),OrderId int ,TruckOrderSize int
,bit_value AS
CONVERT
(
integer,
POWER(2, id - 1)
)
PERSISTED UNIQUE CLUSTERED)
insert into #orderSummary
SELECT OrderId, SUM(OrderDetailSize) AS TruckOrderSize
FROM #OrderDetail GROUP BY OrderId

DECLARE @max integer =
POWER(2,
(
SELECT COUNT(*) FROM #orderSummary 
)
) - 1
declare @Delta int
select @Delta= max(TruckOrderSize)-min(TruckOrderSize)   from #orderSummary

请检查我的Delta值,并让我知道是否错误

;WITH cte 
     AS (SELECT n.number, 
                c.* 
         FROM   dbo.tblnumber AS N 
                CROSS apply (SELECT s.orderid, 
                                    s.truckordersize 
                             FROM   #ordersummary AS s 
                             WHERE  n.number & s.bit_value = s.bit_value) c 
         WHERE  N.number BETWEEN 1 AND @max), 
     cte1 
     AS (SELECT c.number, 
                Sum(truckordersize) SumSize 
         FROM   cte c 
         GROUP  BY c.number 
        --HAVING sum(TruckOrderSize) between(@Delta-25) and (@Delta+25) 
        ) 
SELECT c1.*, 
       c.orderid 
FROM   cte1 c1 
       INNER JOIN cte c 
               ON c1.number = c.number 
ORDER  BY sumsize 

DROP TABLE #orderdetail 

DROP TABLE #ordersummary 

DROP TABLE #paulwhitetruck 

您可以检查CTE1的结果,它有可能Permutation and Combination of order along with their size

如果到现在为止我的方法是正确的,那么我需要有人帮助。

待处理任务:

过滤并将结果分成CTE13个部分(Truck count),这样Orderid在每个组中唯一,并且每个部分T ruckOrderSize都接近Delta。


检查我的最新答案。发帖时我错过了一个查询,没人指出我的错误。
复制
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.