Я переглянув подібні проблеми і ніколи не зміг знайти рішення віконної функції, яке б одночасно передавало дані. Я не думаю, що це можливо. Функції вікна потрібно мати можливість застосовувати до всіх значень стовпця. Це робить обчислення скидання таким чином дуже важким, оскільки одне скидання змінює значення для всіх наступних значень.
Один із способів задуматися над проблемою полягає в тому, що ви можете отримати бажаний кінцевий результат, якщо обчислити основний пробіг, доки ви зможете відняти загальний обсяг з правильного попереднього рядка. Наприклад, у ваших вибіркових даних значенням id
4 є running total of row 4 - the running total of row 3
. Значенням id
6 є те, running total of row 6 - the running total of row 3
що скидання ще не відбулося. Значення для id
7 - це 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;