Opiszę problem w kategoriach załadowania stałej liczby ciężarówek z zamówieniami, tak równo, jak to możliwe.
Wejścia:
@TruckCount - the number of empty trucks to fill
Zestaw:
OrderId,
OrderDetailId,
OrderDetailSize,
TruckId (initially null)
Orders
składają się z jednego lub więcej OrderDetails
.
Wyzwaniem jest przypisanie TruckId
do każdego rekordu.
Pojedynczego zamówienia nie można podzielić na ciężarówki.
Ciężarówki powinny być możliwie równomiernie * obciążone, mierzone według sum(OrderDetailSize)
.
* Równomiernie: najmniejsza możliwa do osiągnięcia delta między najmniej obciążoną ciężarówką a najbardziej obciążoną ciężarówką. Według tej definicji 1,2,3 jest bardziej równomiernie rozłożone niż 1,1,4. Jeśli to pomaga, udawaj, że jesteś algorytmem statystycznym, tworząc nawet histogramy wysokości.
Nie bierze się pod uwagę maksymalnego obciążenia ciężarówki. To magiczne, elastyczne ciężarówki. Liczba ciężarówek jest jednak stała.
Istnieje oczywiście iteracyjne rozwiązanie - okrągły robin przydziela zamówienia.
Ale czy można to zrobić jako logikę opartą na zestawie?
Moje główne zainteresowania to SQL Server 2014 lub nowszy. Ciekawe mogą być również rozwiązania oparte na zestawach dla innych platform.
To wygląda jak terytorium Itzika Ben-Gana :)
Moja aplikacja w świecie rzeczywistym rozkłada obciążenie przetwarzania na wiele segmentów dopasowanych do liczby logicznych procesorów. Dlatego każde wiadro nie ma maksymalnego rozmiaru. W szczególności aktualizacje statystyk. Pomyślałem, że fajniej jest streścić problem na ciężarówki jako sposób ujęcia wyzwania.
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
źródło
Odpowiedzi:
Moją pierwszą myślą było
Część „najlepszego rozwiązania” została zdefiniowana w pytaniu - najmniejsza różnica między najbardziej obciążonymi i najmniej obciążonymi ciężarówkami. Druga część - wszystkie kombinacje - spowodowała, że zastanowiłem się.
Rozważmy sytuację, w której mamy trzy zamówienia A, B i C oraz trzy ciężarówki. Możliwości są
Wiele z nich jest symetrycznych. Na przykład pierwsze sześć rzędów różni się tylko tym, w jakiej ciężarówce jest składane każde zamówienie. Ponieważ ciężarówki można zamieniać, te aranżacje przyniosą ten sam rezultat. Na razie zignoruję to.
Znane są zapytania dotyczące tworzenia permutacji i kombinacji. Powodują one jednak ustalenia w ramach jednego segmentu. W przypadku tego problemu potrzebuję uzgodnień między wieloma segmentami.
Patrząc na wynik standardowego zapytania „wszystkie kombinacje”
Zauważyłem, że wyniki uformowały taki sam wzór jak w Tabeli A. Dokonując sprytnego skoku biorąc pod uwagę każdą kolumnę jako Zamówienie 1 , wartości określające, która ciężarówka będzie utrzymywać to Zamówienie, a rząd jako układ Zamówień w ciężarówkach. Zapytanie staje się następnie
Rozwijając to, aby objąć czternaście zamówień w przykładowych danych, i upraszczając nazwy, otrzymujemy to:
Dla wygody wybieram przechowywanie wyników pośrednich w tabelach tymczasowych.
Kolejne kroki będą znacznie łatwiejsze, jeśli dane zostaną po raz pierwszy UNPIVOTED.
Wagi można wprowadzić, łącząc się z tabelą Zamówienia.
Można teraz odpowiedzieć na pytanie, znajdując układy, które mają najmniejszą różnicę między najczęściej załadowanymi i najmniej załadowanymi ciężarówkami
Dyskusja
Jest z tym bardzo wiele problemów. Po pierwsze, jest to algorytm brutalnej siły. Liczba wierszy w tabelach roboczych jest wykładnicza pod względem liczby ciężarówek i zamówień. Liczba wierszy w #Arrangements to (liczba ciężarówek) ^ (liczba zamówień). To nie będzie dobrze skalować.
Po drugie, zapytania SQL mają osadzoną liczbę zamówień. Jedynym sposobem na obejście tego jest użycie dynamicznego SQL, który ma własne problemy. Jeśli liczba zamówień jest w tysiącach, może przyjść czas, kiedy wygenerowany SQL stanie się zbyt długi.
Trzecia to nadmiarowość w ustaleniach. Powoduje to nadmierne powiększanie tabel pośrednich, zwiększając znacznie czas działania.
Po czwarte, wiele wierszy w #Arrangements pozostawia jedną lub więcej ciężarówek pustych. Nie może to być optymalna konfiguracja. Łatwo byłoby odfiltrować te wiersze podczas tworzenia. Zdecydowałem się tego nie robić, aby kod był prostszy i bardziej skoncentrowany.
Z drugiej strony ma to wpływ na ujemne ciężary, jeśli Twoje przedsiębiorstwo zacznie kiedykolwiek wysyłać wypełnione balony z helem!
Myśli
Gdyby istniał sposób na wypełnienie #FilledTrucks bezpośrednio z listy ciężarówek i zamówień, myślę, że najgorszym z tych problemów można by zaradzić. Niestety moja wyobraźnia natknęła się na tę przeszkodę. Mam nadzieję, że jakiś przyszły współpracownik może być w stanie dostarczyć to, co mi umknęło.
1 Mówisz, że wszystkie elementy zamówienia muszą znajdować się w tej samej ciężarówce. Oznacza to, że atomem przypisania jest Zamówienie, a nie Zamówienie Szczegóły. Wygenerowałem je z danych testowych w ten sposób:
Nie ma jednak znaczenia, czy oznaczymy przedmiotowe pozycje „Zamów”, czy „Szczegóły zamówienia”, rozwiązanie pozostaje takie samo.
źródło
Patrząc na twoje wymagania w świecie rzeczywistym (zakładam, że próbuję zrównoważyć twoje obciążenie pracą przez zestaw procesorów) ...
Czy istnieje powód, dla którego trzeba wstępnie przypisywać procesy do określonych segmentów / procesorów? [Próbuje zrozumieć twoje prawdziwe wymagania]
Na przykład „aktualizacji statystyk”, skąd wiesz, ile czasu zajmie dana operacja? Co się stanie, jeśli dana operacja napotka nieoczekiwane opóźnienie (np. Nadmierna planowana / nadmierna fragmentacja tabeli / indeksu, długo działający użytkownik txn blokuje operację „aktualizacji statystyk”)?
Dla celów równoważenia obciążenia zazwyczaj generuję listę zadań (np. Listę tabel, aby zaktualizować statystyki) i umieszczam tę listę w tabeli (tymczasowej / zadrapania).
Struktura tabeli może być modyfikowana zgodnie z Twoimi wymaganiami, np .:
Następnie rozpoczynam X równoczesnych procesów, aby wykonać rzeczywiste operacje „aktualizacji statystyk”, przy czym każdy proces wykonuje następujące czynności:
tasks
stole (zapewnia, że żadne zadanie nie zostanie odebrane przez więcej niż jeden proces; powinna być relatywnie krótkotrwałą blokadą)start = NULL
(„pierwszy” zostanie określony przez Ciebie, np. uporządkować wedługpriority
?)start = getdate(), thread = <process_number>
id
itarget/command
wartościtarget
(alternatywnie, uruchomcommand
), a po zakończeniu ...tasks
pomocąend = getdate() where id = <id>
Dzięki powyższemu projektowi mam teraz dynamicznie (głównie) zrównoważoną operację.
UWAGI:
tasks
tasks
tabeli powinna zapewniać inne korzyści, np. historię czasów wykonywania, które można zarchiwizować do wykorzystania w przyszłości, historię czasów pracy, które można wykorzystać do modyfikacji priorytetów, zapewnienia statusu bieżących operacji itp.tasks
może wydawać się nieco nadmierna, pamiętaj, że musimy zaplanować potencjalny problem 2 (lub więcej) procesów próbujących uzyskać nowe zadanie w tym samym czasie , więc musimy zagwarantować zadanie jest przypisany tylko do jednego procesu (i tak, można uzyskać te same wyniki za pomocą komendy „update / select” - zależnie od możliwości języka SQL RDBMS); etap uzyskania nowego „zadania” powinien być szybki, tj. „blokada wyłączności” powinna być krótkotrwała, a w rzeczywistości procesy będą uderzaćtasks
w dość losowy sposób, więc i tak będzie mało blokowaćOsobiście uważam, że ten
tasks
proces oparty na tabeli jest nieco łatwiejszy do wdrożenia i utrzymania ... w przeciwieństwie do (zwykle) bardziej złożonego procesu próbowania wstępnego przypisania mapowania zadań / procesów ... ymmv.Oczywiście dla twojego przykładu, nie możesz kazać swoim ciężarówkom wracać do dystrybucji / magazynu dla następnego zamówienia, więc musisz wstępnie przypisać swoje zamówienia do różnych ciężarówek (pamiętając, że UPS / Fedex / itp. Również muszą przypisywanie na podstawie tras dostaw, aby skrócić czas dostawy i zużycie gazu).
Jednak w twoim przykładzie z prawdziwego świata („aktualizacja statystyk”) nie ma powodu, dla którego przypisania zadania / procesu nie mogą być wykonywane dynamicznie, co zapewnia większą szansę na zrównoważenie obciążenia pracą (między procesami i pod względem skrócenia ogólnego czasu działania) .
UWAGA: Rutynowo widzę ludzi (IT), którzy próbują wstępnie przypisać swoje zadania (jako formę równoważenia obciążenia) przed faktycznym uruchomieniem tych zadań, i w każdym przypadku musi on stale dostosowywać proces wstępnego przypisania, aby podjąć biorąc pod uwagę stale zmieniające się problemy dotyczące zadań (np. poziom fragmentacji w tabeli / indeksie, równoczesna aktywność użytkownika itp.)
źródło
utwórz i wypełnij tabelę liczb według własnego uznania. Jest to jednorazowe stworzenie.
Utworzono stół dla ciężarówek
Stworzyłem jeden
OrderSummary
stółSprawdź moją wartość Delta i daj mi znać, jeśli jest niepoprawna
Możesz sprawdzić wynik CTE1, to wszystko jest możliwe
Permutation and Combination of order along with their size
.Jeśli moje podejście jest poprawne do tej pory, potrzebuję pomocy.
filtruj i dziel wynik
CTE1
na 3 części (Truck count
), tak, żeOrderid
jest unikalny dla każdej grupy, a każda część TruckOrderSize
jest zbliżona do Delta.źródło