Вот удар по алгоритму. Он не идеален, и в зависимости от того, сколько времени вы хотите потратить на его переработку, возможно, будут достигнуты еще некоторые небольшие выгоды.
Предположим, у вас есть таблица задач, которые должны быть выполнены четырьмя очередями. Вы знаете объем работы, связанной с выполнением каждой задачи, и хотите, чтобы все четыре очереди выполняли почти одинаковый объем работы, поэтому все очереди будут завершены примерно в одно и то же время.
Прежде всего, я бы разделил задачи, используя модульную, упорядоченную по размеру, от малого до большого.
SELECT [time], ROW_NUMBER() OVER (ORDER BY [time])%4 AS grp, 0
Эти ROW_NUMBER()заказы каждый ряд по размеру, а затем назначает номер строки, начиная с 1. Этот номер строки присваивается «группа» (The grpстолбца) на круговом основе. Первый ряд - это группа 1, второй ряд - это группа 2, затем 3, четвертый - группа 0 и т. Д.
time ROW_NUMBER() grp
---- ------------ ---
1 1 1
10 2 2
12 3 3
15 4 0
19 5 1
22 6 2
...
Для простоты использования, я храню timeи grpстолбцов в табличной переменной называется @work.
Теперь мы можем выполнить несколько расчетов по этим данным:
WITH cte AS (
SELECT *, SUM([time]) OVER (PARTITION BY grp)
-SUM([time]) OVER (PARTITION BY (SELECT NULL))/4 AS _grpoffset
FROM @work)
...
Колонка _grpoffsetсколько общее timeв grpотличается от «идеального» среднего. Если общее timeколичество всех заданий равно 1000 и имеется четыре группы, в идеале должно быть в общей сложности 250 в каждой группе. Если группа содержит в общей сложности 268, эта группа _grpoffset=18.
Идея состоит в том, чтобы определить две лучшие строки: одну в «положительной» группе (с большим количеством работы) и одну в «отрицательной» группе (с небольшим количеством работы). Если бы мы могли поменять местами группы в этих двух строках, мы могли бы уменьшить абсолютное значение _grpoffsetдля обеих групп.
Пример:
time grp total _grpoffset
---- --- ----- ----------
3 1 222 40
46 1 222 40
73 1 222 40
100 1 222 40
6 2 134 -48
52 2 134 -48
76 2 134 -48
11 3 163 -21
66 3 163 -21
86 3 163 -21
45 0 208 24
71 0 208 24
92 0 208 24
----
=727
При общей сумме в 727 баллов каждая группа должна набрать около 182 баллов, чтобы распределение было идеальным. Разница между оценкой группы и 182 - это то, что мы помещаем в _grpoffsetколонку.
Как вы можете видеть сейчас, в лучшем из миров мы должны переместить ряды примерно на 40 баллов из группы 1 в группу 2 и около 24 баллов из группы 3 в группу 0.
Вот код для идентификации этих строк-кандидатов:
SELECT TOP 1 pos._row AS _pos_row, pos.grp AS _pos_grp,
neg._row AS _neg_row, neg.grp AS _neg_grp
FROM cte AS pos
INNER JOIN cte AS neg ON
pos._grpoffset>0 AND
neg._grpoffset<0 AND
--- To prevent infinite recursion:
pos.moved<4 AND
neg.moved<4
WHERE --- must improve positive side's offset:
ABS(pos._grpoffset-pos.[time]+neg.[time])<=pos._grpoffset AND
--- must improve negative side's offset:
ABS(neg._grpoffset-neg.[time]+pos.[time])<=ABS(neg._grpoffset)
--- Largest changes first:
ORDER BY ABS(pos.[time]-neg.[time]) DESC
) AS x ON w._row IN (x._pos_row, x._neg_row);
Я сам присоединяюсь к общему табличному выражению, которое мы создали ранее cte: с одной стороны, группы с положительными _grpoffset, с другой стороны - с отрицательными. Чтобы дополнительно отфильтровать, какие строки должны соответствовать друг другу, своп положительных и отрицательных сторон должен улучшиться _grpoffset, то есть приблизиться к 0.
Параметр TOP 1и ORDER BYвыбирает «лучшее» совпадение для замены в первую очередь.
Теперь все, что нам нужно, это добавить UPDATEи зациклить его, пока не будет больше никакой оптимизации.
TL; DR - вот запрос
Вот полный код:
DECLARE @work TABLE (
_row int IDENTITY(1, 1) NOT NULL,
[time] int NOT NULL,
grp int NOT NULL,
moved tinyint NOT NULL,
PRIMARY KEY CLUSTERED ([time], _row)
);
WITH cte AS (
SELECT 0 AS n, CAST(1+100*RAND(CHECKSUM(NEWID())) AS int) AS [time]
UNION ALL
SELECT n+1, CAST(1+100*RAND(CHECKSUM(NEWID())) AS int) AS [time]
FROM cte WHERE n<100)
INSERT INTO @work ([time], grp, moved)
SELECT [time], ROW_NUMBER() OVER (ORDER BY [time])%4 AS grp, 0
FROM cte;
WHILE (@@ROWCOUNT!=0)
WITH cte AS (
SELECT *, SUM([time]) OVER (PARTITION BY grp)
-SUM([time]) OVER (PARTITION BY (SELECT NULL))/4 AS _grpoffset
FROM @work)
UPDATE w
SET w.grp=(CASE w._row
WHEN x._pos_row THEN x._neg_grp
ELSE x._pos_grp END),
w.moved=w.moved+1
FROM @work AS w
INNER JOIN (
SELECT TOP 1 pos._row AS _pos_row, pos.grp AS _pos_grp,
neg._row AS _neg_row, neg.grp AS _neg_grp
FROM cte AS pos
INNER JOIN cte AS neg ON
pos._grpoffset>0 AND
neg._grpoffset<0 AND
--- To prevent infinite recursion:
pos.moved<4 AND
neg.moved<4
WHERE --- must improve positive side's offset:
ABS(pos._grpoffset-pos.[time]+neg.[time])<=pos._grpoffset AND
--- must improve negative side's offset:
ABS(neg._grpoffset-neg.[time]+pos.[time])<=ABS(neg._grpoffset)
--- Largest changes first:
ORDER BY ABS(pos.[time]-neg.[time]) DESC
) AS x ON w._row IN (x._pos_row, x._neg_row);