Mi primer pensamiento fue
select
<best solution>
from
<all possible combinations>
La parte de "mejor solución" se define en la pregunta: la diferencia más pequeña entre los camiones más cargados y los menos cargados. La otra parte, todas las combinaciones, me hizo reflexionar.
Considere una situación en la que tenemos tres órdenes A, B y C y tres camiones. Las posibilidades son
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.
Muchos de estos son simétricos. Las primeras seis filas, por ejemplo, difieren solo en qué camión se realiza cada pedido. Como los camiones son fungibles, estos arreglos producirán el mismo resultado. Ignoraré esto por ahora.
Existen consultas conocidas para producir permutaciones y combinaciones. Sin embargo, estos producirán arreglos dentro de un solo cubo. Para este problema, necesito arreglos en varios cubos.
Mirando el resultado de la consulta estándar "todas las combinaciones"
;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.
Noté que los resultados formaron el mismo patrón que la Tabla A. Al dar el salto cognitivo de considerar cada columna como una Orden 1 , los valores para decir qué camión mantendrá esa Orden y una fila para ser una disposición de las Órdenes dentro de los camiones. La consulta se convierte en
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.
Expandiendo esto para cubrir las catorce Órdenes en los datos de ejemplo, y simplificando los nombres obtenemos esto:
;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.
Elijo mantener los resultados intermedios en tablas temporales por conveniencia.
Los pasos subsiguientes serán mucho más fáciles si los datos se DESVIOTAN por primera vez.
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.
Los pesos se pueden introducir uniéndose a la tabla Pedidos.
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
La pregunta ahora puede responderse encontrando los arreglos que tienen la menor diferencia entre los camiones con más carga y los menos cargados
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
Discusión
Hay muchos problemas con esto. Primero es un algoritmo de fuerza bruta. El número de filas en las tablas de trabajo es exponencial en el número de camiones y pedidos. El número de filas en #Arrangements es (número de camiones) ^ (número de pedidos). Esto no escalará bien.
En segundo lugar, las consultas SQL tienen incrustada la cantidad de Órdenes. La única forma de evitar esto es usar SQL dinámico, que tiene sus propios problemas. Si el número de pedidos es de miles, puede llegar un momento en que el SQL generado sea demasiado largo.
El tercero es la redundancia en los arreglos. Esto hincha las tablas intermedias aumentando enormemente el tiempo de ejecución.
Cuarto, muchas filas en #Arrangements dejan uno o más camiones vacíos. Esto no puede ser la configuración óptima. Sería fácil filtrar estas filas después de la creación. Elegí no hacerlo para mantener el código más simple y enfocado.
En el lado positivo, esto maneja pesos negativos, en caso de que su empresa comience a enviar globos de helio llenos.
Pensamientos
Si hubiera una manera de poblar #FilledTrucks directamente desde la lista de camiones y pedidos, creo que la peor de estas preocupaciones sería manejable. Lamentablemente, mi imaginación tropezó con ese obstáculo. Espero que algún contribuyente futuro pueda proporcionar lo que se me escapó.
1 Usted dice que todos los artículos para un pedido deben estar en el mismo camión. Esto significa que el átomo de asignación es el Order, no el OrderDetail. Los he generado a partir de sus datos de prueba de esta manera:
select
OrderId,
Size = sum(OrderDetailSize)
into #Order
from #OrderDetail
group by OrderId;
Sin embargo, no importa si etiquetamos los artículos en cuestión como 'Pedido' o 'Detalle del pedido', la solución sigue siendo la misma.