web-dev-qa-db-fra.com

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

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

8
iStan

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 à effectuer par quatre files d'attente. Vous connaissez la quantité de travail associée à l'exécution de chaque tâche et vous souhaitez 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

La fonction ROW_NUMBER() classe chaque ligne par taille, puis attribue un numéro de ligne, commençant à 1. Ce numéro de ligne se voit attribuer un "groupe" (la colonne grp) sur une base de round robin. 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 _grpoffset Indique à 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 avoir un total de 250 dans chaque groupe. Si un groupe contient au total 268, _grpoffset=18 De ce groupe.

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 le _grpoffset Absolu 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 colonne _grpoffset.

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 moi-même l'expression de table commune que nous avons créée auparavant, cte: d'un côté, les groupes avec un _grpoffset Positif, de l'autre côté les groupes avec des négatifs. Pour filtrer davantage les lignes censées correspondre, l'échange des lignes des côtés positif et négatif doit améliorer _grpoffset, C'est-à-dire le rapprocher de 0.

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);
14
Daniel Hutmacher