web-dev-qa-db-ja.com

値で均等に分散されたグループに分割されたデータを選択します

グループ内の値の合計が可能な限り均等に分散されているテーブルからデータを4つのグループに選択したいと思います。私はそれを十分に明確に説明していないと確信しているので、例を挙げようと思います。

ここでは、NTILE(4)を使用して4つのグループを作成します。

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

上記のクエリと結果では、他の列は簡潔にするために省略されています。

したがって、次のようにグループを表示することもできます。

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

NTileを使用した時間の合計は、グループ間で実際にはバランスが取れていないことに注意してください。時間値のより良い分布は、例えば次のようになります:

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

ここでは、時間の合計が4つのグループに均等に分散されています。

TSQLステートメントを使用してこれをどのように実行できますか?

さらに、私はSQL Server 2012を使用していると言わなければなりません。私を助けることができるものがあれば、私に知らせてください。

よい一日を。

スタン

8
iStan

これがアルゴリズムのスタブです。それは完璧ではありません、そしてあなたがそれを精製することに費やしたいどのくらいの時間に依存して、作られるべきいくつかの更なる小さな利益があるでしょう。

4つのキューによって実行されるタスクのテーブルがあるとします。各タスクの実行に関連する作業量がわかっていて、4つのキューすべてにほぼ同じ量の作業を行わせたいので、すべてのキューがほぼ同時に完了します。

最初に、サイズの大きいものから小さいものへと順番に並べて、タスクを分割します。

_SELECT [time], ROW_NUMBER() OVER (ORDER BY [time])%4 AS grp, 0
_

ROW_NUMBER()は、すべての行をサイズで並べ替え、1から始まる行番号を割り当てます。この行番号には、ラウンドロビンベースで「グループ」(grp列)が割り当てられます。最初の行はグループ1、2番目の行はグループ2、次に3、4番目はグループ0と続きます。

_time ROW_NUMBER() grp
---- ------------ ---
   1            1   1
  10            2   2
  12            3   3
  15            4   0
  19            5   1
  22            6   2
...
_

使いやすくするために、time列とgrp列を_@work_というテーブル変数に格納しています。

これで、このデータに対していくつかの計算を実行できます。

_WITH cte AS (
    SELECT *, SUM([time]) OVER (PARTITION BY grp)
             -SUM([time]) OVER (PARTITION BY (SELECT NULL))/4 AS _grpoffset
    FROM @work)
...
_

列__grpoffset_は、timeあたりの合計grpが「理想的な」平均とどれだけ異なるかを示します。すべてのタスクの合計timeが1000で、4つのグループがある場合、理想的には、各グループで合計250になるはずです。グループに合計268が含まれる場合、そのグループは__grpoffset=18_です。

アイデアは、「ポジティブ」グループ(作業量が多すぎる)と「ネガティブ」グループ(作業量が少なすぎる)の2つの最適な行を識別することです。これらの2つの行でグループを交換できる場合、両方のグループの絶対__grpoffset_を減らすことができます。

例:

_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
_

総計が727であるため、各グループの分布が完璧になるには、約182のスコアが必要です。グループのスコアと182の違いは、__grpoffset_列に入力するものです。

ご覧のとおり、最高の世界では、グループ1からグループ2に約40ポイントの行を移動し、グループ3からグループ0に約24ポイントを移動する必要があります。

これらの候補行を識別するコードは次のとおりです。

_    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);
_

前に作成した共通テーブル式cteを自己結合しています。片側では、正の__grpoffset_のグループ、反対側では負のグループです。どの行が互いに一致すると想定されるかをさらにフィルターで除外するには、正側と負側の行の入れ替えにより__grpoffset_を改善する必要があります。つまり、0に近づけます。

_TOP 1_および_ORDER BY_は、最初にスワップする「最良の」一致を選択します。

ここで必要なことは、UPDATEを追加し、最適化が見つからなくなるまでループすることです。

TL; DR-これがクエリです

完全なコードは次のとおりです。

_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