Voici un coup de poignard à un algorithme. Ce n'est pas parfait, et selon le temps que vous souhaitez passer à l'affiner, il y a probablement d'autres petits gains à faire.
Supposons que vous ayez une table de tâches à exécuter par quatre files d'attente. Vous connaissez la quantité de travail associée à l'exécution de chaque tâche et vous voulez que les quatre files d'attente obtiennent une quantité de travail presque égale, de sorte que toutes les files d'attente se termineront à peu près au même moment.
Tout d'abord, je partitionnerais les tâches en utilisant un module modulé, ordonné par leur taille, de petit à grand.
SELECT [time], ROW_NUMBER() OVER (ORDER BY [time])%4 AS grp, 0
Les ROW_NUMBER()
commandes chaque ligne par la taille, attribue ensuite un numéro de ligne, à partir de 1. Ce numéro de ligne est attribué un « groupe » (la grp
colonne) sur une base préliminaire ronde. La première rangée est le groupe 1, la deuxième rangée est le groupe 2, puis 3, la quatrième obtient le groupe 0, et ainsi de suite.
time ROW_NUMBER() grp
---- ------------ ---
1 1 1
10 2 2
12 3 3
15 4 0
19 5 1
22 6 2
...
Pour faciliter l'utilisation, je stocke les colonnes time
et grp
dans une variable de table appelée @work
.
Maintenant, nous pouvons effectuer quelques calculs sur ces données:
WITH cte AS (
SELECT *, SUM([time]) OVER (PARTITION BY grp)
-SUM([time]) OVER (PARTITION BY (SELECT NULL))/4 AS _grpoffset
FROM @work)
...
La colonne indique à _grpoffset
quel point le total time
par grp
diffère de la moyenne "idéale". Si le total time
de toutes les tâches est de 1000 et qu'il y a quatre groupes, il devrait idéalement y en avoir 250 dans chaque groupe. Si un groupe contient un total de 268, ce groupe est _grpoffset=18
.
L'idée est d'identifier les deux meilleures lignes, une dans un groupe «positif» (avec trop de travail) et une dans un groupe «négatif» (avec trop peu de travail). Si nous pouvons échanger des groupes sur ces deux lignes, nous pourrions réduire l'absolu _grpoffset
des deux groupes.
Exemple:
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
Avec un grand total de 727, chaque groupe devrait avoir un score d'environ 182 pour que la distribution soit parfaite. La différence entre le score du groupe et 182 est ce que nous mettons dans la _grpoffset
colonne.
Comme vous pouvez le voir maintenant, dans le meilleur des mondes, nous devrions déplacer environ 40 points de lignes du groupe 1 au groupe 2 et environ 24 points du groupe 3 au groupe 0.
Voici le code pour identifier ces lignes candidates:
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);
Je rejoins l'expression de table commune que nous avons créée auparavant cte
: d'un côté, les groupes avec un positif _grpoffset
, de l'autre côté les groupes avec des négatifs. Pour filtrer davantage les lignes supposées correspondre, l'échange des lignes des côtés positif et négatif doit s'améliorer _grpoffset
, c'est-à-dire le rapprocher de 0.
Le TOP 1
et ORDER BY
sélectionne la «meilleure» correspondance à permuter en premier.
Maintenant, tout ce que nous devons faire est d'ajouter un UPDATE
et de le boucler jusqu'à ce qu'il n'y ait plus d'optimisation à trouver.
TL; DR - voici la requête
Voici le code complet:
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);