Сброс промежуточного итога на основе другого столбца

10

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

create table #reset_runn_total
(
id int identity(1,1),
val int, 
reset_val int,
grp int
)

insert into #reset_runn_total
values 
(1,10,1),
(8,12,1),(6,14,1),(5,10,1),(6,13,1),(3,11,1),(9,8,1),(10,12,1)


SELECT Row_number()OVER(partition BY grp ORDER BY id)AS rn,*
INTO   #test
FROM   #reset_runn_total

Детали индекса:

CREATE UNIQUE CLUSTERED INDEX ix_load_reset_runn_total
  ON #test(rn, grp) 

образец данных

+----+-----+-----------+-----+
| id | val | reset_val | Grp |
+----+-----+-----------+-----+
|  1 |   1 |        10 | 1   |
|  2 |   8 |        12 | 1   |
|  3 |   6 |        14 | 1   |
|  4 |   5 |        10 | 1   |
|  5 |   6 |        13 | 1   |
|  6 |   3 |        11 | 1   |
|  7 |   9 |         8 | 1   |
|  8 |  10 |        12 | 1   |
+----+-----+-----------+-----+ 

Ожидаемый результат

+----+-----+-----------------+-------------+
| id | val |    reset_val    | Running_tot |
+----+-----+-----------------+-------------+
|  1 |   1 | 10              |       1     |  
|  2 |   8 | 12              |       9     |  --1+8
|  3 |   6 | 14              |       15    |  --1+8+6 -- greater than reset val
|  4 |   5 | 10              |       5     |  --reset 
|  5 |   6 | 13              |       11    |  --5+6
|  6 |   3 | 11              |       14    |  --5+6+3 -- greater than reset val
|  7 |   9 | 8               |       9     |  --reset -- greater than reset val 
|  8 |  10 | 12              |      10     |  --reset
+----+-----+-----------------+-------------+

Запрос:

Я получил результат, используя Recursive CTE. Оригинальный вопрос здесь /programming/42085404/reset-running-total-based-on-another-column

;WITH cte
     AS (SELECT rn,id,
                val,
                reset_val,
                grp,
                val                   AS running_total,
                Iif (val > reset_val, 1, 0) AS flag
         FROM   #test
         WHERE  rn = 1
         UNION ALL
         SELECT r.*,
                Iif(c.flag = 1, r.val, c.running_total + r.val),
                Iif(Iif(c.flag = 1, r.val, c.running_total + r.val) > r.reset_val, 1, 0)
         FROM   cte c
                JOIN #test r
                  ON r.grp = c.grp
                     AND r.rn = c.rn + 1)
SELECT *
FROM   cte 

Есть ли лучшая альтернатива T-SQLбез использования CLR.?

P ரதீப்
источник
Лучше как? Этот запрос демонстрирует низкую производительность? Используя какие метрики?
Аарон Бертран
@AaronBertrand - Для лучшего понимания я разместил образцы данных только для одной группы. Я должен сделать то же самое для 50000групп с 60 идентификаторами . так что общее количество записей будет около 3000000. Уверена Recursive CTEне будет хорошо масштабироваться 3000000. Обновлю метрики, когда вернусь в офис. Можем ли мы добиться этого, используя, sum()Over(Order by)как вы использовали в этой статье sqlperformance.com/2012/07/t-sql-queries/running-totals
P P
Курсор может работать лучше, чем рекурсивный CTE
папараццо
2
К вашему сведению ... все еще активный элемент подключения - добавьте пункт СБРОС КОГДА, как в Teradata, для перезапуска раздела окна в оконных функциях - Ицик Бен-Ган
Кин Шах

Ответы:

6

Я смотрел на подобные проблемы и никогда не мог найти решение оконной функции, которое делает один проход по данным. Я не думаю, что это возможно. Оконные функции должны быть в состоянии применяться ко всем значениям в столбце. Это делает такие вычисления сброса очень сложными, потому что один сброс изменяет значение для всех следующих значений.

Один из способов решения проблемы заключается в том, что вы можете получить желаемый конечный результат, если вычисляете базовую промежуточную сумму, если вы можете вычесть промежуточную сумму из правильной предыдущей строки. Например, в ваших данных образца значение для id4 являетсяrunning total of row 4 - the running total of row 3 . Значение id6 - это running total of row 6 - the running total of row 3потому что сброс еще не произошел. Значение для id7 - это running total of row 7 - the running total of row 6и так далее.

Я хотел бы подойти к этому с T-SQL в цикле. Я немного увлекся и думаю, что у меня есть полное решение. Для 3 миллионов строк и 500 групп код завершился за 24 секунды на моем рабочем столе. Я тестирую с SQL Server 2016 для разработчиков с 6 vCPU. Я использую преимущества параллельных вставок и параллельного выполнения в целом, поэтому вам может потребоваться изменить код, если вы используете более старую версию или имеете ограничения DOP.

Ниже код, который я использовал для генерации данных. Диапазоны на VALиRESET_VAL должны быть аналогичны вашим образцам данных.

drop table if exists reset_runn_total;

create table reset_runn_total
(
id int identity(1,1),
val int, 
reset_val int,
grp int
);

DECLARE 
@group_num INT,
@row_num INT;
BEGIN
    SET NOCOUNT ON;
    BEGIN TRANSACTION;

    SET @group_num = 1;
    WHILE @group_num <= 50000 
    BEGIN
        SET @row_num = 1;
        WHILE @row_num <= 60
        BEGIN
            INSERT INTO reset_runn_total WITH (TABLOCK)
            SELECT 1 + ABS(CHECKSUM(NewId())) % 10, 8 + ABS(CHECKSUM(NewId())) % 8, @group_num;

            SET @row_num = @row_num + 1;
        END;
        SET @group_num = @group_num + 1;
    END;
    COMMIT TRANSACTION;
END;

Алгоритм выглядит следующим образом:

1) Начните с вставки всех строк со стандартным промежуточным итогом во временную таблицу.

2) В цикле:

2a) Для каждой группы вычислите первую строку с промежуточной суммой выше оставшегося в таблице значения reset_value и сохраните идентификатор, промежуточную сумму, которая была слишком большой, и предыдущую промежуточную сумму, которая была слишком большой, во временной таблице.

2b) Удалить строки из первой временной таблицы во временную таблицу результатов, которые имеют значение, IDменьшее или равное IDзначению второй временной таблицы. Используйте другие столбцы для корректировки промежуточного итога по мере необходимости.

3) После удаления больше не обрабатываемых строк запускаются дополнительные DELETE OUTPUT в таблицу результатов. Это для строк в конце группы, которые никогда не превышают значение сброса.

Я покажу пошаговую реализацию одного из описанных выше алгоритмов в T-SQL.

Начните с создания нескольких временных таблиц. #initial_resultsсодержит исходные данные со стандартным промежуточным итогом, #group_bookkeepingобновляет каждый цикл, чтобы выяснить, какие строки могут быть перемещены, и #final_resultsсодержит результаты с промежуточным итогом, откорректированным для сброса.

CREATE TABLE #initial_results (
id int,
val int, 
reset_val int,
grp int,
initial_running_total int
);

CREATE TABLE #group_bookkeeping (
grp int,
max_id_to_move int,
running_total_to_subtract_this_loop int,
running_total_to_subtract_next_loop int,
grp_done bit, 
PRIMARY KEY (grp)
);

CREATE TABLE #final_results (
id int,
val int, 
reset_val int,
grp int,
running_total int
);

INSERT INTO #initial_results WITH (TABLOCK)
SELECT ID, VAL, RESET_VAL, GRP, SUM(VAL) OVER (PARTITION BY GRP ORDER BY ID) RUNNING_TOTAL
FROM reset_runn_total;

CREATE CLUSTERED INDEX i1 ON #initial_results (grp, id);

INSERT INTO #group_bookkeeping WITH (TABLOCK)
SELECT DISTINCT GRP, 0, 0, 0, 0
FROM reset_runn_total;

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

Приведенный ниже код запускается в цикле и обновляет таблицу учета. Для каждой группы нам нужно найти максимальное значение, IDкоторое следует переместить в таблицу результатов. Нам нужна промежуточная сумма из этой строки, чтобы мы могли вычесть ее из начальной промежуточной суммы. grp_doneСтолбец устанавливается в 1 , если не больше работы , чтобы сделать для grp.

WITH UPD_CTE AS (
        SELECT 
        #grp_bookkeeping.GRP
        , MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN ID ELSE NULL END) max_id_to_update
        , MIN(#group_bookkeeping.running_total_to_subtract_next_loop) running_total_to_subtract_this_loop
        , MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN initial_running_total ELSE NULL END) additional_value_next_loop
        , CASE WHEN MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN ID ELSE NULL END) IS NULL THEN 1 ELSE 0 END grp_done
        FROM #group_bookkeeping 
        INNER JOIN #initial_results IR ON #group_bookkeeping.grp = ir.grp
        WHERE #group_bookkeeping.grp_done = 0
        GROUP BY #group_bookkeeping.GRP
    )
    UPDATE #group_bookkeeping
    SET #group_bookkeeping.max_id_to_move = uv.max_id_to_update
    , #group_bookkeeping.running_total_to_subtract_this_loop = uv.running_total_to_subtract_this_loop
    , #group_bookkeeping.running_total_to_subtract_next_loop = uv.additional_value_next_loop
    , #group_bookkeeping.grp_done = uv.grp_done
    FROM UPD_CTE uv
    WHERE uv.GRP = #group_bookkeeping.grp
OPTION (LOOP JOIN);

На самом деле не фанат LOOP JOINподсказки в целом, но это простой запрос, и это был самый быстрый способ получить то, что я хотел. Чтобы действительно оптимизировать время отклика, я хотел соединений с параллельными вложенными циклами вместо объединений DOP 1.

Приведенный ниже код выполняется в цикле и перемещает данные из исходной таблицы в таблицу окончательных результатов. Обратите внимание на корректировку начального промежуточного итога.

DELETE ir
OUTPUT DELETED.id,  
    DELETED.VAL,  
    DELETED.RESET_VAL,  
    DELETED.GRP ,
    DELETED.initial_running_total - tb.running_total_to_subtract_this_loop
INTO #final_results
FROM #initial_results ir
INNER JOIN #group_bookkeeping tb ON ir.GRP = tb.GRP AND ir.ID <= tb.max_id_to_move
WHERE tb.grp_done = 0;

Для вашего удобства ниже приведен полный код:

DECLARE @RC INT;
BEGIN
SET NOCOUNT ON;

CREATE TABLE #initial_results (
id int,
val int, 
reset_val int,
grp int,
initial_running_total int
);

CREATE TABLE #group_bookkeeping (
grp int,
max_id_to_move int,
running_total_to_subtract_this_loop int,
running_total_to_subtract_next_loop int,
grp_done bit, 
PRIMARY KEY (grp)
);

CREATE TABLE #final_results (
id int,
val int, 
reset_val int,
grp int,
running_total int
);

INSERT INTO #initial_results WITH (TABLOCK)
SELECT ID, VAL, RESET_VAL, GRP, SUM(VAL) OVER (PARTITION BY GRP ORDER BY ID) RUNNING_TOTAL
FROM reset_runn_total;

CREATE CLUSTERED INDEX i1 ON #initial_results (grp, id);

INSERT INTO #group_bookkeeping WITH (TABLOCK)
SELECT DISTINCT GRP, 0, 0, 0, 0
FROM reset_runn_total;

SET @RC = 1;
WHILE @RC > 0 
BEGIN
    WITH UPD_CTE AS (
        SELECT 
        #group_bookkeeping.GRP
        , MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN ID ELSE NULL END) max_id_to_move
        , MIN(#group_bookkeeping.running_total_to_subtract_next_loop) running_total_to_subtract_this_loop
        , MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN initial_running_total ELSE NULL END) additional_value_next_loop
        , CASE WHEN MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN ID ELSE NULL END) IS NULL THEN 1 ELSE 0 END grp_done
        FROM #group_bookkeeping 
        CROSS APPLY (SELECT ID, RESET_VAL, initial_running_total FROM #initial_results ir WHERE #group_bookkeeping.grp = ir.grp ) ir
        WHERE #group_bookkeeping.grp_done = 0
        GROUP BY #group_bookkeeping.GRP
    )
    UPDATE #group_bookkeeping
    SET #group_bookkeeping.max_id_to_move = uv.max_id_to_move
    , #group_bookkeeping.running_total_to_subtract_this_loop = uv.running_total_to_subtract_this_loop
    , #group_bookkeeping.running_total_to_subtract_next_loop = uv.additional_value_next_loop
    , #group_bookkeeping.grp_done = uv.grp_done
    FROM UPD_CTE uv
    WHERE uv.GRP = #group_bookkeeping.grp
    OPTION (LOOP JOIN);

    DELETE ir
    OUTPUT DELETED.id,  
        DELETED.VAL,  
        DELETED.RESET_VAL,  
        DELETED.GRP ,
        DELETED.initial_running_total - tb.running_total_to_subtract_this_loop
    INTO #final_results
    FROM #initial_results ir
    INNER JOIN #group_bookkeeping tb ON ir.GRP = tb.GRP AND ir.ID <= tb.max_id_to_move
    WHERE tb.grp_done = 0;

    SET @RC = @@ROWCOUNT;
END;

DELETE ir 
OUTPUT DELETED.id,  
    DELETED.VAL,  
    DELETED.RESET_VAL,  
    DELETED.GRP ,
    DELETED.initial_running_total - tb.running_total_to_subtract_this_loop
    INTO #final_results
FROM #initial_results ir
INNER JOIN #group_bookkeeping tb ON ir.GRP = tb.GRP;

CREATE CLUSTERED INDEX f1 ON #final_results (grp, id);

/* -- do something with the data
SELECT *
FROM #final_results
ORDER BY grp, id;
*/

DROP TABLE #final_results;
DROP TABLE #initial_results;
DROP TABLE #group_bookkeeping;

END;
Джо Оббиш
источник
просто потрясающе, я
награжу
На нашем сервере для 50000 и 60 идентификаторов у вас ушло 1 минута и 10 секунд. Recursive CTEзаняло 2 минуты 15 секунд
P ரதீப்
Я проверил оба кода с одинаковыми данными. Твой был потрясающим. Можно ли его еще улучшить?
P ரதீப்
Я имел в виду, я запустил ваш код на наших реальных данных и проверил его. В моей реальной процедуре вычисления обрабатываются во временных таблицах, скорее всего, они должны быть плотно упакованы. Будет хорошо, если его можно будет уменьшить до 30 секунд
P
@Prdp Пробовал быстрый подход, который использовал обновление, но, казалось, было хуже. Не буду больше в этом разбираться. Попробуйте записать, сколько времени занимает каждая операция, чтобы определить, какая часть работает медленнее всего на вашем сервере. Вполне возможно, что есть способ ускорить этот код или лучший алгоритм в целом.
Джо Оббиш
4

Используя КУРСОР:

ALTER TABLE #reset_runn_total ADD RunningTotal int;

DECLARE @id int, @val int, @reset int, @acm int, @grp int, @last_grp int;
SET @acm = 0;

DECLARE curRes CURSOR FAST_FORWARD FOR 
SELECT id, val, reset_val, grp
FROM #reset_runn_total
ORDER BY grp, id;

OPEN curRes;
FETCH NEXT FROM curRes INTO @id, @val, @reset, @grp;
SET @last_grp = @grp;

WHILE @@FETCH_STATUS = 0  
BEGIN
    IF @grp <> @last_grp SET @acm = 0;
    SET @last_grp = @grp;
    SET @acm = @acm + @val;
    UPDATE #reset_runn_total
    SET RunningTotal = @acm
    WHERE id = @id;
    IF @acm > @reset SET @acm = 0;
    FETCH NEXT FROM curRes INTO @id, @val, @reset, @grp;
END

CLOSE curRes;
DEALLOCATE curRes;

+----+-----+-----------+-------------+
| id | val | reset_val | RunningTotal|
+----+-----+-----------+-------------+
| 1  | 1   | 10        |     1       |
+----+-----+-----------+-------------+
| 2  | 8   | 12        |     9       |
+----+-----+-----------+-------------+
| 3  | 6   | 14        |     15      |
+----+-----+-----------+-------------+
| 4  | 5   | 10        |     5       |
+----+-----+-----------+-------------+
| 5  | 6   | 13        |     11      |
+----+-----+-----------+-------------+
| 6  | 3   | 11        |     14      |
+----+-----+-----------+-------------+
| 7  | 9   | 8         |     9       |
+----+-----+-----------+-------------+
| 8  | 10  | 12        |     10      |
+----+-----+-----------+-------------+

Проверьте здесь: http://rextester.com/WSPLO95303

McNets
источник
3

Не оконная, а чистая версия SQL:

WITH x AS (
    SELECT TOP 1 id,
           val,
           reset_val,
           val AS running_total,
           1 AS level 
      FROM reset_runn_total
    UNION ALL
    SELECT r.id,
           r.val,
           r.reset_val,
           CASE WHEN x.running_total < x.reset_val THEN x.running_total + r.val ELSE r.val END,
           level = level + 1
      FROM x JOIN reset_runn_total AS r ON (r.id > x.id)
) SELECT
  *
FROM x
WHERE NOT EXISTS (
        SELECT 1
        FROM x AS x2
        WHERE x2.id = x.id
        AND x2.level > x.level
    )
ORDER BY id, level DESC
;

Я не специалист по диалекту SQL Server. Это начальная версия для PostrgreSQL (если я правильно понимаю, я не могу использовать LIMIT 1 / TOP 1 в рекурсивной части в SQL Server):

WITH RECURSIVE x AS (
    (SELECT id, val, reset_val, val AS running_total
       FROM reset_runn_total
      ORDER BY id
      LIMIT 1)
    UNION
    (SELECT r.id, r.val, r.reset_val,
            CASE WHEN x.running_total < x.reset_val THEN x.running_total + r.val ELSE r.val END
       FROM x JOIN reset_runn_total AS r ON (r.id > x.id)
      ORDER BY id
      LIMIT 1)
) SELECT * FROM x;
Роман Ткачук
источник
@JoeObbish, честно говоря, это не совсем понятно из вопроса. Например, ожидаемые результаты не показывают grpстолбец.
ypercubeᵀᴹ
@JoeObbish, это то, что я тоже понял. тем не менее, вопрос мог бы выиграть от явного заявления об этом. Код в вопросе (с CTE) также не использует его (и даже имеет по-разному именованные столбцы). Это было бы очевидно для любого, кто читает вопрос - они не будут - и не должны - читать другие ответы или комментарии.
ypercubeᵀᴹ
@ ypercubeᵀᴹ Добавлена ​​необходимая информация по вопросу.
P ரதீப்
1

Кажется, у вас есть несколько запросов / методов для решения проблемы, но вы не предоставили нам - или даже не рассмотрели? - индексы на столе.

Какие показатели есть в таблице? Это куча или кластерный индекс?

Я бы попробовал различные решения, предложенные после добавления этого индекса:

(grp, id) INCLUDE (val, reset_val)

Или просто измените (или сделайте) кластерный индекс на (grp, id).

Наличие индекса, предназначенного для конкретного запроса, должно повысить эффективность - большинства, если не всех методов.

ypercubeᵀᴹ
источник
Добавлена ​​необходимая информация по вопросу.
P ரதீப்