Descreverei o problema em termos de carregamento de um número fixo de caminhões com pedidos, o mais uniformemente possível.
Entradas:
@TruckCount - the number of empty trucks to fill
Um conjunto:
OrderId,
OrderDetailId,
OrderDetailSize,
TruckId (initially null)
Orders
são compostos por um ou mais OrderDetails
.
O desafio aqui é atribuir um TruckId
a cada registro.
Um único pedido não pode ser dividido entre caminhões.
Os caminhões devem ser carregados o mais uniformemente possível, medido por sum(OrderDetailSize)
.
* Uniformemente: o menor delta possível entre o caminhão menos carregado e o caminhão mais carregado. Por essa definição, 1,2,3 é distribuído de maneira mais uniforme que 1,1,4. Se ajudar, finja que você é o algoritmo de estatísticas, criando histogramas de altura uniformes.
Não há consideração pela carga máxima do caminhão. Estes são caminhões elásticos mágicos. O número de caminhões, no entanto, é fixo.
Obviamente, existe uma solução iterativa - o rodízio rotativo aloca pedidos.
Mas isso pode ser feito como lógica baseada em conjunto?
Meu principal interesse é pelo SQL Server 2014 ou posterior. Mas soluções baseadas em outras plataformas também podem ser interessantes.
Parece o território Itzik Ben-Gan :)
Meu aplicativo no mundo real está distribuindo uma carga de trabalho de processamento em vários buckets para corresponder ao número de CPUs lógicas. Portanto, cada balde não tem tamanho máximo. Atualizações de estatísticas, especificamente. Eu apenas pensei que era mais divertido abstrair o problema em caminhões como uma maneira de enquadrar o desafio.
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
fonte
Respostas:
Meu primeiro pensamento foi
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
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"
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
Expandir isso para cobrir os quatorze pedidos nos dados de exemplo e simplificar os nomes que obtemos:
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.
Os pesos podem ser introduzidos ingressando na tabela Pedidos.
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
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:
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.
fonte
Analisando seus requisitos do mundo real (que eu suponho que seja uma tentativa de equilibrar sua carga de trabalho em um conjunto de cpus) ...
Existe uma razão para você precisar pré-atribuir processos a buckets / cpus específicos? [Tentando entender seus requisitos reais ]
Para o seu exemplo de 'atualizações de estatísticas', como você sabe quanto tempo uma operação específica levará? E se uma determinada operação tiver um atraso inesperado (por exemplo, fragmentação acima da planejada / excessiva de tabela / índice, o usuário de execução prolongada txn bloqueia uma operação de 'atualização de estatísticas')?
Para fins de balanceamento de carga, normalmente eu gero a lista de tarefas (por exemplo, lista de tabelas para atualizar as estatísticas) e coloco a lista em uma tabela (temporária / temporária).
A estrutura da tabela pode ser modificada de acordo com seus requisitos, por exemplo:
Em seguida, inicio o número X de processos simultâneos para executar as operações reais de 'atualização de estatísticas', com cada processo executando o seguinte:
tasks
mesa (garante que nenhuma tarefa seja escolhida por mais de um processo; deve ser um bloqueio relativamente curto)start = NULL
('primeiro' seria determinado por você, por exemplo, ordenar porpriority
?)start = getdate(), thread = <process_number>
id
etarget/command
valorestarget
(alternativamente, executecommand
) e quando terminar ...tasks
comend = getdate() where id = <id>
Com o design acima, agora tenho uma operação balanceada dinamicamente (principalmente).
NOTAS:
tasks
tasks
tabela deve fornecer outros benefícios, por exemplo, um histórico de tempos de execução que você pode arquivar para referência futura, um histórico de tempos de execução que pode ser usado para modificar prioridades, fornecer um status das operações atuais etc.tasks
possa parecer um pouco excessivo, lembre-se de que precisamos planejar o possível problema de 2 (ou mais) processos que tentam obter uma nova tarefa ao mesmo tempo , portanto, precisamos garantir uma tarefa é atribuído a apenas um processo (e sim, você pode obter os mesmos resultados com uma instrução 'update / select' combinada - dependendo dos recursos da linguagem SQL do RDBMS); a etapa de obtenção de uma nova 'tarefa' deve ser rápida, ou seja, a 'trava exclusiva' deve durar pouco e, na realidade, os processos serão atingidos detasks
maneira bastante aleatória e, portanto, será pouco obstrutiva.Pessoalmente, acho esse
tasks
processo orientado por tabela um pouco mais fácil de implementar e manter ... em oposição a um processo (geralmente) mais complexo de tentar pré-atribuir mapeamentos de tarefas / processos ... ymmv.Obviamente, no seu exemplo de faz de conta, você não pode ter seus caminhões voltando à distribuição / armazém para o próximo pedido; portanto, é necessário pré-atribuir seus pedidos a vários caminhões (tendo em mente que a UPS / Fedex / etc também precisa atribuir com base nas rotas de entrega para reduzir o tempo de entrega e o uso de gás).
No entanto, no seu exemplo do mundo real ('atualização de estatísticas'), não há razão para que as atribuições de tarefas / processos não possam ser feitas dinamicamente, garantindo assim uma melhor chance de equilibrar a carga de trabalho (entre cpus e em termos de redução do tempo de execução geral) .
OBSERVAÇÃO: Eu vejo rotineiramente as pessoas (TI) tentando pré-atribuir suas tarefas (como uma forma de balanceamento de carga) antes de executar as tarefas, e em todos os casos, ele acaba precisando ajustar constantemente o processo de pré-atribuição para executar levar em consideração questões de tarefas que variam constantemente (por exemplo, nível de fragmentação na tabela / índice, atividade simultânea do usuário etc.).
fonte
crie e preencha a tabela numérica como desejar. Essa é apenas uma criação.
Tabela de caminhão criada
Eu criei uma
OrderSummary
tabelaVerifique meu valor Delta e deixe-me saber se está errado
Você pode verificar o resultado do CTE1, tudo isso é possível
Permutation and Combination of order along with their size
.Se minha abordagem estiver correta até aqui, preciso de ajuda de alguém.
filtre e Divida o resultado
CTE1
em até 3 partes (Truck count
), de modoOrderid
exclusivo entre cada grupo e cada parte TruckOrderSize
esteja próxima ao Delta.fonte