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


8

グループ内の値の合計が可能な限り均等に分散されているテーブルからデータを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を使用していると言わなければなりません。私を助けることができるものがあれば、私に知らせてください。

よい一日を。

スタン


あなたの値は常に整数ですか?もしそうなら、それらは連続したシリーズですか、それともギャップがありますか?ユニークな値?
Daniel Hutmacher、2016

こんにちは、はい、それらは整数であり、それらは連続的ではありません。いくつかはおそらく2倍であり、それらの間に確実なギャップがあります。その特定のアイテムに対する操作を実行するのに必要な時間であると想像してください(特定のアイテムは省略された列です)。
iStan 2016

回答:


14

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

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列に入力するものです。

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

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

    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);
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.