Seleziona i dati divisi in gruppi distribuiti uniformemente per valore


8

Vorrei selezionare in 4 gruppi i dati da una tabella con la somma di valori nei gruppi il più uniformemente possibile distribuiti. Sono sicuro di non spiegarlo abbastanza chiaramente, quindi cercherò di fare un esempio.

Qui uso NTILE (4) per creare i 4 gruppi:

SELECT Time, NTILE(4) OVER (ORDER BY Time DESC) AS N FROM TableX

Time -  N
-------------
10  -   1
 9  -   2
 8  -   3
 7  -   4
 6  -   1
 5  -   2
 4  -   3
 3  -   4
 2  -   1
 1  -   2

Nella query e nel risultato precedenti, le altre colonne sono state omesse per brevità.

Quindi puoi vedere i gruppi anche come segue:

  1    2    3    4
---  ---  ---  ---
 10    9    8    7
  6    5    4    3
  2    1    
---  ---  ---  ---
 18   15   12   10  Sum Totals of Time

Si noti che la somma dei totali del tempo usando NTile non è realmente bilanciata tra i gruppi. Una migliore distribuzione dei valori temporali sarebbe ad esempio:

  1    2    3    4
---  ---  ---  ---
 10    9    8    7
  3    5    4    6
  1         2
---  ---  ---  ---
 14   14   14   13  Sum Totals of Time

Qui la somma dei totali del tempo è distribuita più uniformemente sui 4 gruppi.

Come posso eseguire ciò tramite istruzioni TSQL?

Inoltre devo dire che sto usando SQL Server 2012. Se hai qualcosa che mi può aiutare, fammi sapere.

Vi auguro una buona giornata.

Stan


I tuoi valori sono sempre numeri interi? In tal caso, sono in una serie continua o possono esserci delle lacune? Valori unici?
Daniel Hutmacher,

Salve, sì, sono numeri interi e no, non sono continui, alcuni vuoti forse doppi e sicuri sono tra loro. Immagina che è il tempo necessario per eseguire un'operazione per quel particolare oggetto (l'elemento particolare è una colonna omessa).
iStan

Risposte:


14

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 grpcolonna) 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 timee grpin 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 timeper ogni grpdiverso dal "ideale" media. Se il totale timedi 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 _grpoffsetdi 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 _grpoffsetcolonna.

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 1e ORDER BYseleziona la corrispondenza "migliore" da scambiare per prima.

Ora, tutto ciò che dobbiamo fare è aggiungere un UPDATEe 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);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.