He analizado problemas similares y nunca he podido encontrar una solución de función de ventana que haga un solo paso sobre los datos. No creo que sea posible. Las funciones de ventana deben poder aplicarse a todos los valores de una columna. Eso hace que los cálculos de reinicio como este sean muy difíciles, porque un reinicio cambia el valor de todos los siguientes valores.
Una forma de pensar sobre el problema es que puede obtener el resultado final que desea si calcula un total acumulado básico siempre que pueda restar el total acumulado de la fila anterior correcta. Por ejemplo, en sus datos de muestra, el valor de id
4 es el running total of row 4 - the running total of row 3
. El valor de id
6 es running total of row 6 - the running total of row 3
porque todavía no se ha reiniciado. El valor de id
7 es el running total of row 7 - the running total of row 6
y así sucesivamente.
Enfocaría esto con T-SQL en un bucle. Me dejé llevar y creo que tengo una solución completa. Durante 3 millones de filas y 500 grupos, el código terminó en 24 segundos en mi escritorio. Estoy probando con SQL Server 2016 Developer Edition con 6 vCPU. Aprovecho las inserciones paralelas y la ejecución paralela en general, por lo que es posible que deba cambiar el código si tiene una versión anterior o tiene limitaciones de DOP.
Debajo del código que usé para generar los datos. Los rangos en VAL
y RESET_VAL
deben ser similares a sus datos de muestra.
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;
El algoritmo es como sigue:
1) Comience insertando todas las filas con un total acumulado estándar en una tabla temporal.
2) En un bucle:
2a) Para cada grupo, calcule la primera fila con un total acumulado por encima del valor reset_valor restante en la tabla y almacene la identificación, el total acumulado que era demasiado grande y el total acumulado anterior que era demasiado grande en una tabla temporal.
2b) Eliminar filas de la primera tabla temporal en una tabla temporal de resultados que tenga un valor ID
menor o igual al ID
de la segunda tabla temporal. Use las otras columnas para ajustar el total acumulado según sea necesario.
3) Después de que la eliminación ya no procese filas, ejecute un adicional DELETE OUTPUT
en la tabla de resultados. Esto es para filas al final del grupo que nunca exceden el valor de reinicio.
Revisaré una implementación del algoritmo anterior en T-SQL paso a paso.
Comience creando algunas tablas temporales. #initial_results
contiene los datos originales con el total acumulado estándar, #group_bookkeeping
se actualiza cada ciclo para determinar qué filas se pueden mover y #final_results
contiene los resultados con el total acumulado ajustado para restablecimientos.
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;
Luego creo el índice agrupado en la tabla temporal para que la inserción y la creación del índice se puedan hacer en paralelo. Hice una gran diferencia en mi máquina, pero puede que no en la tuya. La creación de un índice en la tabla de origen no pareció ayudar, pero eso podría ayudar en su máquina.
El siguiente código se ejecuta en el bucle y actualiza la tabla de contabilidad. Para cada grupo, necesitamos obtener el máximo de búsqueda ID
que se debe mover a la tabla de resultados. Necesitamos el total acumulado de esa fila para poder restarlo del total acumulado inicial. La grp_done
columna se establece en 1 cuando no hay más trabajo que hacer 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 no soy fanático de la LOOP JOIN
pista en general, pero esta es una consulta simple y fue la forma más rápida de obtener lo que quería. Para optimizar realmente el tiempo de respuesta, quería combinaciones de bucles anidados paralelos en lugar de combinaciones de combinación DOP 1.
El siguiente código se ejecuta en el bucle y mueve los datos de la tabla inicial a la tabla de resultados final. Observe el ajuste al total acumulado inicial.
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 su comodidad, a continuación se encuentra el 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;