Voici quelques méthodes que vous pouvez comparer. Commençons par configurer une table avec des données factices. Je remplis cela avec un tas de données aléatoires de sys.all_columns. Eh bien, c'est un peu aléatoire - je m'assure que les dates sont contiguës (ce qui n'est vraiment important que pour l'une des réponses).
CREATE TABLE dbo.Hits(Day SMALLDATETIME, CustomerID INT);
CREATE CLUSTERED INDEX x ON dbo.Hits([Day]);
INSERT dbo.Hits SELECT TOP (5000) DATEADD(DAY, r, '20120501'),
COALESCE(ASCII(SUBSTRING(name, s, 1)), 86)
FROM (SELECT name, r = ROW_NUMBER() OVER (ORDER BY name)/10,
s = CONVERT(INT, RIGHT(CONVERT(VARCHAR(20), [object_id]), 1))
FROM sys.all_columns) AS x;
SELECT
Earliest_Day = MIN([Day]),
Latest_Day = MAX([Day]),
Unique_Days = DATEDIFF(DAY, MIN([Day]), MAX([Day])) + 1,
Total_Rows = COUNT(*)
FROM dbo.Hits;
Résultats:
Earliest_Day Latest_Day Unique_Days Total_Days
------------------- ------------------- ----------- ----------
2012-05-01 00:00:00 2013-09-13 00:00:00 501 5000
Les données ressemblent à ceci (5000 lignes) - mais auront une apparence légèrement différente sur votre système en fonction de la version et du numéro de construction:
Day CustomerID
------------------- ---
2012-05-01 00:00:00 95
2012-05-01 00:00:00 97
2012-05-01 00:00:00 97
2012-05-01 00:00:00 117
2012-05-01 00:00:00 100
...
2012-05-02 00:00:00 110
2012-05-02 00:00:00 110
2012-05-02 00:00:00 95
...
Et les résultats des totaux cumulés devraient ressembler à ceci (501 lignes):
Day c rt
------------------- -- --
2012-05-01 00:00:00 6 6
2012-05-02 00:00:00 5 11
2012-05-03 00:00:00 4 15
2012-05-04 00:00:00 7 22
2012-05-05 00:00:00 6 28
...
Donc, les méthodes que je vais comparer sont les suivantes:
- "auto-rejoindre" - l'approche puriste basée sur les ensembles
- "CTE récursif avec dates" - cela repose sur des dates contiguës (pas de lacunes)
- "CTE récursif avec row_number" - similaire au précédent mais plus lent, en s'appuyant sur ROW_NUMBER
- "CTE récursif avec #temp table" - volé à la réponse de Mikael comme suggéré
- "mise à jour originale" qui, bien que non pris en charge et ne promettant pas un comportement défini, semble être très populaire
- "le curseur"
- SQL Server 2012 utilisant la nouvelle fonctionnalité de fenêtrage
auto-rejoindre
C’est ainsi que les gens vous diront de le faire quand ils vous avertiront de rester à l’écart des curseurs, car «les réglages sur les ensembles sont toujours plus rapides». Dans certaines expériences récentes, j'ai constaté que le curseur surpassait cette solution.
;WITH g AS
(
SELECT [Day], c = COUNT(DISTINCT CustomerID)
FROM dbo.Hits
GROUP BY [Day]
)
SELECT g.[Day], g.c, rt = SUM(g2.c)
FROM g INNER JOIN g AS g2
ON g.[Day] >= g2.[Day]
GROUP BY g.[Day], g.c
ORDER BY g.[Day];
cte récursive avec dates
Rappel - cela repose sur des dates contiguës (sans espaces), sur une récurrence allant jusqu'à 10000, et sur la date de début de la plage qui vous intéresse (pour définir l'ancre). Vous pouvez définir l’ancre de manière dynamique en utilisant une sous-requête, bien sûr, mais je voulais garder les choses simples.
;WITH g AS
(
SELECT [Day], c = COUNT(DISTINCT CustomerID)
FROM dbo.Hits
GROUP BY [Day]
), x AS
(
SELECT [Day], c, rt = c
FROM g
WHERE [Day] = '20120501'
UNION ALL
SELECT g.[Day], g.c, x.rt + g.c
FROM x INNER JOIN g
ON g.[Day] = DATEADD(DAY, 1, x.[Day])
)
SELECT [Day], c, rt
FROM x
ORDER BY [Day]
OPTION (MAXRECURSION 10000);
cte récursif avec row_number
Le calcul de Row_number est légèrement coûteux ici. Encore une fois, cela prend en charge le niveau maximal de récursivité de 10 000, mais vous n'avez pas besoin d'attribuer l'ancre.
;WITH g AS
(
SELECT [Day], rn = ROW_NUMBER() OVER (ORDER BY DAY),
c = COUNT(DISTINCT CustomerID)
FROM dbo.Hits
GROUP BY [Day]
), x AS
(
SELECT [Day], rn, c, rt = c
FROM g
WHERE rn = 1
UNION ALL
SELECT g.[Day], g.rn, g.c, x.rt + g.c
FROM x INNER JOIN g
ON g.rn = x.rn + 1
)
SELECT [Day], c, rt
FROM x
ORDER BY [Day]
OPTION (MAXRECURSION 10000);
cte récursif avec table temporaire
Voler la réponse de Mikael, comme suggéré, pour l'inclure dans les tests.
CREATE TABLE #Hits
(
rn INT PRIMARY KEY,
c INT,
[Day] SMALLDATETIME
);
INSERT INTO #Hits (rn, c, Day)
SELECT ROW_NUMBER() OVER (ORDER BY DAY),
COUNT(DISTINCT CustomerID),
[Day]
FROM dbo.Hits
GROUP BY [Day];
WITH x AS
(
SELECT [Day], rn, c, rt = c
FROM #Hits as c
WHERE rn = 1
UNION ALL
SELECT g.[Day], g.rn, g.c, x.rt + g.c
FROM x INNER JOIN #Hits as g
ON g.rn = x.rn + 1
)
SELECT [Day], c, rt
FROM x
ORDER BY [Day]
OPTION (MAXRECURSION 10000);
DROP TABLE #Hits;
mise à jour décalée
Encore une fois, je n'inclue ceci que par souci d'exhaustivité; Personnellement, je ne m'appuierais pas sur cette solution car, comme je l'ai mentionné dans une autre réponse, cette méthode n'est pas garantie du tout et peut complètement casser dans une future version de SQL Server. (Je fais de mon mieux pour contraindre SQL Server à obéir à l'ordre que je souhaite, en utilisant un indice pour le choix d'index.)
CREATE TABLE #x([Day] SMALLDATETIME, c INT, rt INT);
CREATE UNIQUE CLUSTERED INDEX x ON #x([Day]);
INSERT #x([Day], c)
SELECT [Day], c = COUNT(DISTINCT CustomerID)
FROM dbo.Hits
GROUP BY [Day]
ORDER BY [Day];
DECLARE @rt1 INT;
SET @rt1 = 0;
UPDATE #x
SET @rt1 = rt = @rt1 + c
FROM #x WITH (INDEX = x);
SELECT [Day], c, rt FROM #x ORDER BY [Day];
DROP TABLE #x;
le curseur
"Attention, il y a des curseurs ici! Les curseurs sont diaboliques! Vous devriez éviter les curseurs à tout prix!" Non, ce n'est pas moi qui parle, c'est juste des choses que j'entends beaucoup. Contrairement à l'opinion populaire, il existe des cas où les curseurs sont appropriés.
CREATE TABLE #x2([Day] SMALLDATETIME, c INT, rt INT);
CREATE UNIQUE CLUSTERED INDEX x ON #x2([Day]);
INSERT #x2([Day], c)
SELECT [Day], COUNT(DISTINCT CustomerID)
FROM dbo.Hits
GROUP BY [Day]
ORDER BY [Day];
DECLARE @rt2 INT, @d SMALLDATETIME, @c INT;
SET @rt2 = 0;
DECLARE c CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
FOR SELECT [Day], c FROM #x2 ORDER BY [Day];
OPEN c;
FETCH NEXT FROM c INTO @d, @c;
WHILE @@FETCH_STATUS = 0
BEGIN
SET @rt2 = @rt2 + @c;
UPDATE #x2 SET rt = @rt2 WHERE [Day] = @d;
FETCH NEXT FROM c INTO @d, @c;
END
SELECT [Day], c, rt FROM #x2 ORDER BY [Day];
DROP TABLE #x2;
SQL Server 2012
Si vous utilisez la version la plus récente de SQL Server, les améliorations apportées à la fonctionnalité de fenêtrage nous permettent de calculer facilement les totaux cumulés sans le coût exponentiel de l’adhésion automatique (la somme est calculée en un seul passage), la complexité des CTE (y compris la de lignes contiguës pour le CTE le plus performant), la mise à jour décalée non prise en charge et le curseur interdit. Méfiez-vous simplement de la différence entre utiliser RANGE
et ROWS
, ou ne pas spécifier du tout - cela ROWS
évite seulement un spool sur disque, ce qui nuirait considérablement aux performances.
;WITH g AS
(
SELECT [Day], c = COUNT(DISTINCT CustomerID)
FROM dbo.Hits
GROUP BY [Day]
)
SELECT g.[Day], c,
rt = SUM(c) OVER (ORDER BY [Day] ROWS UNBOUNDED PRECEDING)
FROM g
ORDER BY g.[Day];
comparaisons de performance
J'ai pris chaque approche et l'ai emballée un lot en utilisant ce qui suit:
SELECT SYSUTCDATETIME();
GO
DBCC DROPCLEANBUFFERS;DBCC FREEPROCCACHE;
-- query here
GO 10
SELECT SYSUTCDATETIME();
Voici les résultats de la durée totale, en millisecondes (rappelez-vous, cela inclut également les commandes DBCC):
method run 1 run 2
----------------------------- -------- --------
self-join 1296 ms 1357 ms -- "supported" non-SQL 2012 winner
recursive cte with dates 1655 ms 1516 ms
recursive cte with row_number 19747 ms 19630 ms
recursive cte with #temp table 1624 ms 1329 ms
quirky update 880 ms 1030 ms -- non-SQL 2012 winner
cursor 1962 ms 1850 ms
SQL Server 2012 847 ms 917 ms -- winner if SQL 2012 available
Et je l'ai encore fait sans les commandes DBCC:
method run 1 run 2
----------------------------- -------- --------
self-join 1272 ms 1309 ms -- "supported" non-SQL 2012 winner
recursive cte with dates 1247 ms 1593 ms
recursive cte with row_number 18646 ms 18803 ms
recursive cte with #temp table 1340 ms 1564 ms
quirky update 1024 ms 1116 ms -- non-SQL 2012 winner
cursor 1969 ms 1835 ms
SQL Server 2012 600 ms 569 ms -- winner if SQL 2012 available
Supprimer à la fois le DBCC et les boucles, en mesurant une seule itération brute:
method run 1 run 2
----------------------------- -------- --------
self-join 313 ms 242 ms
recursive cte with dates 217 ms 217 ms
recursive cte with row_number 2114 ms 1976 ms
recursive cte with #temp table 83 ms 116 ms -- "supported" non-SQL 2012 winner
quirky update 86 ms 85 ms -- non-SQL 2012 winner
cursor 1060 ms 983 ms
SQL Server 2012 68 ms 40 ms -- winner if SQL 2012 available
Enfin, j'ai multiplié par 10 le nombre de lignes de la table source (en remplaçant top par 50000 et en ajoutant une autre table en jointure croisée). Les résultats de cela, une seule itération sans commande DBCC (simplement pour gagner du temps):
method run 1 run 2
----------------------------- -------- --------
self-join 2401 ms 2520 ms
recursive cte with dates 442 ms 473 ms
recursive cte with row_number 144548 ms 147716 ms
recursive cte with #temp table 245 ms 236 ms -- "supported" non-SQL 2012 winner
quirky update 150 ms 148 ms -- non-SQL 2012 winner
cursor 1453 ms 1395 ms
SQL Server 2012 131 ms 133 ms -- winner
J'ai seulement mesuré la durée - je laisserai au lecteur le soin de comparer ces approches sur leurs données, en comparant d'autres métriques pouvant être importantes (ou pouvant varier avec leurs schémas / données). Avant de tirer des conclusions de cette réponse, il vous appartiendra de la tester par rapport à vos données et à votre schéma ... ces résultats changeront presque certainement à mesure que le nombre de lignes augmentera.
démo
J'ai ajouté un sqlfiddle . Résultats:
conclusion
Dans mes tests, le choix serait:
- Méthode SQL Server 2012, si SQL Server 2012 est disponible.
- Si SQL Server 2012 n'est pas disponible et que mes dates sont contiguës, j'utiliserais la méthode cts récursive avec dates.
- Si ni 1. ni 2. ne sont applicables, j'appuierais l'auto-jointure sur la mise à jour originale, même si les performances étaient proches, simplement parce que le comportement est documenté et garanti. Je suis moins inquiet à propos de la compatibilité future, car si tout va bien, si la mise à jour excentrique tombe, ce sera après avoir déjà converti tout mon code en 1. :-)
Mais encore une fois, vous devriez les tester contre votre schéma et vos données. Comme il s’agissait d’un test artificiel avec un nombre de lignes relativement faible, il se peut tout aussi bien que ce soit un pet dans le vent. J'ai fait d'autres tests avec différents nombres de schémas et de lignes, et l'heuristique des performances était très différente ... C'est pourquoi j'ai posé autant de questions complémentaires à votre question initiale.
MISE À JOUR
J'ai blogué plus à ce sujet ici:
Meilleures approches pour les totaux cumulés - mises à jour pour SQL Server 2012
Day
une clé et les valeurs sont-elles contiguës?