Виклик із запитом: Створення відра рівних розмірів на основі міри, а не кількості рядків


12

Я опишу проблему з точки зору завантаження фіксованої кількості вантажних автомобілів із замовленнями, наскільки це можливо рівномірно.

Вхідні дані:

@TruckCount - the number of empty trucks to fill

Набір:

OrderId, 
OrderDetailId, 
OrderDetailSize, 
TruckId (initially null)

Ordersскладаються з одного або декількох OrderDetails.

Завдання тут полягає в призначенні TruckIdкожного запису.

Одне замовлення не можна розділити на вантажні автомобілі.

Вантажівки повинні бути максимально рівномірно * завантажені, виміряні sum(OrderDetailSize).

* Рівномірно: Найменша досяжна дельта між найменш завантаженою вантажівкою та найбільш завантаженою вантажівкою. За цим визначенням 1,2,3 розподіляється більш рівномірно, ніж 1,1,4. Якщо це допомагає, зробіть вигляд, що ви алгоритм статистики, створюючи гістограми рівних висот.

Максимальне завантаження вантажівки не враховується. Це чарівні еластичні вантажівки. Однак кількість вантажних автомобілів є фіксованою.

Очевидно, що це ітеративне рішення - круглий робот розподіляє замовлення.

Але чи можна це зробити як задану логіку?

Моя основна зацікавленість - для SQL Server 2014 або новішої версії. Але набір рішень для інших платформ також може бути цікавим.

Це відчувається як територія Іціка Бен-Гана :)

Моя реальна програма - це розподіл навантаження на обробку в декілька відер, щоб відповідати кількості логічних процесорів. Отже, кожне відро не має максимального розміру. Зокрема, оновлення статистики. Я просто подумав, що було цікавіше абстрагувати проблему на вантажівках як спосіб вирішення проблеми.

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


1
Уго Корнеліс також добре працює над цим.
Ерік Дарлінг

Чи всі значення OrderDetailSize будуть рівними для даного OrderId або це просто співпадіння у ваших вибіркових даних?
youcantryreachingme

@youcantryreachingme Ах, добре місце ... ні, це не лише випадкові випадки у даних вибірки.
Пол Холмс

Відповіді:


5

Моя перша думка була

select
    <best solution>
from
    <all possible combinations>

Частина "найкращого рішення" визначена у питанні - найменша різниця між найбільш завантаженими та найменш завантаженими вантажівками. Інший шматочок - всі комбінації - викликав у мене паузу для роздумів.

Розглянемо ситуацію, коли у нас є три замовлення A, B і C і три вантажівки. Можливості є

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.

Багато з них симетричні. Наприклад, перші шість рядків відрізняються лише тим, на яку вантажівку розміщено кожне замовлення. Оскільки вантажні автомобілі підлягають зміні, ці домовленості дадуть такий же результат. Я зараз ігнорую це.

Відомі запити для створення перестановок та комбінацій. Однак вони створюють домовленості в межах одного відра. Для цієї проблеми мені потрібні домовленості в декількох відрах.

Дивлячись на вихід із стандартного запиту "всі комбінації"

;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.

Я зазначив, що результати формували ту саму схему, що і Таблиця А. Здійснюючи спільний стрибок, розглядаючи кожен стовпець як Порядок 1 , значення говорять про те, який вантажівка буде приймати цей Орден, а ряд - це розташування ордерів у вантажних автомобілях. Потім запит стає

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.

Розширивши це, щоб охопити чотирнадцять порядків у прикладі даних та спростити імена, ми отримаємо це:

;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.

Я вирішу провести проміжні результати у тимчасових таблицях для зручності.

Подальші кроки стануть набагато простішими, якщо дані спочатку НЕ ПОВЕРНЕННІ.

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.

Ваги можна ввести, приєднавшись до таблиці Замовлення.

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

Тепер на це питання можна відповісти, знайшовши домовленості, які мають найменшу різницю між найбільш завантаженими та найменш завантаженими вантажівками

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

Обговорення

З цим існує дуже багато проблем. По-перше, це алгоритм грубої сили. Кількість рядків у робочих столах експоненціальна у кількості вантажних автомобілів та замовлень. Кількість рядків у # Домовленостях (кількість вантажівок) ^ (кількість замовлень). Це не буде добре масштабуватися.

По-друге, те, що запити SQL мають кількість вкладених у них замовлень. Єдиний спосіб цього - використовувати динамічний SQL, який має власні проблеми. Якщо кількість замовлень становить тисячі, може настати час, коли згенерований SQL стане занадто довгим.

Третє - це надмірність у домовленостях. Це сильно збільшує проміжні таблиці, збільшуючи час виконання.

По-четверте, багато рядків у # Домовленостях залишають порожнім один або кілька вантажівок. Це не може бути оптимальною конфігурацією. Було б легко відфільтрувати ці рядки під час створення. Я вирішив не робити цього, щоб зробити код простішим та зосередженим.

З верхньої сторони це обробляє негативні ваги, якщо ваше підприємство коли-небудь почне перевозити наповнені гелієм повітряні кулі!

Думки

Якби був спосіб заповнити #FilledTrucks безпосередньо зі списку вантажівок та замовлень, я думаю, що найгірше з цих проблем було б керованим. На жаль, моє уявлення натрапило на цю перешкоду. Я сподіваюсь, що якийсь майбутній співробітник, можливо, зможе поставити те, що мені ухиляється.




1 Ви говорите, що всі товари для замовлення повинні знаходитися на одній вантажівці. Це означає, що атомом присвоєння є Порядок, а не OrderDetail. Я створив це з ваших тестових даних таким чином:

select
    OrderId,
    Size = sum(OrderDetailSize)
into #Order
from #OrderDetail
group by OrderId;

Немає жодної різниці, однак, якщо ми позначимо питання, що виникають під питанням, "Порядок" чи "Замовлення", рішення залишається тим самим.


4

Дивлячись на ваші вимоги реального світу (які я припускаю, це спроба збалансувати ваше навантаження через набір процесорів) ...

Чи є причина, чому вам потрібно заздалегідь призначити процеси певним пакетам / процесору? [Намагання зрозуміти ваші реальні вимоги]

Для вашого прикладу "статистики оновлень", як ви знаєте, скільки часу займе певна операція? Що робити, якщо дана операція стикається з несподіваною затримкою (наприклад, більш ніж запланована / надмірна фрагментація таблиці / індексу, тривалий користувач txn блокує операцію "оновлення статистики")?


Для цілей збалансування навантаження я зазвичай генерую список завдань (наприклад, список таблиць для оновлення статистики) і розміщую зазначений список у (тимчасовій / подряпині) таблиці.

Структуру таблиці можна змінити відповідно до ваших вимог, наприклад:

create table tasks
(id        int             -- auto-increment?

,target    varchar(1000)   -- 'schema.table' to have stats updated, or perhaps ...
,command   varchar(1000)   -- actual command to be run, eg, 'update stats schema.table ... <options>'

,priority  int             -- provide means of ordering operations, eg, maybe you know some tasks will run really long so you want to kick them off first
,thread    int             -- identifier for parent process?
,start     datetime        -- default to NULL
,end       datetime        -- default to NULL
)

Далі я починаю X кількість одночасних процесів для виконання фактичних операцій "оновлення статистики", при цьому кожен процес виконує наступне:

  • розмістіть ексклюзивний замок на tasksстолі (гарантує, що завдання не буде знято більш ніж одним процесом; має бути відносно короткочасним блокуванням)
  • знайдіть рядок "перший", де start = NULL("перший" буде визначений вами, наприклад, замовити priority?)
  • оновити набір рядків start = getdate(), thread = <process_number>
  • зробити оновлення (і випустити ексклюзивний замок)
  • зробити помітку idта target/commandзначення
  • виконайте потрібну операцію проти target(alterntaively, run command) і по завершенні ...
  • оновлення tasksсend = getdate() where id = <id>
  • повторіть вище, поки не буде більше завдань для виконання

З вищенаведеним дизайном у мене зараз динамічно (в основному) збалансована робота.

ПРИМІТКИ:

  • Я намагаюся надати певний метод визначення пріоритетності, щоб я міг розпочати довші завдання, що працюють вперед; в той час як пара процесів працює над більш тривалими завданнями, інші процеси можуть переглядати список коротших виконання завдань
  • якщо процес стикається із незапланованою затримкою (наприклад, тривалий, блокуючий користувач txn), інші процеси можуть "забрати слабкий", продовжуючи витягувати операцію "наступна доступна" з tasks
  • дизайн tasksтаблиці повинен передбачати інші переваги, наприклад, історію часу запуску, яку можна архівувати для подальшої довідки, історію часу виконання, яку можна використовувати для зміни пріоритетів, надання статусу поточних операцій тощо
  • хоча "ексклюзивний замок" tasksможе здатися трохи надмірним, майте на увазі, що ми повинні планувати потенційну проблему 2 (або більше) процесів, які намагаються отримати нове завдання в той же самий час , тому нам потрібно гарантувати завдання призначений лише одному процесу (і так, ви можете отримати однакові результати за допомогою комбінованого оператора "update / select" - залежно від можливостей мови SQL вашого RDBMS); крок отримання нового "завдання" повинен бути швидким, тобто "ексклюзивний замок" повинен бути короткочасним, а насправді процеси будуть вражати tasksдосить випадковим чином, так що їх буде мало блокувати

Особисто мені здається, що цей tasksпроцес, керований таблицею, трохи простіший у здійсненні та підтримці ... на відміну від (зазвичай) більш складного процесу спроби попередньо призначити відображення завдань / процесів ... ymmv.


Очевидно, для вашого прикладного переконання, що ви не можете повернути ваші вантажні автомобілі до дистрибутива / складу для наступного замовлення, тому вам потрібно попередньо призначити свої замовлення на різні вантажні автомобілі (маючи на увазі, що UPS / Fedex / і т.д. також повинні призначати на основі маршрутів доставки, щоб скоротити терміни доставки та використання газу).

Однак у вашому прикладі реального світу ("оновлення статистики") немає причин, через які завдання / процес не можна виконувати динамічно, таким чином забезпечуючи кращі шанси на врівноваження робочого навантаження (через процесор і з точки зору скорочення загального часу виконання) .

ПРИМІТКА: Я звичайно бачу (ІТ) людей, які намагаються заздалегідь призначити свої завдання (як форму балансування навантаження) перед тим, як реально виконувати вказані завдання, і в кожному випадку він / він закінчується, щоб постійно змінювати процес попереднього призначення. враховуйте постійно різні завдання завдань (наприклад, рівень фрагментації таблиці / індексу, паралельна активність користувача тощо).


По-перше, якщо ми розглядаємо "порядок" як таблицю, а "orderdetail" як конкретну статистику на столі, то причиною нерозбиття є те, щоб уникнути блокування очікування між конкуруючими відрами. Traceflag 7471 призначений для усунення цієї проблеми, але в моєму тестуванні у мене все ще були проблеми з блокуванням.
Пол Холмс

Спочатку я сподівався зробити дуже легке рішення. Створіть відра як єдині багатоступінчасті SQL-блоки, а потім «запускайте і забудьте» кожне, використовуючи завдання, що саморуйнуються SQL Agent. тобто немає керування чергою. Однак згодом я виявив, що не міг легко виміряти обсяг роботи за статистикою - кількість рядків не скоротила її. Не дивно, що, враховуючи, що кількість рядків не відображає лінійно лінійну кількість вводу-виводу з однієї таблиці, або, дійсно, статичної, в іншу. Так, так, для цього додатка він справді може самоврівноважуватися, додаючи деяке активне управління чергою, як ви пропонуєте.
Пол Холмс

На ваш перший коментар ... так, все ще є (очевидно) рішення про деталізацію команд ... і питання одночасності на зразок: чи можна кілька команд виконувати паралельно і отримувати користь від їх комбінованого читання диска і т. Д. Але я все-таки знаходжу (дещо легке) динамічне керування чергою трохи ефективніше, ніж попереднє призначення відер :-) У вас є гарний набір відповідей / ідей, з якими можна працювати ... не повинно бути надто важким, щоб придумати рішення, яке забезпечує деяке пристойне врівноваження навантаження.
markp-fuso

1

створити та заповнити таблицю номерів за своїм бажанням. Це одноразове створення.

 create table tblnumber(number int not null)

    insert into tblnumber (number)
    select ROW_NUMBER()over(order by a.number) from master..spt_values a
    , master..spt_values b

    CREATE unique clustered index CI_num on tblnumber(number)

Створений стіл вантажівки

CREATE TABLE #PaulWhiteTruck (
Truckid int NOT NULL)

insert into #PaulWhiteTruck
values(113),(203),(303)

declare @PaulTruckCount int
Select @PaulTruckCount= count(*) from #PaulWhiteTruck

CREATE TABLE #OrderDetail (
id int identity(1,1),
OrderId int NOT NULL,
OrderDetailId int NOT NULL PRIMARY KEY,
OrderDetailSize int NOT NULL,
TruckId int NULL
)

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 )

Я створив одну OrderSummaryТаблицю

create table #orderSummary(id int identity(1,1),OrderId int ,TruckOrderSize int
,bit_value AS
CONVERT
(
integer,
POWER(2, id - 1)
)
PERSISTED UNIQUE CLUSTERED)
insert into #orderSummary
SELECT OrderId, SUM(OrderDetailSize) AS TruckOrderSize
FROM #OrderDetail GROUP BY OrderId

DECLARE @max integer =
POWER(2,
(
SELECT COUNT(*) FROM #orderSummary 
)
) - 1
declare @Delta int
select @Delta= max(TruckOrderSize)-min(TruckOrderSize)   from #orderSummary

Будь ласка, перевірте моє значення Delta і повідомте мене, якщо воно неправильне

;WITH cte 
     AS (SELECT n.number, 
                c.* 
         FROM   dbo.tblnumber AS N 
                CROSS apply (SELECT s.orderid, 
                                    s.truckordersize 
                             FROM   #ordersummary AS s 
                             WHERE  n.number & s.bit_value = s.bit_value) c 
         WHERE  N.number BETWEEN 1 AND @max), 
     cte1 
     AS (SELECT c.number, 
                Sum(truckordersize) SumSize 
         FROM   cte c 
         GROUP  BY c.number 
        --HAVING sum(TruckOrderSize) between(@Delta-25) and (@Delta+25) 
        ) 
SELECT c1.*, 
       c.orderid 
FROM   cte1 c1 
       INNER JOIN cte c 
               ON c1.number = c.number 
ORDER  BY sumsize 

DROP TABLE #orderdetail 

DROP TABLE #ordersummary 

DROP TABLE #paulwhitetruck 

Ви можете перевірити результат CTE1, він має все можливе Permutation and Combination of order along with their size.

Якщо мій підхід до цього часу правильний, тоді мені потрібна хтось допомога.

Завдання, що очікує:

відфільтруйте і розділіть результат CTE1на 3 частини ( Truck count), такий, що Orderidє унікальним серед кожної групи, і кожна частина T ruckOrderSizeзнаходиться поруч з Delta.


Перевірте мою останню відповідь. Я пропускаю один запит під час публікації, ніхто не вказав на мою помилку. Скопіюйте пасту і запустіть
KumarHarsh
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.