Je suis en train de concevoir une table de transactions. J'ai réalisé que le calcul des totaux cumulés pour chaque ligne sera nécessaire et que les performances pourraient être lentes. J'ai donc créé une table avec 1 million de lignes à des fins de test.
CREATE TABLE [dbo].[Table_1](
[seq] [int] IDENTITY(1,1) NOT NULL,
[value] [bigint] NOT NULL,
CONSTRAINT [PK_Table_1] PRIMARY KEY CLUSTERED
(
[seq] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
Et j'ai essayé d'obtenir 10 lignes récentes et ses totaux cumulés, mais cela a pris environ 10 secondes.
--1st attempt
SELECT TOP 10 seq
,value
,sum(value) OVER (ORDER BY seq) total
FROM Table_1
ORDER BY seq DESC
--(10 rows affected)
--Table 'Worktable'. Scan count 1000001, logical reads 8461526, physical reads 2, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
--Table 'Table_1'. Scan count 1, logical reads 2608, physical reads 516, read-ahead reads 2617, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
--Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
--
--(1 row affected)
--
-- SQL Server Execution Times:
-- CPU time = 8483 ms, elapsed time = 9786 ms.
Je soupçonnais TOP
la raison de la lenteur des performances du plan, j'ai donc changé la requête comme ceci, et cela a pris environ 1 à 2 secondes. Mais je pense que c'est encore lent pour la production et je me demande si cela peut encore être amélioré.
--2nd attempt
SELECT *
,(
SELECT SUM(value)
FROM Table_1
WHERE seq <= t.seq
) total
FROM (
SELECT TOP 10 seq
,value
FROM Table_1
ORDER BY seq DESC
) t
ORDER BY seq DESC
--(10 rows affected)
--Table 'Table_1'. Scan count 11, logical reads 26083, physical reads 1, read-ahead reads 443, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
--
--(1 row affected)
--
-- SQL Server Execution Times:
-- CPU time = 1422 ms, elapsed time = 1621 ms.
Mes questions sont:
- Pourquoi la requête de la 1ère tentative est plus lente que la 2ème?
- Comment puis-je encore améliorer les performances? Je peux également modifier les schémas.
Juste pour être clair, les deux requêtes renvoient le même résultat que ci-dessous.
value
?Réponses:
Je recommande de tester avec un peu plus de données pour avoir une meilleure idée de ce qui se passe et voir comment les différentes approches fonctionnent. J'ai chargé 16 millions de lignes dans une table avec la même structure. Vous pouvez trouver le code pour remplir le tableau au bas de cette réponse.
L'approche suivante prend 19 secondes sur ma machine:
Plan réel ici . La plupart du temps est consacré au calcul de la somme et au tri. De façon inquiétante, le plan de requête effectue presque tout le travail pour l'ensemble des résultats et filtre les 10 lignes que vous avez demandées à la fin. Le temps d'exécution de cette requête évolue avec la taille de la table plutôt qu'avec la taille de l'ensemble de résultats.
Cette option prend 23 secondes sur ma machine:
Plan réel ici . Cette approche évolue avec le nombre de lignes demandées et la taille de la table. Près de 160 millions de lignes sont lues dans le tableau:
Pour obtenir des résultats corrects, vous devez additionner les lignes de l'ensemble du tableau. Idéalement, vous ne devriez effectuer cette sommation qu'une seule fois. Il est possible de le faire si vous changez la façon dont vous abordez le problème. Vous pouvez calculer la somme pour l'ensemble du tableau, puis soustraire un total cumulé des lignes de l'ensemble de résultats. Cela vous permet de trouver la somme pour la Nème ligne. Une façon de procéder:
Plan réel ici . La nouvelle requête s'exécute en 644 ms sur ma machine. Le tableau est analysé une fois pour obtenir le total complet, puis une ligne supplémentaire est lue pour chaque ligne du jeu de résultats. Il n'y a pas de tri et presque tout le temps est consacré au calcul de la somme dans la partie parallèle du plan:
Si vous souhaitez que cette requête soit encore plus rapide, il vous suffit d'optimiser la partie qui calcule la somme complète. La requête ci-dessus effectue une analyse d'index en cluster. L'index cluster comprend toutes les colonnes, mais vous n'avez besoin que de la
[value]
colonne. Une option consiste à créer un index non cluster sur cette colonne. Une autre option consiste à créer un index columnstore non cluster sur cette colonne. Les deux amélioreront les performances. Si vous êtes sur Enterprise, une excellente option consiste à créer une vue indexée comme celle-ci:Cette vue renvoie une seule ligne, donc elle ne prend presque pas d'espace. Il y aura une pénalité lors de l'exécution de DML, mais cela ne devrait pas être très différent de la maintenance d'index. Avec la vue indexée en jeu, la requête prend désormais 0 ms:
Plan réel ici . La meilleure partie de cette approche est que le temps d'exécution n'est pas modifié par la taille de la table. La seule chose qui compte est le nombre de lignes renvoyées. Par exemple, si vous obtenez les 10000 premières lignes, la requête prend maintenant 18 ms pour s'exécuter.
Code pour remplir la table:
la source
Différence dans les deux premières approches
Le premier plan passe environ 7 des 10 secondes dans l'opérateur de bobine de fenêtre, c'est donc la principale raison pour laquelle il est si lent. Cela crée beaucoup d'E / S dans tempdb pour créer cela. Mes statistiques d'E / S et de temps ressemblent à ceci:
Le deuxième plan est capable d'éviter la bobine, et donc la table de travail entièrement. Il récupère simplement les 10 premières lignes de l'index clusterisé, puis une boucle imbriquée se joint à l'agrégation (somme) issue d'un balayage d'index cluster séparé. La face intérieure finit toujours par lire la table entière, mais la table est très dense, donc cela est raisonnablement efficace avec un million de lignes.
Amélioration des performances
Columnstore
Si vous voulez vraiment l'approche du «reporting en ligne», columnstore est probablement votre meilleure option.
Ensuite, cette requête est ridiculement rapide:
Voici les statistiques de ma machine:
Vous n'allez probablement pas battre cela (à moins que vous ne soyez vraiment intelligent - gentil, Joe). Columnstore est extrêmement bon pour analyser et agréger de grandes quantités de données.
Utiliser
ROW
plutôt que l'RANGE
option de fonction de fenêtreVous pouvez obtenir des performances très similaires à votre deuxième requête avec cette approche, qui a été mentionnée dans une autre réponse, et que j'ai utilisée dans l'exemple columnstore ci-dessus ( plan d'exécution ):
Il en résulte moins de lectures que votre deuxième approche et aucune activité tempdb par rapport à votre première approche car le spool de fenêtre se produit en mémoire :
Malheureusement, l'exécution est à peu près la même que votre deuxième approche.
Solution basée sur un schéma: totaux cumulés asynchrones
Puisque vous êtes ouvert à d'autres idées, vous pouvez envisager de mettre à jour le "total cumulé" de manière asynchrone. Vous pouvez périodiquement prendre les résultats d'une de ces requêtes et les charger dans une table "totaux". Vous feriez donc quelque chose comme ceci:
Chargez-le tous les jours / heures / peu importe (cela a pris environ 2 secondes sur ma machine avec des rangées de 1 mm et pourrait être optimisé):
Ensuite, votre requête de rapport est très efficace:
Voici les statistiques de lecture:
Solution basée sur un schéma: totaux en ligne avec contraintes
Une solution vraiment intéressante à cela est traitée en détail dans cette réponse à la question: Écrire un schéma bancaire simple: Comment dois-je garder mes soldes en synchronisation avec l'historique de leurs transactions?
L'approche de base serait de suivre le total cumulé actuel en ligne avec le total cumulé précédent et le numéro de séquence. Ensuite, vous pouvez utiliser des contraintes pour valider que les totaux cumulés sont toujours corrects et à jour.
Nous remercions Paul White d' avoir fourni un exemple d'implémentation du schéma dans ce Q&R:
la source
Lorsqu'il s'agit d'un si petit sous-ensemble de lignes renvoyées, la jointure triangulaire est une bonne option. Cependant, lorsque vous utilisez des fonctions de fenêtre, vous disposez de plus d'options qui peuvent augmenter leurs performances. L'option par défaut pour l'option de fenêtre est RANGE, mais l'option optimale est ROWS. Sachez que la différence n'est pas seulement dans la performance, mais aussi dans les résultats lorsque les liens sont impliqués.
Le code suivant est légèrement plus rapide que ceux que vous avez présentés.
la source
ROWS
. Je l'ai essayé mais je ne peux pas dire que c'est plus rapide que ma 2ème requête. Le résultat a étéCPU time = 1438 ms, elapsed time = 1537 ms.