Meu primeiro pensamento foi
select
<best solution>
from
<all possible combinations>
A parte "melhor solução" é definida na pergunta - a menor diferença entre os caminhões mais carregados e os menos carregados. A outra parte - todas as combinações - me fez parar para pensar.
Considere uma situação em que temos três ordens A, B e C e três caminhões. As possibilidades são
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.
Muitos destes são simétricos. As seis primeiras linhas, por exemplo, diferem apenas em qual caminhão cada pedido é feito. Como os caminhões são fungíveis, esses arranjos produzirão o mesmo resultado. Vou ignorar isso por enquanto.
Existem consultas conhecidas para produzir permutações e combinações. No entanto, isso produzirá arranjos dentro de um único balde. Para esse problema, preciso de arranjos em vários baldes.
Analisando a saída da consulta padrão "todas as combinações"
;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.
Observei que os resultados formaram o mesmo padrão da Tabela A. Ao dar o salto de considerar cada coluna como uma ordem 1 , os valores para dizer qual caminhão manterá essa ordem e uma linha para ser um arranjo de ordens dentro de caminhões. A consulta então se torna
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.
Expandir isso para cobrir os quatorze pedidos nos dados de exemplo e simplificar os nomes que obtemos:
;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.
Eu escolho manter os resultados intermediários em tabelas temporárias por conveniência.
As etapas subseqüentes serão muito mais fáceis se os dados não forem liberados pela primeira 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.
Os pesos podem ser introduzidos ingressando na tabela 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
Agora, a pergunta pode ser respondida encontrando-se o (s) arranjo (s) com a menor diferença entre os caminhões mais carregados e os menos carregados
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
Discussão
Existem muitos problemas com isso. Primeiro, é um algoritmo de força bruta. O número de linhas nas tabelas de trabalho é exponencial no número de caminhões e pedidos. O número de linhas em # Arranjos é (número de caminhões) ^ (número de pedidos). Isso não vai escalar bem.
Segundo: as consultas SQL têm o número de pedidos incorporado. A única maneira de contornar isso é usar o SQL dinâmico, que possui problemas próprios. Se o número de pedidos estiver na casa dos milhares, poderá chegar um momento em que o SQL gerado se tornará muito longo.
Terceiro: a redundância nos acordos. Isso incha as tabelas intermediárias, aumentando enormemente o tempo de execução.
Quarto, muitas linhas em #Arrangements deixam um ou mais caminhões vazios. Esta não pode ser a configuração ideal. Seria fácil filtrar essas linhas na criação. Decidi não fazer isso para manter o código mais simples e focado.
No lado positivo, isso lida com pesos negativos, caso sua empresa comece a enviar balões de hélio cheios!
Pensamentos
Se houvesse uma maneira de preencher a #FilledTrucks diretamente da lista de caminhões e pedidos, acho que a pior dessas preocupações seria administrável. Infelizmente, minha imagem tropeçou nesse obstáculo. Minha esperança é que algum colaborador futuro possa suprir aquilo que me escapou.
1 Você diz que todos os itens de um pedido devem estar no mesmo caminhão. Isso significa que o átomo de atribuição é a Ordem, não o Detalhe da Ordem. Eu os criei a partir dos dados de teste assim:
select
OrderId,
Size = sum(OrderDetailSize)
into #Order
from #OrderDetail
group by OrderId;
Não faz diferença, porém, se rotularmos os itens em questão como 'Pedido' ou 'Detalhe da solicitação', a solução permanecerá a mesma.