ce document
est une excellente ressource sur le calcul des totaux cumulés dans SQL Server. par Itzik Ben Gan qui a été soumis à l'équipe SQL Server dans le cadre de sa campagne pour avoir le OVER
clause étendue par rapport à sa mise en œuvre initiale de SQL Server 2005. Il y montre comment, une fois que vous entrez dans des dizaines de milliers de lignes, les curseurs exécutent des solutions basées sur des ensembles. SQL Server 2012 a en effet étendu le OVER
clause rendant ce type de requête beaucoup plus facile.
SELECT col1,
SUM(col1) OVER (ORDER BY ind ROWS UNBOUNDED PRECEDING)
FROM @tmp
Comme vous êtes sur SQL Server 2005, cela ne vous est pas disponible.
Adam Machanic s'affiche ici comment le CLR peut être utilisé pour améliorer les performances des curseurs TSQL standard.
Pour cette définition de table
CREATE TABLE RunningTotals
(
ind int identity(1,1) primary key,
col1 int
)
Je crée des tables avec à la fois 2 000 et 10 000 lignes dans une base de données avec ALLOW_SNAPSHOT_ISOLATION ON
et un avec ce paramètre désactivé (la raison en est que mes premiers résultats étaient dans une base de données avec le paramètre activé, ce qui a conduit à un aspect déroutant des résultats).
Les index clusterisés pour toutes les tables n'avaient qu'une seule page racine. Le nombre de pages feuille pour chacun est indiqué ci-dessous.
+-------------------------------+-----------+------------+
| | 2,000 row | 10,000 row |
+-------------------------------+-----------+------------+
| ALLOW_SNAPSHOT_ISOLATION OFF | 5 | 22 |
| ALLOW_SNAPSHOT_ISOLATION ON | 8 | 39 |
+-------------------------------+-----------+------------+
J'ai testé les cas suivants (les liens montrent les plans d'exécution)
- Joindre à gauche et grouper par
- Sous-requête corrélée Plan de 2 000 lignes ,plan de 10000 rangées
- CTE de la réponse (mise à jour) de Mikael
- CTE ci-dessous
La raison de l'inclusion de l'option CTE supplémentaire était de fournir une solution CTE qui fonctionnerait toujours si le ind
la colonne n'était pas garantie séquentielle.
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
DECLARE @col1 int, @sumcol1 bigint;
WITH RecursiveCTE
AS (
SELECT TOP 1 ind, col1, CAST(col1 AS BIGINT) AS Total
FROM RunningTotals
ORDER BY ind
UNION ALL
SELECT R.ind, R.col1, R.Total
FROM (
SELECT T.*,
T.col1 + Total AS Total,
rn = ROW_NUMBER() OVER (ORDER BY T.ind)
FROM RunningTotals T
JOIN RecursiveCTE R
ON R.ind < T.ind
) R
WHERE R.rn = 1
)
SELECT @col1 =col1, @sumcol1=Total
FROM RecursiveCTE
OPTION (MAXRECURSION 0);
Toutes les requêtes avaient un CAST(col1 AS BIGINT)
ajouté afin d'éviter les erreurs de débordement lors de l'exécution. De plus, pour chacun d'eux, j'ai attribué les résultats aux variables comme ci-dessus afin d'éliminer le temps passé à renvoyer les résultats.
Résultats
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| | | | Base Table | Work Table | Time |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| | Snapshot | Rows | Scan count | logical reads | Scan count | logical reads | cpu | elapsed |
| Group By | On | 2,000 | 2001 | 12709 | | | 1469 | 1250 |
| | On | 10,000 | 10001 | 216678 | | | 30906 | 30963 |
| | Off | 2,000 | 2001 | 9251 | | | 1140 | 1160 |
| | Off | 10,000 | 10001 | 130089 | | | 29906 | 28306 |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| Sub Query | On | 2,000 | 2001 | 12709 | | | 844 | 823 |
| | On | 10,000 | 2 | 82 | 10000 | 165025 | 24672 | 24535 |
| | Off | 2,000 | 2001 | 9251 | | | 766 | 999 |
| | Off | 10,000 | 2 | 48 | 10000 | 165025 | 25188 | 23880 |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| CTE No Gaps | On | 2,000 | 0 | 4002 | 2 | 12001 | 78 | 101 |
| | On | 10,000 | 0 | 20002 | 2 | 60001 | 344 | 342 |
| | Off | 2,000 | 0 | 4002 | 2 | 12001 | 62 | 253 |
| | Off | 10,000 | 0 | 20002 | 2 | 60001 | 281 | 326 |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| CTE Alllows Gaps | On | 2,000 | 2001 | 4009 | 2 | 12001 | 47 | 75 |
| | On | 10,000 | 10001 | 20040 | 2 | 60001 | 312 | 413 |
| | Off | 2,000 | 2001 | 4006 | 2 | 12001 | 94 | 90 |
| | Off | 10,000 | 10001 | 20023 | 2 | 60001 | 313 | 349 |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
La sous-requête corrélée et le GROUP BY
version utilise des jointures de boucles imbriquées "triangulaires" pilotées par un balayage d'index clusterisé sur RunningTotals
tableau (T1
) et, pour chaque ligne renvoyée par ce balayage, en recherchant dans la table (T2
) auto-jointure sur T2.ind<=T1.ind
.
Cela signifie que les mêmes lignes sont traitées à plusieurs reprises. Lorsque le T1.ind=1000
la ligne est traitée l'auto-jointure récupère et additionne toutes les lignes avec un ind <= 1000
, puis pour la ligne suivante où T1.ind=1001
les mêmes 1000 lignes sont récupérées à nouveau et additionné avec une ligne supplémentaire et ainsi de suite.
Le nombre total d'opérations de ce type pour une table de 2 000 lignes est de 2 001 000, pour 10 000 lignes de 50 005 000 ou plus généralement (n² + n) / 2
qui croît clairement de façon exponentielle.
Dans le cas de 2 000 lignes, la principale différence entre le GROUP BY
et les versions de sous-requête sont que la première a l'agrégat de flux après la jointure et a donc trois colonnes qui l'alimentent (T1.ind
, T2.col1
, T2.col1
) et un GROUP BY
propriété de T1.ind
alors que ce dernier est calculé comme un agrégat scalaire, avec l'agrégat de flux avant la jointure, n'a que T2.col1
l'alimentant et n'a pas de GROUP BY
propriété définie du tout. Cet arrangement plus simple peut être considéré comme ayant un avantage mesurable en termes de réduction du temps CPU.
Pour le cas de 10 000 lignes, il existe une différence supplémentaire dans le plan de sous-requête. Il ajoute un spool impatient
qui copie tous les ind,cast(col1 as bigint)
valeurs dans tempdb
. Dans le cas où l'isolation d'instantané est activée, cela fonctionne plus compact que la structure d'index cluster et l'effet net est de réduire le nombre de lectures d'environ 25 % (car la table de base conserve beaucoup d'espace vide pour les informations de version), lorsque cette option est désactivée, elle est moins compacte (probablement à cause du bigint
contre int
différence) et plus de résultats de lectures. Cela réduit l'écart entre la sous-requête et le groupe par versions, mais la sous-requête l'emporte toujours.
Le grand gagnant était cependant le CTE récursif. Pour la version "sans lacunes", les lectures logiques de la table de base sont désormais 2 x (n + 1)
reflétant le n
index cherche dans l'index à 2 niveaux pour récupérer toutes les lignes plus la ligne supplémentaire à la fin qui ne renvoie rien et met fin à la récursivité. Cela signifiait tout de même 20 002 lectures pour traiter un tableau de 22 pages !
Les lectures de table de travail logique pour la version CTE récursive sont très élevées. Cela semble fonctionner à 6 lectures de table de travail par ligne source. Celles-ci proviennent de la bobine d'index qui stocke la sortie de la ligne précédente, puis est relue à l'itération suivante (bonne explication de cela par Umachandar Jayachandran ici ). Malgré le nombre élevé, c'est toujours le plus performant.