Pourquoi est-il considéré comme une mauvaise pratique d’utiliser des curseurs dans SQL Server ?

StackOverflow https://stackoverflow.com/questions/58141

Question

Je connaissais certaines raisons de performances dans les 7 jours de SQL, mais les mêmes problèmes existent-ils toujours dans SQL Server 2005 ?Si j'ai un jeu de résultats dans une procédure stockée sur lequel je souhaite agir individuellement, les curseurs sont-ils toujours un mauvais choix ?Si oui, pourquoi?

Était-ce utile?

La solution

Parce que les curseurs occupent de la mémoire et créent des verrous.

Ce que vous faites en réalité, c'est essayer de forcer la technologie basée sur des ensembles à devenir des fonctionnalités non basées sur des ensembles.Et, en toute honnêteté, je dois souligner que les curseurs faire ont une utilité, mais ils sont mal vus car de nombreuses personnes qui ne sont pas habituées à utiliser des solutions basées sur des ensembles utilisent des curseurs au lieu de déterminer la solution basée sur des ensembles.

Mais lorsque vous ouvrez un curseur, vous chargez essentiellement ces lignes en mémoire et les verrouillez, créant ainsi des blocs potentiels.Ensuite, lorsque vous parcourez le curseur, vous apportez des modifications à d'autres tables tout en gardant toute la mémoire et les verrous du curseur ouverts.

Tout cela peut potentiellement entraîner des problèmes de performances pour les autres utilisateurs.

Ainsi, en règle générale, les curseurs sont mal vus.Surtout si c'est la première solution trouvée pour résoudre un problème.

Autres conseils

Les commentaires ci-dessus sur le fait que SQL est un environnement basé sur des ensembles sont tous vrais.Cependant, il arrive parfois que les opérations ligne par ligne soient utiles.Envisagez une combinaison de métadonnées et de Dynamic-SQL.

À titre d'exemple très simple, disons que j'ai plus de 100 enregistrements dans une table qui définissent les noms des tables que je souhaite copier/tronquer/quoi que ce soit.Quel est le meilleur ?Coder en dur le SQL pour faire ce dont j'ai besoin ?Ou parcourir cet ensemble de résultats et utiliser Dynamic-SQL (sp_executesql) pour effectuer les opérations ?

Il n'existe aucun moyen d'atteindre l'objectif ci-dessus en utilisant du SQL basé sur des ensembles.

Alors, utiliser des curseurs ou une boucle while (pseudo-curseurs) ?

Les curseurs SQL conviennent tant que vous utilisez les bonnes options :

INSENSITIVE fera une copie temporaire de votre ensemble de résultats (vous évitant ainsi d'avoir à le faire vous-même pour votre pseudo-curseur).

READ_ONLY garantira qu'aucun verrou n'est maintenu sur le jeu de résultats sous-jacent.Les modifications apportées au jeu de résultats sous-jacent seront reflétées dans les récupérations ultérieures (comme si vous obteniez TOP 1 à partir de votre pseudo-curseur).

FAST_FORWARD créera un curseur optimisé en avant uniquement et en lecture seule.

Découvrez les options disponibles avant de considérer tous les curseurs comme mauvais.

Il existe une solution concernant les curseurs que j'utilise chaque fois que j'en ai besoin.

Je crée une variable de table contenant une colonne d'identité.

insérez-y toutes les données dont j'ai besoin pour travailler.

Créez ensuite un bloc while avec une variable de compteur et sélectionnez les données souhaitées dans la variable de table avec une instruction select où la colonne d'identité correspond au compteur.

De cette façon, je ne verrouille rien et j'utilise beaucoup moins de mémoire et c'est sûr, je ne perdrai rien avec une corruption de mémoire ou quelque chose comme ça.

Et le code de bloc est facile à voir et à gérer.

Ceci est un exemple simple :

DECLARE @TAB TABLE(ID INT IDENTITY, COLUMN1 VARCHAR(10), COLUMN2 VARCHAR(10))

DECLARE @COUNT INT,
        @MAX INT, 
        @CONCAT VARCHAR(MAX), 
        @COLUMN1 VARCHAR(10), 
        @COLUMN2 VARCHAR(10)

SET @COUNT = 1

INSERT INTO @TAB VALUES('TE1S', 'TE21')
INSERT INTO @TAB VALUES('TE1S', 'TE22')
INSERT INTO @TAB VALUES('TE1S', 'TE23')
INSERT INTO @TAB VALUES('TE1S', 'TE24')
INSERT INTO @TAB VALUES('TE1S', 'TE25')

SELECT @MAX = @@IDENTITY

WHILE @COUNT <= @MAX BEGIN
    SELECT @COLUMN1 = COLUMN1, @COLUMN2 = COLUMN2 FROM @TAB WHERE ID = @COUNT

    IF @CONCAT IS NULL BEGIN
        SET @CONCAT = '' 
    END ELSE BEGIN 
        SET @CONCAT = @CONCAT + ',' 
    END

    SET @CONCAT = @CONCAT + @COLUMN1 + @COLUMN2

    SET @COUNT = @COUNT + 1
END

SELECT @CONCAT

Je pense que les curseurs ont une mauvaise réputation parce que les débutants en SQL les découvrent et pensent "Hé, une boucle for !Je sais comment les utiliser ! » et ensuite ils continuent à les utiliser pour tout.

Si vous les utilisez pour ce pour quoi ils sont conçus, je n’y trouve rien à redire.

SQL est un langage basé sur des ensembles - c'est ce qu'il fait de mieux.

Je pense que les curseurs restent un mauvais choix à moins que vous en compreniez suffisamment pour justifier leur utilisation dans des circonstances limitées.

Une autre raison pour laquelle je n'aime pas les curseurs est la clarté.Le bloc curseur est si laid qu'il est difficile de l'utiliser de manière claire et efficace.

Tout cela ayant été dit, là sont certains cas où un curseur est vraiment le meilleur - ce ne sont généralement pas les cas pour lesquels les débutants souhaitent les utiliser.

Parfois, la nature du traitement que vous devez effectuer nécessite des curseurs, mais pour des raisons de performances, il est toujours préférable d'écrire la ou les opérations en utilisant si possible une logique basée sur des ensembles.

Je n'appellerais pas cela une "mauvaise pratique" d'utiliser des curseurs, mais ils consomment plus de ressources sur le serveur (qu'une approche équivalente basée sur des ensembles) et le plus souvent, ils ne sont pas nécessaires.Compte tenu de cela, mon conseil serait d’envisager d’autres options avant de recourir à un curseur.

Il existe plusieurs types de curseurs (vers l'avant uniquement, statiques, keyset, dynamiques).Chacun a des caractéristiques de performances différentes et des frais généraux associés.Assurez-vous d'utiliser le type de curseur approprié pour votre opération.Le transfert uniquement est la valeur par défaut.

Un argument en faveur de l'utilisation d'un curseur est lorsque vous devez traiter et mettre à jour des lignes individuelles, en particulier pour un ensemble de données qui n'a pas de bonne clé unique.Dans ce cas, vous pouvez utiliser la clause FOR UPDATE lors de la déclaration du curseur et traiter les mises à jour avec UPDATE...O COURANT DE.

Notez que les curseurs "côté serveur" étaient populaires (depuis ODBC et OLE DB), mais ADO.NET ne les prend pas en charge, et AFAIK ne le fera jamais.

@ Daniel P -> vous n'avez pas besoin d'utiliser un curseur pour le faire.Vous pouvez facilement utiliser la théorie basée sur les ensembles pour le faire.Par exemple:avec SQL 2008

DECLARE @commandname NVARCHAR(1000) = '';

SELECT @commandname += 'truncate table ' + tablename + '; ';
FROM tableNames;

EXEC sp_executesql @commandname;

fera simplement ce que vous avez dit ci-dessus.Et vous pouvez faire la même chose avec SQL 2000 mais la syntaxe de la requête serait différente.

Cependant, mon conseil est d’éviter autant que possible les curseurs.

Gayam

Il existe très, très peu de cas où l'utilisation d'un curseur est justifiée.Il n'y a presque aucun cas où elle surpassera une requête relationnelle basée sur un ensemble.Parfois, il est plus facile pour un programmeur de penser en termes de boucles, mais l'utilisation d'une logique définie, par exemple pour mettre à jour un grand nombre de lignes dans une table, aboutira à une solution qui ne consiste pas seulement à beaucoup moins de lignes de code SQL, mais ça va beaucoup plus vite, souvent plusieurs ordres de grandeur plus rapide.

Même le curseur d'avance rapide de SQL Server 2005 ne peut pas rivaliser avec les requêtes basées sur des ensembles.Le graphique de dégradation des performances commence souvent à ressembler à une opération n ^ 2 par rapport à une opération basée sur un ensemble, qui a tendance à être plus linéaire à mesure que l'ensemble de données devient très volumineux.

Les curseurs ont leur place, mais je pense que c'est principalement parce qu'ils sont souvent utilisés lorsqu'une seule instruction select suffirait à fournir l'agrégation et le filtrage des résultats.

Éviter les curseurs permet à SQL Server d'optimiser davantage les performances de la requête, ce qui est très important dans les systèmes plus grands.

Les curseurs ne sont généralement pas la maladie, mais un symptôme de celle-ci :ne pas utiliser l'approche basée sur les ensembles (comme mentionné dans les autres réponses).

Ne pas comprendre ce problème et croire simplement qu'éviter le curseur « maléfique » le résoudra peut aggraver les choses.

Par exemple, remplacer l'itération du curseur par un autre code itératif, tel que le déplacement de données vers des tables temporaires ou des variables de table, pour parcourir les lignes d'une manière telle que :

SELECT * FROM @temptable WHERE Id=@counter 

ou

SELECT TOP 1 * FROM @temptable WHERE Id>@lastId

Une telle approche, comme le montre le code d’une autre réponse, aggrave les choses et ne résout pas le problème d’origine.C'est un anti-modèle appelé programmation culte du cargo:ne pas savoir POURQUOI quelque chose ne va pas et donc mettre en œuvre quelque chose de pire pour l'éviter !J'ai récemment modifié ce code (en utilisant un #temptable et aucun index sur l'identité/PK) en un curseur, et la mise à jour d'un peu plus de 10 000 lignes n'a pris que 1 seconde au lieu de près de 3 minutes.Il manque toujours une approche basée sur des ensembles (étant le moindre mal), mais le mieux que j'ai pu faire à ce moment-là.

Un autre symptôme de ce manque de compréhension peut être ce que j’appelle parfois « la maladie d’un objet » :applications de base de données qui gèrent des objets uniques via des couches d'accès aux données ou des mappeurs relationnels objet.Code généralement comme :

var items = new List<Item>();
foreach(int oneId in itemIds)
{
    items.Add(dataAccess.GetItemById(oneId);
}

au lieu de

var items = dataAccess.GetItemsByIds(itemIds);

Le premier inonde généralement la base de données avec des tonnes de SELECT, un aller-retour pour chacun, en particulier lorsque des arbres/graphiques d'objets entrent en jeu et que le fameux problème SELECT N+1 survient.

C'est le côté application qui consiste à ne pas comprendre les bases de données relationnelles et l'approche basée sur les ensembles, de la même manière que les curseurs le sont lors de l'utilisation de code de base de données procédural, comme T-SQL ou PL/SQL !

Le problème fondamental, je pense, est que les bases de données sont conçues et réglées pour des opérations basées sur des ensembles : sélection, mise à jour et suppression de grandes quantités de données en une seule étape rapide en fonction des relations entre les données.

Les logiciels en mémoire, en revanche, sont conçus pour des opérations individuelles. Par conséquent, parcourir un ensemble de données et potentiellement effectuer différentes opérations sur chaque élément en série est ce qu'il fait le mieux.

La boucle n'est pas ce pour quoi la base de données ou l'architecture de stockage sont conçues, et même dans SQL Server 2005, vous n'obtiendrez pas de performances proches de celles que vous obtenez si vous extrayez les données de base définies dans un programme personnalisé et effectuez la boucle en mémoire. , en utilisant des objets/structures de données aussi légers que possible.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top