この問題について、一定数のトラックに注文をできるだけ均等に積み込むという観点から説明します。
入力:
_@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
_
私の最初の考えは
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と見なすことの認識の飛躍をすることによって1、valuesはその注文を保持するトラックを示し、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」のどちらのラベルを付けても、解決策は同じです。
実際の要件(一連の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
を実行)し、完了したら...tasks
をend = getdate() where id = <id>
で更新します上記の設計により、動的(大部分)のバランスの取れた操作が実現しました。
ノート:
tasks
から「次に利用可能な」操作を引き続きプルすることにより、「余裕期間を増やす」ことができます。tasks
テーブルの設計は、他の利点を提供する必要があります。たとえば、将来の参照用にアーカイブできる実行時間の履歴、優先度の変更に使用できる実行時間の履歴、現在の操作のステータスを提供する必要があります。 、などtasks
の「排他ロック」は少し過剰に見えるかもしれませんが、新しいタスクを取得しようとする2つ(またはそれ以上)のプロセスの潜在的な問題を計画する必要があることに注意してください同時に正確な時間、したがって、タスクが1つのプロセスにのみ割り当てられることを保証する必要があります(そうです、RDBMSのSQL言語機能に応じて、コンボ「更新/選択」ステートメントで同じ結果を得ることができます);新しい「タスク」を取得するステップは迅速である必要があります。つまり、「排他ロック」は短命であり、実際には、プロセスはかなりランダムな方法でtasks
にヒットするため、とにかくほとんどブロックされません個人的には、このtasks
テーブル駆動プロセスは、実装と保守が少し簡単です...(通常)タスク/プロセスマッピングを事前に割り当てようとするより複雑なプロセス... ymmvとは対照的です。
当然のことながら、あなたのトラックを次の注文のために配送/倉庫に戻すことはできないので、あなたはneedをさまざまなトラックに事前に割り当てる必要があります(UPS/Fedex/etcは、配送時間とガス使用量を減らすために配送ルートに基づいて割り当てる必要もあります)。
ただし、実際の例(「統計の更新」)では、タスク/プロセスの割り当てを動的に実行できない理由がないため、(CPU全体で、全体的な実行時間を短縮するという観点から)ワークロードのバランスをとる可能性が高くなります。 。
注:実際にタスクを実行する前に(IT)が(負荷分散の一種として)タスクを事前に割り当てようとしていることを日常的に見ており、everyの場合、最終的には常に調整する必要があります常に変化するタスクの問題(たとえば、テーブル/インデックスの断片化のレベル、同時ユーザーアクティビティなど)を考慮する事前割り当てプロセス.
必要に応じて番号テーブルを作成してデータを追加します。これは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
がデルタに近い.