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


12

可能な限り均等に、一定数のトラックに注文を積むという観点から問題を説明します。

入力:

@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


1
Hugo Kornelisも同様に良い仕事をしています。
エリックダーリン

すべてのOrderDetailSize値は、指定されたOrderIdで等しくなりますか、それともサンプルデータに一致するだけですか?
youcantryreachingme

@youcantryreachingmeああ、良いスポット...いや、それは単なるサンプルデータの偶然の一致です。
ポールホームズ

回答:


5

私の最初の考えは

select
    <best solution>
from
    <all possible combinations>

「最適なソリューション」の部分は、質問で定義されています-最も負荷の高いトラックと最も負荷の少ないトラックの最小の差。他のビット-すべての組み合わせ-私は思考のために一時停止しました。

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.

私は、結果がそれぞれ考慮のcongnitive飛躍することにより、表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.

サンプルデータの14個の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.

便宜上、中間結果を一時テーブルに保持することを選択します。

データが最初にUNPIVOTEDされると、後続のステップははるかに簡単になります。

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クエリに埋め込まれたOrderの数があることです。これを回避する唯一の方法は、独自の問題がある動的SQLを使用することです。注文数が数千の場合、生成されたSQLが長くなりすぎる場合があります。

3番目は、配置の冗長性です。これにより、中間テーブルが膨張し、実行時間が大幅に増加します。

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

あなたの企業が充填ヘリウム風船の出荷を開始する必要がある場合、これは負の重みを処理します!

考え

#FilledTrucksをトラックと注文のリストから直接入力する方法があれば、これらの懸念の最悪は管理可能だと思います。悲しいことに私の想像力はそのハードルにつまずいた。私の希望は、将来の貢献者が私を逃したものを提供できるかもしれないことです。




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

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

ただし、問題のアイテムに「Order」または「OrderDetail」というラベルを付けても、ソリューションは変わりません。


4

あなたの実世界の要件を見ると(これは、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>
  • 更新のコミット(および排他ロックの解放)
  • メイクのノートidtarget/command
  • target(または、実行command)に対して目的の操作を実行し、完了したら...
  • アップデートtasksend = getdate() where id = <id>
  • 実行するタスクがなくなるまで上記を繰り返します

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

ノート:

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

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


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

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

注:私は定期的(ロードバランシングの形など)への事前割り当てタスクをしよう(IT)が人々を参照する前に、実際に実行中のタスクを言った、としておきケースS /彼は常にテイクに事前割り当てプロセスを微調整する必要が終わります常に変化するタスクの問題(たとえば、テーブル/インデックスの断片化のレベル、同時ユーザーアクティビティなど)を考慮します。


まず、「order」をテーブル、「orderdetail」をテーブルの特定の統計と考える場合、分割しない理由は、競合するバケット間のロック待機を避けるためです。Traceflag 7471はこの問題を解決するように設計されていますが、テストではロックの問題がまだありました。
ポールホームズ

私はもともと非常に軽量なソリューションを作りたいと思っていました。単一のマルチステートメントSQLブロックとしてバケットを作成し、自己破棄SQLエージェントジョブを使用してそれぞれを「発射して忘れる」。つまり、キュー管理作業はありません。しかし、その後、統計ごとに作業量を簡単に測定できないことがわかりました。行数がそれを削減しませんでした。行カウントが1つのテーブルからのIOの量に線形にマッピングされない、または実際には次のテーブルに確実にマッピングされないことを考えると、本当に驚くことではありません。そのため、このアプリケーションでは、提案されているように、アクティブなキュー管理を追加することで、実際に自己バランスを取ることができます。
ポールホームズ

あなたの最初のコメント...ええ、コマンドの粒度についての(明らかな)決定がまだあります...そして次のような並行性の問題があります:いくつかのコマンドは並行して実行でき、それらの結合されたディスク読み取りなどの恩恵を受けることができますが、私はまだ見つけています(やや軽い)動的なキュー管理は、バケットを事前に割り当てるよりも少し効率的です:-)適切な答え/アイデアのセットがあります...提供するソリューションを考え出すのは難しくないはずですある程度の負荷分散。
マーク

1

必要に応じて番号テーブルを作成して入力します。これは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 )

1つの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

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

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

ここまで私のアプローチが正しい場合は、誰かの助けが必要です。

保留中のタスク:

各グループ間で一意であり、各部分T がデルタに近いように、フィルターと分割結果CTE1を3部分に分割します(Truck count)。OrderidruckOrderSize


掲載しながら、私の最新answer.Iミス1つのクエリを確認し、誰も私のmistake.Copyペーストして実行指さない
KumarHarsh
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.