Database
 sql >> Base de données >  >> RDS >> Database

Faites-vous ces erreurs lorsque vous utilisez SQL CURSOR ?

Pour certaines personnes, c'est la mauvaise question. CURSEUR SQL EST l'erreur. Le diable est dans les détails! Vous pouvez lire toutes sortes de blasphèmes dans toute la blogosphère SQL au nom de SQL CURSOR.

Si vous ressentez la même chose, qu'est-ce qui vous a amené à cette conclusion ?

Si cela vient d'un ami et d'un collègue de confiance, je ne peux pas vous en vouloir. Ça arrive. Parfois beaucoup. Mais si quelqu'un vous a convaincu avec une preuve, c'est une autre histoire.

Nous ne nous sommes pas rencontrés auparavant. Tu ne me connais pas comme ami. Mais j'espère pouvoir l'expliquer avec des exemples et vous convaincre que SQL CURSOR a sa place. Ce n'est pas grand-chose, mais cette petite place dans notre code a des règles.

Mais d'abord, laissez-moi vous raconter mon histoire.

J'ai commencé à programmer avec des bases de données en utilisant xBase. C'était à l'université jusqu'à mes deux premières années de programmation professionnelle. Je vous dis cela parce qu'à l'époque, nous avions l'habitude de traiter les données de manière séquentielle, et non par lots définis comme SQL. Quand j'ai appris SQL, c'était comme un changement de paradigme. Le moteur de base de données décide pour moi avec ses commandes basées sur des ensembles que j'ai émises. Quand j'ai entendu parler de SQL CURSOR, j'ai eu l'impression d'être de retour avec les méthodes anciennes mais confortables.

Mais certains collègues seniors m'ont prévenu, "Évitez SQL CURSOR à tout prix!" J'ai eu quelques explications verbales, et c'est tout.

SQL CURSOR peut être mauvais si vous l'utilisez pour le mauvais travail. Comme utiliser un marteau pour couper du bois, c'est ridicule. Bien sûr, des erreurs peuvent se produire, et c'est là que nous nous concentrerons.

1. Utiliser SQL CURSOR lorsque des commandes basées sur des ensembles suffiront

Je ne saurais trop insister là-dessus, mais c'est le cœur du problème. Lorsque j'ai appris ce qu'était SQL CURSOR, une ampoule s'est allumée. "Boucles! Je sais que!" Cependant, jusqu'à ce que cela me donne des maux de tête et que mes aînés me grondent.

Vous voyez, l'approche de SQL est basée sur les ensembles. Vous émettez une commande INSERT à partir des valeurs de la table, et elle fera le travail sans boucles sur votre code. Comme je l'ai dit plus tôt, c'est le travail du moteur de base de données. Ainsi, si vous forcez une boucle à ajouter des enregistrements à une table, vous contournez cette autorité. Ça va devenir moche.

Avant d'essayer un exemple ridicule, préparons les données :


SELECT TOP (500)
  val = ROW_NUMBER() OVER (ORDER BY sod.SalesOrderDetailID)
, modified = GETDATE()
, status = 'inserted'
INTO dbo.TestTable
FROM AdventureWorks.Sales.SalesOrderDetail sod
CROSS JOIN AdventureWorks.Sales.SalesOrderDetail sod2

SELECT
 tt.val
,GETDATE() AS modified
,'inserted' AS status
INTO dbo.TestTable2
FROM dbo.TestTable tt
WHERE CAST(val AS VARCHAR) LIKE '%2%'

La première instruction générera 500 enregistrements de données. Le second en recevra un sous-ensemble. Ensuite, nous sommes prêts. Nous allons insérer les données manquantes de TestTable dans TestTable2 à l'aide du CURSEUR SQL. Voir ci-dessous :


DECLARE @val INT

DECLARE test_inserts CURSOR FOR 
	SELECT val FROM TestTable tt
	WHERE NOT EXISTS(SELECT val FROM TestTable2 tt1
                 WHERE tt1.val = tt.val)

OPEN test_inserts
FETCH NEXT FROM test_inserts INTO @val
WHILE @@fetch_status = 0
BEGIN
	INSERT INTO TestTable2
	(val, modified, status)
	VALUES
	(@val, GETDATE(),'inserted')

	FETCH NEXT FROM test_inserts INTO @val
END

CLOSE test_inserts
DEALLOCATE test_inserts

Voilà comment boucler en utilisant SQL CURSOR pour insérer un enregistrement manquant un par un. Assez long, n'est-ce pas ?

Maintenant, essayons une meilleure façon - l'alternative basée sur les ensembles. Voici :


INSERT INTO TestTable2
(val, modified, status)
SELECT val, GETDATE(), status
FROM TestTable tt
WHERE NOT EXISTS(SELECT val FROM TestTable2 tt1
                 WHERE tt1.val = tt.val)

C'est court, net et rapide. À quelle vitesse? Voir la figure 1 ci-dessous :

À l'aide de xEvent Profiler dans SQL Server Management Studio, j'ai comparé les chiffres du temps CPU, la durée et les lectures logiques. Comme vous pouvez le voir dans la figure 1, l'utilisation de la commande basée sur l'ensemble pour INSÉRER des enregistrements remporte le test de performances. Les chiffres parlent d'eux-mêmes. L'utilisation de SQL CURSOR consomme plus de ressources et de temps de traitement.

Par conséquent, avant d'utiliser SQL CURSOR, essayez d'abord d'écrire une commande basée sur un ensemble. Cela rapportera mieux à long terme.

Mais que se passe-t-il si vous avez besoin de SQL CURSOR pour faire le travail ?

2. Ne pas utiliser les options appropriées du CURSEUR SQL

Une autre erreur que j'ai commise dans le passé était de ne pas utiliser les options appropriées dans DECLARE CURSOR. Il existe des options pour la portée, le modèle, la simultanéité et si défilable ou non. Ces arguments sont facultatifs et il est facile de les ignorer. Cependant, si SQL CURSOR est le seul moyen d'effectuer la tâche, vous devez être explicite avec votre intention.

Alors, demandez-vous :

  • Lorsque vous parcourrez la boucle, allez-vous parcourir les lignes vers l'avant uniquement, ou passerez-vous à la première, à la dernière, à la précédente ou à la suivante ? Vous devez spécifier si le CURSEUR est en avant uniquement ou défilable. C'est DECLARE CURSOR FORWARD_ONLY ou DÉCLARER DÉFILEMENT DU CURSEUR .
  • Allez-vous mettre à jour les colonnes dans le CURSEUR ? Utilisez READ_ONLY s'il ne peut pas être mis à jour.
  • Avez-vous besoin des dernières valeurs lorsque vous parcourez la boucle ? Utilisez STATIC si les valeurs n'ont pas d'importance, qu'elles soient les plus récentes ou non. Utilisez DYNAMIC si d'autres transactions mettent à jour des colonnes ou suppriment des lignes que vous utilisez dans le CURSEUR et que vous avez besoin des dernières valeurs. Remarque  :DYNAMIC coûtera cher.
  • Le CURSEUR est-il global à la connexion ou local au lot ou à une procédure stockée ? Spécifiez si LOCAL ou GLOBAL.

Pour plus d'informations sur ces arguments, recherchez la référence dans Microsoft Docs.

Exemple

Essayons un exemple comparant trois CURSOR pour le temps CPU, les lectures logiques et la durée à l'aide de xEvents Profiler. Le premier n'aura pas d'options appropriées après DECLARE CURSOR. Le second est LOCAL STATIC FORWARD_ONLY READ_ONLY. Le dernier est LOtyuiCAL FAST_FORWARD.

Voici le premier :

-- NOTE: Don't just COPY and PASTE this code then run in your machine. Read and assess.

-- DECLARE CURSOR with no options
SET NOCOUNT ON

DECLARE @command NVARCHAR(2000) = N'SET NOCOUNT ON;'
CREATE TABLE #commands (
	ID INT IDENTITY (1, 1) PRIMARY KEY CLUSTERED
   ,Command NVARCHAR(2000)
);

INSERT INTO #commands (Command)
	VALUES (@command)

INSERT INTO #commands (Command)
	SELECT
	'SELECT ' + CHAR(39) + a.TABLE_SCHEMA + '.' + a.TABLE_NAME 
                  + ' - ' + CHAR(39) 
	          + ' + cast(count(*) as varchar) from ' 
		  + a.TABLE_SCHEMA + '.' + a.TABLE_NAME
	FROM INFORMATION_SCHEMA.tables a
	WHERE a.TABLE_TYPE = 'BASE TABLE';

DECLARE command_builder CURSOR FOR 
  SELECT
	Command
  FROM #commands

OPEN command_builder

FETCH NEXT FROM command_builder INTO @command
WHILE @@fetch_status = 0
BEGIN
	PRINT @command
	FETCH NEXT FROM command_builder INTO @command
END
CLOSE command_builder
DEALLOCATE command_builder

DROP TABLE #commands
GO

Il y a une meilleure option que le code ci-dessus, bien sûr. Si le but est simplement de générer un script à partir de tables utilisateur existantes, SELECT fera l'affaire. Ensuite, collez la sortie dans une autre fenêtre de requête.

Mais si vous avez besoin de générer un script et de l'exécuter immédiatement, c'est une autre histoire. Vous devez évaluer le script de sortie s'il va taxer votre serveur ou non. Voir erreur #4 plus tard.

Pour vous montrer la comparaison de trois CURSOR avec différentes options, cela suffira.

Maintenant, prenons un code similaire mais avec LOCAL STATIC FORWARD_ONLY READ_ONLY.

--- STATIC LOCAL FORWARD_ONLY READ_ONLY

SET NOCOUNT ON

DECLARE @command NVARCHAR(2000) = N'SET NOCOUNT ON;'
CREATE TABLE #commands (
	ID INT IDENTITY (1, 1) PRIMARY KEY CLUSTERED
   ,Command NVARCHAR(2000)
);

INSERT INTO #commands (Command)
	VALUES (@command)

INSERT INTO #commands (Command)
	SELECT
	'SELECT ' + CHAR(39) + a.TABLE_SCHEMA + '.' + a.TABLE_NAME 
                  + ' - ' + CHAR(39) 
	          + ' + cast(count(*) as varchar) from ' 
		  + a.TABLE_SCHEMA + '.' + a.TABLE_NAME
	FROM INFORMATION_SCHEMA.tables a
	WHERE a.TABLE_TYPE = 'BASE TABLE';

DECLARE command_builder CURSOR LOCAL STATIC FORWARD_ONLY READ_ONLY FOR SELECT
	Command
FROM #commands

OPEN command_builder

FETCH NEXT FROM command_builder INTO @command
WHILE @@fetch_status = 0
BEGIN
	PRINT @command
	FETCH NEXT FROM command_builder INTO @command
END
CLOSE command_builder
DEALLOCATE command_builder

DROP TABLE #commands
GO

Comme vous pouvez le voir ci-dessus, la seule différence par rapport au code précédent est le LOCAL STATIC FORWARD_ONLY READ_ONLY arguments.

Le troisième aura un LOCAL FAST_FORWARD. Maintenant, selon Microsoft, FAST_FORWARD est un FORWARD_ONLY, READ_ONLY CURSOR avec des optimisations activées. Nous verrons comment cela se passera avec les deux premiers.

Comment se comparent-ils ? Voir Figure 2 :

Celui qui prend le moins de temps CPU et de durée est le LOCAL STATIC FORWARD_ONLY READ_ONLY CURSOR. Notez également que SQL Server a des valeurs par défaut si vous ne spécifiez pas d'arguments tels que STATIC ou READ_ONLY. Il y a une conséquence terrible à cela, comme vous le verrez dans la section suivante.

Ce que sp_describe_cursor a révélé

sp_describe_cursor est une procédure stockée du maître base de données que vous pouvez utiliser pour obtenir des informations à partir du CURSEUR ouvert. Et voici ce qu'il a révélé à partir du premier lot de requêtes sans options CURSOR. Voir la figure 3 pour le résultat de sp_describe_cursor :

Trop exagéré ? Tu paries. Le CURSEUR du premier lot de requêtes est :

  • global à la connexion existante.
  • dynamique, ce qui signifie qu'il suit les modifications dans la table #commands pour les mises à jour, les suppressions et les insertions.
  • optimiste, ce qui signifie que SQL Server a ajouté une colonne supplémentaire à une table temporaire appelée CWT. Il s'agit d'une colonne de somme de contrôle permettant de suivre les modifications apportées aux valeurs de la table #commands.
  • scrollable, ce qui signifie que vous pouvez passer à la ligne précédente, suivante, supérieure ou inférieure dans le curseur.

Absurde? Je suis entièrement d'accord. Pourquoi avez-vous besoin d'une connexion mondiale ? Pourquoi avez-vous besoin de suivre les modifications apportées à la table temporaire #commands ? Avons-nous fait défiler autre part que l'enregistrement suivant dans le CURSEUR ?

Comme un serveur SQL le détermine pour nous, la boucle CURSOR devient une terrible erreur.

Vous réalisez maintenant pourquoi la spécification explicite des options SQL CURSOR est si cruciale. Donc, à partir de maintenant, spécifiez toujours ces arguments CURSOR si vous avez besoin d'utiliser un CURSOR.

Le plan d'exécution en dit plus

Le plan d'exécution réel a quelque chose de plus à dire sur ce qui se passe chaque fois qu'une commande FETCH NEXT FROM command_builder INTO @command est exécutée. Dans la figure 4, une ligne est insérée dans l'index clusterisé CWT_PrimaryKey dans la tempdb tableau CWT :

Les écritures se produisent dans tempdb sur chaque FETCH NEXT. En plus, il y a plus. Rappelez-vous que le CURSEUR est OPTIMISTE dans la figure 3 ? Les propriétés de Clustered Index Scan sur la partie la plus à droite du plan révèlent la colonne inconnue supplémentaire appelée Chk1002 :

Serait-ce la colonne Checksum ? Le plan XML confirme que c'est bien le cas :

Maintenant, comparez le plan d'exécution réel de FETCH NEXT lorsque le CURSEUR est LOCAL STATIC FORWARD_ONLY READ_ONLY :

Il utilise tempdb aussi, mais c'est beaucoup plus simple. Pendant ce temps, la figure 8 montre le plan d'exécution lorsque LOCAL FAST_FORWARD est utilisé :

À emporter

L'une des utilisations appropriées de SQL CURSOR est la génération de scripts ou l'exécution de certaines commandes administratives vers un groupe d'objets de base de données. Même s'il y a des utilisations mineures, votre première option est d'utiliser le LOCAL STATIC FORWARD_ONLY READ_ONLY CURSOR ou LOCAL FAST_FORWARD. Celui qui a un meilleur plan et des lectures logiques gagnera.

Ensuite, remplacez l'un d'entre eux par celui qui convient selon les besoins. Mais tu sais quoi? Dans mon expérience personnelle, je n'ai utilisé qu'un CURSEUR local en lecture seule avec une traversée vers l'avant uniquement. Je n'ai jamais eu besoin de rendre le CURSOR global et modifiable.

Outre l'utilisation de ces arguments, le moment de l'exécution est important.

3. Utilisation de SQL CURSOR sur les transactions quotidiennes

Je ne suis pas administrateur. Mais j'ai une idée de ce à quoi ressemble un serveur occupé à partir des outils du DBA (ou du nombre de décibels que crient les utilisateurs). Dans ces circonstances, voudrez-vous ajouter un fardeau supplémentaire ?

Si vous essayez de créer votre code avec un CURSEUR pour les transactions quotidiennes, détrompez-vous. Les CURSOR conviennent aux exécutions uniques sur un serveur moins occupé avec de petits ensembles de données. Cependant, lors d'une journée chargée typique, un CURSEUR peut :

  • Verrouiller les lignes, en particulier si l'argument de concurrence SCROLL_LOCKS est explicitement spécifié.
  • Entraîner une utilisation intensive du processeur.
  • Utilisez tempdb abondamment.

Imaginez que vous en ayez plusieurs en cours d'exécution simultanément au cours d'une journée type.

Nous sommes sur le point de terminer, mais il reste une erreur dont nous devons parler.

4. Ne pas évaluer l'impact que SQL CURSOR apporte

Vous savez que les options CURSOR sont bonnes. Pensez-vous que les préciser est suffisant ? Vous avez déjà vu les résultats ci-dessus. Sans les outils, nous n'arriverions pas à la bonne conclusion.

De plus, il y a du code à l'intérieur du CURSEUR . Selon ce qu'il fait, il ajoute plus aux ressources consommées. Ceux-ci peuvent avoir été disponibles pour d'autres processus. L'ensemble de votre infrastructure, votre matériel et la configuration de SQL Server ajouteront plus à l'histoire.

Qu'en est-il du volume de données ? ? Je n'ai utilisé SQL CURSOR que sur quelques centaines d'enregistrements. Cela peut être différent pour vous. Le premier exemple ne prenait que 500 enregistrements car c'était le nombre que j'accepterais d'attendre. 10 000 ou même 1 000 ne l'ont pas coupé. Ils ont mal performé.

En fin de compte, qu'il y en ait moins ou plus, vérifier les lectures logiques, par exemple, peut faire la différence.

Que faire si vous ne vérifiez pas le plan d'exécution, les lectures logiques ou le temps écoulé ? Quelles choses terribles peuvent se produire autres que les blocages de SQL Server ? Nous ne pouvons qu'imaginer toutes sortes de scénarios apocalyptiques. Vous avez compris.

Conclusion

SQL CURSOR fonctionne en traitant les données ligne par ligne. Il a sa place, mais il peut être mauvais si vous ne faites pas attention. C'est comme un outil qui sort rarement de la boîte à outils.

Donc, première chose, essayez de résoudre le problème en utilisant des commandes basées sur des ensembles. Il répond à la plupart de nos besoins SQL. Et si jamais vous utilisez SQL CURSOR, utilisez-le avec les bonnes options. Estimez l'impact avec le plan d'exécution, STATISTICS IO et xEvent Profiler. Ensuite, choisissez le bon moment pour exécuter.

Tout cela rendra votre utilisation de SQL CURSOR un peu meilleure.