Mein erster Gedanke war
select
<best solution>
from
<all possible combinations>
Der Teil "Beste Lösung" wird in der Frage definiert - der kleinste Unterschied zwischen den am meisten beladenen und den am wenigsten beladenen Lastwagen. Das andere Stück - alle Kombinationen - ließ mich nachdenken.
Stellen Sie sich eine Situation vor, in der wir drei Aufträge A, B und C sowie drei Lastwagen haben. Die Möglichkeiten sind
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.
Viele davon sind symmetrisch. Die ersten sechs Zeilen unterscheiden sich beispielsweise nur darin, in welchem Lkw jede Bestellung aufgegeben wird. Da die Lastwagen fungibel sind, erzielen diese Arrangements das gleiche Ergebnis. Ich werde das jetzt ignorieren.
Es sind Abfragen zum Erzeugen von Permutationen und Kombinationen bekannt. Diese werden jedoch Anordnungen innerhalb eines einzelnen Eimers erzeugen. Für dieses Problem brauche ich Vereinbarungen über mehrere Eimer.
Betrachtet man die Ausgabe der Standardabfrage "Alle Kombinationen"
;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.
Ich bemerkte, dass die Ergebnisse dasselbe Muster wie in Tabelle A bildeten. Indem ich den Gesamtsprung machte, jede Spalte als eine Bestellung 1 zu betrachten , die Werte , die besagen, welcher LKW diese Bestellung enthält, und eine Reihe , die eine Anordnung von Bestellungen innerhalb von LKWs darstellt. Die Abfrage wird dann
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.
Erweitern Sie dies, um die vierzehn Befehle in den Beispieldaten abzudecken, und vereinfachen Sie die Namen, die wir erhalten:
;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.
Ich halte die Zwischenergebnisse der Einfachheit halber in temporären Tabellen.
Nachfolgende Schritte werden viel einfacher, wenn die Daten zuerst UNPIVOTED sind.
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.
Gewichte können durch Hinzufügen zur Tabelle "Bestellungen" eingegeben werden.
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
Die Frage kann nun beantwortet werden, indem die Anordnung (en) gefunden werden, die den geringsten Unterschied zwischen am meisten beladenen und am wenigsten beladenen Lastwagen aufweisen
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
Diskussion
Es gibt sehr viele Probleme damit. Erstens ist es ein Brute-Force-Algorithmus. Die Anzahl der Zeilen in den Arbeitstabellen ist in Bezug auf die Anzahl der Lastkraftwagen und Aufträge exponentiell. Die Anzahl der Zeilen in #Arrangements ist (Anzahl der Lastwagen) ^ (Anzahl der Bestellungen). Dies wird nicht gut skalieren.
Zweitens ist in den SQL-Abfragen die Anzahl der Bestellungen eingebettet. Der einzige Weg, dies zu umgehen, ist die Verwendung von dynamischem SQL, das seine eigenen Probleme hat. Wenn die Anzahl der Bestellungen in Tausenden liegt, kann es vorkommen, dass die generierte SQL zu lang wird.
Drittens ist die Redundanz in den Vereinbarungen. Dadurch werden die Zwischentabellen aufgebläht und die Laufzeit erheblich erhöht.
Viertens lassen viele Zeilen in #Arrangements einen oder mehrere Lastwagen leer. Dies kann unmöglich die optimale Konfiguration sein. Es wäre einfach, diese Zeilen bei der Erstellung herauszufiltern. Ich habe beschlossen, dies nicht zu tun, um den Code einfacher und fokussierter zu halten.
Auf der anderen Seite können negative Gewichte verarbeitet werden, falls Ihr Unternehmen jemals mit dem Versand gefüllter Heliumballons beginnen sollte!
Gedanken
Wenn es eine Möglichkeit gäbe, #FilledTrucks direkt aus der Liste der LKWs und Aufträge zu übernehmen, wären die schlimmsten dieser Bedenken meines Erachtens beherrschbar. Leider stolperte meine Vorstellungskraft über diese Hürde. Ich hoffe, dass ein zukünftiger Mitwirkender das liefern kann, was mir entgangen ist.
1 Sie sagen, dass sich alle Artikel für eine Bestellung auf demselben LKW befinden müssen. Dies bedeutet, dass das Zuweisungsatom der Auftrag und nicht der Auftragsdetail ist. Ich habe diese aus Ihren Testdaten so generiert:
select
OrderId,
Size = sum(OrderDetailSize)
into #Order
from #OrderDetail
group by OrderId;
Es macht jedoch keinen Unterschied, ob wir die betreffenden Artikel mit "Bestellung" oder "BestellDetail" kennzeichnen, die Lösung bleibt gleich.