Ecco una pugnalata ad un algoritmo. Non è perfetto e, a seconda di quanto tempo vuoi impiegare per perfezionarlo, probabilmente ci sono altri piccoli guadagni da fare.
Supponiamo che tu abbia una tabella di attività che devono essere eseguite da quattro code. Conosci la quantità di lavoro associata all'esecuzione di ciascuna attività e desideri che tutte e quattro le code ottengano una quantità quasi uguale di lavoro, quindi tutte le code verranno completate all'incirca nello stesso momento.
Prima di tutto, partizionerei i compiti usando un modulato, ordinato in base alle loro dimensioni, da piccolo a grande.
SELECT [time], ROW_NUMBER() OVER (ORDER BY [time])%4 AS grp, 0
Gli ROW_NUMBER()
ordini ogni riga per dimensione, quindi assegna un numero di riga, partendo da 1. Questo numero di riga viene assegnato un "gruppo" (la grp
colonna) su base round-robin. La prima riga è il gruppo 1, la seconda riga è il gruppo 2, quindi 3, la quarta ottiene il gruppo 0 e così via.
time ROW_NUMBER() grp
---- ------------ ---
1 1 1
10 2 2
12 3 3
15 4 0
19 5 1
22 6 2
...
Per facilità d'uso, sto memorizzando le colonne time
e grp
in una variabile di tabella chiamata @work
.
Ora, possiamo eseguire alcuni calcoli su questi dati:
WITH cte AS (
SELECT *, SUM([time]) OVER (PARTITION BY grp)
-SUM([time]) OVER (PARTITION BY (SELECT NULL))/4 AS _grpoffset
FROM @work)
...
La colonna _grpoffset
è quanto il totale time
per ogni grp
diverso dal "ideale" media. Se il totale time
di tutte le attività è 1000 e ci sono quattro gruppi, idealmente dovrebbe esserci un totale di 250 in ciascun gruppo. Se un gruppo contiene un totale di 268, quello di quel gruppo _grpoffset=18
.
L'idea è di identificare le due righe migliori, una in un gruppo "positivo" (con troppo lavoro) e una in un gruppo "negativo" (con troppo poco lavoro). Se possiamo scambiare gruppi su quelle due file, potremmo ridurre l'assoluto _grpoffset
di entrambi i gruppi.
Esempio:
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
Con un totale complessivo di 727, ogni gruppo dovrebbe avere un punteggio di circa 182 affinché la distribuzione sia perfetta. La differenza tra il punteggio del gruppo e 182 è ciò che stiamo inserendo nella _grpoffset
colonna.
Come puoi vedere ora, nel migliore dei mondi, dovremmo spostare circa 40 punti di righe dal gruppo 1 al gruppo 2 e circa 24 punti dal gruppo 3 al gruppo 0.
Ecco il codice per identificare quelle righe candidate:
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);
Mi unisco all'espressione comune della tabella che abbiamo creato prima cte
: da un lato, i gruppi con un positivo _grpoffset
, dall'altro i gruppi con quelli negativi. Per filtrare ulteriormente quali file devono corrispondere, è necessario migliorare lo scambio delle file dei lati positivo e negativo _grpoffset
, ovvero avvicinarlo a 0.
Il TOP 1
e ORDER BY
seleziona la corrispondenza "migliore" da scambiare per prima.
Ora, tutto ciò che dobbiamo fare è aggiungere un UPDATE
e farlo scorrere fino a quando non c'è più ottimizzazione da trovare.
TL; DR: ecco la domanda
Ecco il codice completo:
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);