Выберите данные, разделенные на группы, равномерно распределенные по значению

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. Если у вас есть что-то, что может мне помочь, дайте мне знать.

Хорошего дня.

Стан

Istán
источник
Ваши значения всегда целые? Если это так, они в непрерывном ряду или могут быть пробелы? Уникальные ценности?
Даниэль Хутмахер
Привет, да, они являются целыми числами, и нет, они не являются непрерывными, некоторые могут быть двойными, и между ними есть пробелы. Представьте себе, что это время, необходимое для выполнения операции с этим конкретным элементом (конкретный элемент является пропущенным столбцом).
Istán

Ответы:

14

Вот удар по алгоритму. Он не идеален, и в зависимости от того, сколько времени вы хотите потратить на его переработку, возможно, будут достигнуты еще некоторые небольшие выгоды.

Предположим, у вас есть таблица задач, которые должны быть выполнены четырьмя очередями. Вы знаете объем работы, связанной с выполнением каждой задачи, и хотите, чтобы все четыре очереди выполняли почти одинаковый объем работы, поэтому все очереди будут завершены примерно в одно и то же время.

Прежде всего, я бы разделил задачи, используя модульную, упорядоченную по размеру, от малого до большого.

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

Эти ROW_NUMBER()заказы каждый ряд по размеру, а затем назначает номер строки, начиная с 1. Этот номер строки присваивается «группа» (The grpстолбца) на круговом основе. Первый ряд - это группа 1, второй ряд - это группа 2, затем 3, четвертый - группа 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 и имеется четыре группы, в идеале должно быть в общей сложности 250 в каждой группе. Если группа содержит в общей сложности 268, эта группа _grpoffset=18.

Идея состоит в том, чтобы определить две лучшие строки: одну в «положительной» группе (с большим количеством работы) и одну в «отрицательной» группе (с небольшим количеством работы). Если бы мы могли поменять местами группы в этих двух строках, мы могли бы уменьшить абсолютное значение _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);
Даниэль Хутмахер
источник