J'ai examiné des problèmes similaires et je n'ai jamais été en mesure de trouver une solution de fonction de fenêtre qui effectue un seul passage sur les données. Je ne pense pas que ce soit possible. Les fonctions de fenêtre doivent pouvoir être appliquées à toutes les valeurs d'une colonne. Cela rend les calculs de réinitialisation comme celui-ci très difficiles, car une réinitialisation modifie la valeur de toutes les valeurs suivantes.
Une façon de penser au problème est que vous pouvez obtenir le résultat final souhaité si vous calculez un total cumulé de base tant que vous pouvez soustraire le total cumulé de la ligne précédente correcte. Par exemple, dans vos exemples de données, la valeur de id
4 est le running total of row 4 - the running total of row 3
. La valeur de id
6 est le running total of row 6 - the running total of row 3
car une réinitialisation n'a pas encore eu lieu. La valeur de id
7 est le running total of row 7 - the running total of row 6
et ainsi de suite.
J'aborderais cela avec T-SQL en boucle. Je me suis un peu emporté et je pense avoir une solution complète. Pour 3 millions de lignes et 500 groupes, le code s'est terminé en 24 secondes sur mon bureau. Je teste avec SQL Server 2016 Developer Edition avec 6 vCPU. Je profite des insertions parallèles et de l'exécution parallèle en général, vous devrez donc peut-être modifier le code si vous utilisez une version antérieure ou si vous avez des limitations DOP.
Ci-dessous le code que j'ai utilisé pour générer les données. Les plages VAL
et RESET_VAL
doivent être similaires à vos exemples de données.
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;
L'algorithme est le suivant:
1) Commencez par insérer toutes les lignes avec un total cumulé standard dans une table temporaire.
2) En boucle:
2a) Pour chaque groupe, calculez la première ligne avec un total cumulé au-dessus de la valeur reset_value restante dans la table et stockez l'id, le total cumulé qui était trop grand et le total cumulé précédent qui était trop grand dans une table temporaire.
2b) Supprimez les lignes de la première table temporaire dans une table temporaire de résultats dont la valeur est ID
inférieure ou égale à celle ID
de la seconde table temporaire. Utilisez les autres colonnes pour ajuster le total cumulé selon vos besoins.
3) Après que la suppression ne traite plus les lignes, exécutez-en une supplémentaire DELETE OUTPUT
dans le tableau des résultats. Il s'agit des lignes à la fin du groupe qui ne dépassent jamais la valeur de réinitialisation.
Je vais passer en revue une implémentation de l'algorithme ci-dessus dans T-SQL étape par étape.
Commencez par créer quelques tables temporaires. #initial_results
contient les données d'origine avec le total cumulé standard, #group_bookkeeping
est mise à jour à chaque boucle pour déterminer quelles lignes peuvent être déplacées et #final_results
contient les résultats avec le total cumulé ajusté pour les réinitialisations.
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;
Je crée l'index cluster sur la table temporaire après pour que l'insertion et la construction de l'index puissent se faire en parallèle. Cela a fait une grande différence sur ma machine mais peut-être pas sur la vôtre. La création d'un index sur la table source n'a pas semblé aider mais cela pourrait aider sur votre machine.
Le code ci-dessous s'exécute dans la boucle et met à jour la table de comptabilité. Pour chaque groupe, nous devons obtenir la recherche du maximum ID
qui doit être déplacé dans le tableau des résultats. Nous avons besoin du total cumulé de cette ligne pour pouvoir le soustraire du total cumulé initial. La grp_done
colonne est définie sur 1 lorsqu'il n'y a plus de travail à faire pour 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);
Vraiment pas un fan de l' LOOP JOIN
indice en général, mais c'est une requête simple et c'était le moyen le plus rapide d'obtenir ce que je voulais. Pour vraiment optimiser le temps de réponse, je voulais des jointures de boucles imbriquées parallèles au lieu de jointures de fusion DOP 1.
Le code ci-dessous s'exécute dans la boucle et déplace les données de la table initiale vers la table de résultats finale. Remarquez l'ajustement du total cumulé initial.
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;
Pour votre commodité, voici le code complet:
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;