Скинути пробіг на основі іншого стовпця


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.?


Краще як? Цей запит демонструє низьку ефективність? Використовуючи які показники?
Аарон Бертран

@AaronBertrand - Для кращого розуміння я розмістив зразкові дані лише для однієї групи. Я повинен зробити те ж саме для 50000груп із 60 ІД . тому загальна кількість записів буде приблизно 3000000. Впевнений Recursive CTE, не вдасться до масштабування 3000000. Оновлять показники, коли повернусь до офісу. Чи можемо ми досягти цього, використовуючи, sum()Over(Order by)як ви використали в цій статті sqlperformance.com/2012/07/t-sql-queries/running-totals
P ரதீப்

Курсор може зробити краще, ніж рекурсивний CTE
папараццо

Відповіді:


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

Використання CURSOR:

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


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).

Наявність індексу, орієнтованого на конкретний запит, має підвищити ефективність - більшості, якщо не всіх методів.


Додана потрібна інформація щодо питання.
P ரதீப்
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.