J'ai écrit plusieurs fois sur l'utilisation des curseurs et comment, dans la plupart des cas, il est plus efficace de réécrire vos curseurs en utilisant la logique basée sur les ensembles.
Je suis réaliste, cependant.
Je sais qu'il y a des cas où les curseurs sont "requis" - vous devez appeler une autre procédure stockée ou envoyer un e-mail pour chaque ligne, vous effectuez des tâches de maintenance sur chaque base de données, ou vous exécutez une tâche unique qui simplement ne vaut pas la peine d'investir du temps pour se convertir à un ensemble.
Comment vous faites (probablement) aujourd'hui
Quelle que soit la raison pour laquelle vous utilisez toujours des curseurs, vous devez au moins faire attention à ne pas utiliser les options par défaut assez coûteuses. La plupart des gens commencent leurs curseurs comme ceci :
DECLARE c CURSOR FOR SELECT whatever FROM ...
Encore une fois, pour les tâches ponctuelles et ponctuelles, c'est probablement très bien. Mais il y a…
Autres façons de procéder
Je voulais exécuter des tests en utilisant les valeurs par défaut et les comparer à différentes options de curseur telles que LOCAL
, STATIC
, READ_ONLY
et FAST_FORWARD
. (Il existe une tonne d'options, mais ce sont celles qui sont les plus couramment utilisées car elles s'appliquent aux types d'opérations de curseur les plus courants que les gens utilisent.) Non seulement je voulais tester la vitesse brute de quelques combinaisons différentes, mais également l'impact sur tempdb et la mémoire, à la fois après un redémarrage à froid du service et avec un cache à chaud.
La requête que j'ai décidé de transmettre au curseur est une requête très simple sur sys.objects
, dans l'exemple de base de données AdventureWorks2012. Cela renvoie 318 500 lignes sur mon système (un système très modeste à 2 cœurs avec 4 Go de RAM) :
SELECT c1.[object_id] FROM sys.objects AS c1 CROSS JOIN (SELECT TOP 500 name FROM sys.objects) AS c2;
Ensuite, j'ai enveloppé cette requête dans un curseur avec diverses options (y compris les valeurs par défaut) et exécuté des tests, mesurant la mémoire totale du serveur, les pages allouées à tempdb (selon sys.dm_db_task_space_usage
et/ou sys.dm_db_session_space_usage
) et la durée totale. J'ai également essayé d'observer les conflits tempdb à l'aide de scripts de Glenn Berry et Robert Davis, mais sur mon système dérisoire, je n'ai pu détecter aucun conflit. Bien sûr, je suis également sur SSD et absolument rien d'autre ne fonctionne sur le système, donc ce sont peut-être des choses que vous souhaitez ajouter à vos propres tests si tempdb est plus susceptible d'être un goulot d'étranglement.
Donc, à la fin, les requêtes ressemblaient à ceci, avec des requêtes de diagnostic parsemées aux points appropriés :
DECLARE @i INT = 1; DECLARE c CURSOR -- LOCAL -- LOCAL STATIC -- LOCAL FAST_FORWARD -- LOCAL STATIC READ_ONLY FORWARD_ONLY FOR SELECT c1.[object_id] FROM sys.objects AS c1 CROSS JOIN (SELECT TOP 500 name FROM sys.objects) AS c2 ORDER BY c1.[object_id]; OPEN c; FETCH c INTO @i; WHILE (@@FETCH_STATUS = 0) BEGIN SET @i += 1; -- meaningless operation FETCH c INTO @i; END CLOSE c; DEALLOCATE c;
Résultats
Durée
On peut dire que la mesure la plus importante et la plus courante est "combien de temps cela a-t-il pris ?" Eh bien, il a fallu presque cinq fois plus de temps pour exécuter un curseur avec les options par défaut (ou avec uniquement LOCAL
spécifié), par rapport à la spécification soit STATIC
ou FAST_FORWARD
:
Mémoire
Je voulais également mesurer la mémoire supplémentaire que SQL Server demanderait lors de l'exécution de chaque type de curseur. J'ai donc simplement redémarré avant chaque test de cache à froid, en mesurant le compteur de performances Total Server Memory (KB)
avant et après chaque essai. La meilleure combinaison ici était LOCAL FAST_FORWARD
:
utilisation de tempdb
Ce résultat m'a surpris. Étant donné que la définition d'un curseur statique signifie qu'il copie l'intégralité du résultat dans tempdb, et qu'il est en fait exprimé dans sys.dm_exec_cursors
comme SNAPSHOT
, je m'attendais à ce que le hit sur les pages tempdb soit plus élevé avec toutes les variantes statiques du curseur. Ce n'était pas le cas; encore une fois, nous voyons un coup d'environ 5X sur l'utilisation de tempdb avec le curseur par défaut et celui avec seulement LOCAL
spécifié :
Conclusion
Pendant des années, j'ai insisté sur le fait que l'option suivante doit toujours être spécifiée pour vos curseurs :
LOCAL STATIC READ_ONLY FORWARD_ONLY
À partir de ce moment, jusqu'à ce que j'aie la possibilité de tester d'autres permutations ou de trouver des cas où ce n'est pas l'option la plus rapide, je recommanderai ce qui suit :
LOCAL FAST_FORWARD
(En passant, j'ai également effectué des tests en omettant le LOCAL
option, et les différences étaient négligeables.)
Cela dit, ce n'est pas nécessairement vrai pour *tous* les curseurs. Dans ce cas, je parle uniquement des curseurs où vous ne lisez que les données du curseur, dans le sens avant uniquement, et vous ne mettez pas à jour les données sous-jacentes (soit par la clé, soit en utilisant WHERE CURRENT OF
). Ce sont des tests pour un autre jour.