Eu olhei para problemas semelhantes e nunca consegui encontrar uma solução de função de janela que faça uma única passagem sobre os dados. Eu não acho que é possível. As funções da janela precisam poder ser aplicadas a todos os valores em uma coluna. Isso dificulta cálculos de redefinição como esse, porque uma redefinição altera o valor de todos os seguintes valores.
Uma maneira de pensar sobre o problema é que você pode obter o resultado final desejado se calcular um total de execução básico desde que possa subtrair o total de execução da linha anterior correta. Por exemplo, em seus dados de amostra, o valor para id
4 é o running total of row 4 - the running total of row 3
. O valor para id
6 é o running total of row 6 - the running total of row 3
porque uma redefinição ainda não aconteceu. O valor para id
7 é o running total of row 7 - the running total of row 6
e assim por diante.
Eu abordaria isso com o T-SQL em um loop. Fiquei um pouco empolgado e acho que tenho uma solução completa. Para 3 milhões de linhas e 500 grupos, o código terminou em 24 segundos na minha área de trabalho. Estou testando com o SQL Server 2016 Developer edition com 6 vCPU. Estou aproveitando as inserções paralelas e a execução paralela em geral, portanto, talvez você precise alterar o código se estiver em uma versão mais antiga ou tiver limitações de DOP.
Abaixo do código que eu usei para gerar os dados. Os intervalos VAL
e RESET_VAL
devem ser semelhantes aos seus dados de amostra.
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;
O algoritmo é o seguinte:
1) Comece inserindo todas as linhas com um total em execução padrão em uma tabela temporária.
2) Em um loop:
2a) Para cada grupo, calcule a primeira linha com um total de execução acima do reset_value restante na tabela e armazene o ID, o total de execução que era muito grande e o total de execução anterior que era muito grande em uma tabela temporária.
2b) Exclua linhas da primeira tabela temporária em uma tabela temporária de resultados que tenha um valor ID
menor ou igual aoID
da segunda tabela temporária. Use as outras colunas para ajustar o total atual, conforme necessário.
3) Após a exclusão não processar mais as linhas, execute um procedimento adicional DELETE OUTPUT
na tabela de resultados. Isso é para linhas no final do grupo que nunca excedem o valor de redefinição.
Vou passar por uma implementação do algoritmo acima no T-SQL passo a passo.
Comece criando algumas tabelas temporárias. #initial_results
mantém os dados originais com o total de execução padrão, #group_bookkeeping
é atualizado a cada loop para descobrir quais linhas podem ser movidas e #final_results
contém os resultados com o total de execução ajustado para redefinições.
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;
Eu crio o índice em cluster na tabela temp depois para que a inserção e a compilação do índice possam ser feitas em paralelo. Fez uma grande diferença na minha máquina, mas talvez não na sua. Criar um índice na tabela de origem não pareceu ajudar, mas isso poderia ajudar na sua máquina.
O código abaixo é executado no loop e atualiza a tabela de contabilidade. Para cada grupo, precisamos encontrar o máximo ID
que deve ser movido para a tabela de resultados. Precisamos do total atual dessa linha para que possamos subtraí-lo do total inicial. A grp_done
coluna é definida como 1 quando não há mais trabalho a ser feito para a 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);
Realmente não LOOP JOIN
sou fã da dica em geral, mas essa é uma consulta simples e foi a maneira mais rápida de obter o que eu queria. Para otimizar o tempo de resposta, eu queria junções de loop aninhadas paralelas em vez de junções de mesclagem DOP 1.
O código abaixo é executado no loop e move os dados da tabela inicial para a tabela de resultados finais. Observe o ajuste no total inicial em execução.
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;
Para sua conveniência, abaixo está o código completo:
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;