Sélectionnez les données divisées en groupes uniformément répartis par valeur

8

Je voudrais sélectionner en 4 groupes les données d'une table ayant la somme des valeurs dans les groupes aussi uniformément réparties que possible. Je suis sûr que je ne l'explique pas assez clairement, je vais donc essayer de donner un exemple.

Ici, j'utilise NTILE (4) pour créer les 4 groupes:

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

Dans la requête et le résultat ci-dessus, les autres colonnes ont été omises par souci de concision.

Vous pouvez donc voir les groupes également comme suit:

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

Notez que la somme des totaux de temps utilisant NTile n'est pas vraiment équilibrée entre les groupes. Une meilleure distribution des valeurs de temps serait par exemple:

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

Ici, la somme des totaux de temps est répartie de manière plus égale sur les 4 groupes.

Comment puis-je effectuer cela via une instruction TSQL?

De plus, je dois dire que j'utilise SQL Server 2012. Si vous avez quelque chose qui peut m'aider, faites le moi savoir.

Je vous souhaite une bonne journée.

Stan

iStan
la source
Vos valeurs sont-elles toujours des entiers? Si oui, sont-ils dans une série continue ou peut-il y avoir des lacunes? Des valeurs uniques?
Daniel Hutmacher
Salut, oui, ce sont des nombres entiers, et non ils ne sont pas continus, certains peuvent être doubles et sûrs qu'il y a des écarts entre eux. Imaginez-les que c'est le temps nécessaire pour effectuer une opération pour cet élément particulier (l'élément particulier est une colonne omise).
iStan

Réponses:

14

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 grpcolonne) 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 timeet grpdans 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 à _grpoffsetquel point le total timepar grpdiffère de la moyenne "idéale". Si le total timede 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 _grpoffsetdes 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 _grpoffsetcolonne.

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 1et ORDER BYsélectionne la «meilleure» correspondance à permuter en premier.

Maintenant, tout ce que nous devons faire est d'ajouter un UPDATEet 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);
Daniel Hutmacher
la source