web-dev-qa-db-ja.com

クエリの課題:行数ではなくメジャーに基づいて、偶数サイズのバケットを作成する

この問題について、一定数のトラックに注文をできるだけ均等に積み込むという観点から説明します。

入力:

_@TruckCount - the number of empty trucks to fill
_

セット:

_OrderId, 
OrderDetailId, 
OrderDetailSize, 
TruckId (initially null)
_

Ordersは1つ以上のOrderDetailsで構成されます。

ここでの課題は、各レコードにTruckIdを割り当てることです。

1つの注文を複数のトラックに分割することはできません。

トラックは、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
_
12
Paul Holmes

私の最初の考えは

select
    <best solution>
from
    <all possible combinations>

「最良の解決策」の部分は、質問で定義されています-積載量が最も多いトラックと積載量が最も少ないトラックとの間の最小の違い。もう1つのビット-すべての組み合わせ-は、私が考えるのを一時停止させました。

3つの注文A、B、Cと3つのトラックがある状況を考えます。可能性は

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.

これらの多くは対称です。たとえば、最初の6行は、各注文が配置されるトラックのみが異なります。トラックは交換可能であるため、これらの配置は同じ結果になります。今のところこれは無視します。

順列と組み合わせを生成するための既知のクエリがあります。ただし、これらは単一のバケット内に配置を生成します。この問題を解決するには、複数のバケットにまたがる調整が必要です。

標準の「すべての組み合わせ」クエリの出力を確認する

;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と同じパターンを形成していることに注意しました。各columnをOrderと見なすことの認識の飛躍をすることによって1valuesはその注文を保持するトラックを示し、rowはトラック内の注文の配置です。クエリは次のようになります

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.

これを拡張してサンプルデータの14の注文をカバーし、名前を単純化してこれを取得します。

;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.

重みは、Ordersテーブルに結合することで導入できます。

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の行数は(トラック数)^(注文数)です。これはうまくスケーリングしません。

2つ目は、SQLクエリに多数の注文が埋め込まれていることです。これを回避する唯一の方法は、独自の問題がある動的SQLを使用することです。注文数が数千の場合、生成されたSQLが長くなりすぎることがあります。

3つ目は、取り決めの冗長性です。これにより、中間テーブルが膨張し、ランタイムが大幅に増加します。

第4に、#Arrangementsの多くの行は1つ以上のトラックを空のままにします。これはおそらく最適な構成ではありません。作成時にこれらの行をフィルターで除外するのは簡単です。私は、コードをより単純かつ集中的に保つために、そうしないことを選択しました。

有利な点として、これは負の重量を処理します。企業がヘリウム風船を満たし始めたら、

考え

トラックと注文のリストから直接#FilledTrucksにデータを入力する方法があった場合、これらの懸念の中で最悪のものは扱いやすいと思います。悲しいことに、私の想像力はそのハードルに出くわしました。私の希望は、将来の貢献者が私を避けたものを提供できるようになることです。




1 注文のすべてのアイテムが同じトラックにある必要があると言います。これは、割り当てのatomがOrderDetailでなくOrderであることを意味します。テストデータから次のように生成しました。

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

ただし、問題のアイテムに「Order」と「OrderDetail」のどちらのラベルを付けても、解決策は同じです。

5
Michael Green

実際の要件(一連のCPU間でワークロードのバランスをとろうとしていると想定しています)を確認します...

プロセスを特定のバケット/ 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>
  • 更新のコミット(および排他ロックの解放)
  • idと_target/command_の値を書き留めます
  • targetに対して目的の操作を実行(または、commandを実行)し、完了したら...
  • tasksend = getdate() where id = <id>で更新します
  • 実行するタスクがなくなるまで上記を繰り返します

上記の設計により、動的(大部分)のバランスの取れた操作が実現しました。

ノート:

  • ある種の優先順位付け方法を提供して、長時間実行中のタスクを前もって開始できるようにしています。いくつかのプロセスが実行時間の長いタスクに取り組んでいる間、他のプロセスは実行時間の短いタスクのリストをチャーンすることができます
  • プロセスが予定外の遅延に陥った場合(長時間実行、ユーザーtxnのブロックなど)、他のプロセスはtasksから「次に利用可能な」操作を引き続きプルすることにより、「余裕期間を増やす」ことができます。
  • tasksテーブルの設計は、他の利点を提供する必要があります。たとえば、将来の参照用にアーカイブできる実行時間の履歴、優先度の変更に使用できる実行時間の履歴、現在の操作のステータスを提供する必要があります。 、など
  • tasksの「排他ロック」は少し過剰に見えるかもしれませんが、新しいタスクを取得しようとする2つ(またはそれ以上)のプロセスの潜在的な問題を計画する必要があることに注意してください同時に正確な時間、したがって、タスクが1つのプロセスにのみ割り当てられることを保証する必要があります(そうです、RDBMSのSQL言語機能に応じて、コンボ「更新/選択」ステートメントで同じ結果を得ることができます);新しい「タスク」を取得するステップは迅速である必要があります。つまり、「排他ロック」は短命であり、実際には、プロセスはかなりランダムな方法でtasksにヒットするため、とにかくほとんどブロックされません

個人的には、このtasksテーブル駆動プロセスは、実装と保守が少し簡単です...(通常)タスク/プロセスマッピングを事前に割り当てようとするより複雑なプロセス... ymmvとは対照的です。


当然のことながら、あなたのトラックを次の注文のために配送/倉庫に戻すことはできないので、あなたはneedをさまざまなトラックに事前に割り当てる必要があります(UPS/Fedex/etcは、配送時間とガス使用量を減らすために配送ルートに基づいて割り当てる必要もあります)。

ただし、実際の例(「統計の更新」)では、タスク/プロセスの割り当てを動的に実行できない理由がないため、(CPU全体で、全体的な実行時間を短縮するという観点から)ワークロードのバランスをとる可能性が高くなります。 。

注:実際にタスクを実行する前に(IT)が(負荷分散の一種として)タスクを事前に割り当てようとしていることを日常的に見ており、everyの場合、最終的には常に調整する必要があります常に変化するタスクの問題(たとえば、テーブル/インデックスの断片化のレベル、同時ユーザーアクティビティなど)を考慮する事前割り当てプロセス.

4
markp-fuso

必要に応じて番号テーブルを作成してデータを追加します。これは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テーブルを1つ作成しました

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

デルタ値を確認して、間違っている場合はお知らせください

;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です。

ここまでのアプローチが正しければ、誰かの助けが必要です。

保留中のタスク:

CTE1の結果を3つの部分(Truck count)にフィルタリングおよび分割し、Orderidが各グループ間で一意であり、各部分T ruckOrderSizeがデルタに近い.

1
KumarHarsh