web-dev-qa-db-fra.com

Remplacement du curseur pour les débutants

Je voudrais savoir quel est le remplacement général d'un curseur. L'implémentation générale d'un curseur que je vois est

DECLARE @variable INT, @sqlstr NVARCHAR(MAX)

DECLARE cursor_name CURSOR
FOR select_statement --essentially to get an array for @variable 
                     --usually it's a subset of unique ids for accounts, clients, parts, etc

OPEN cursor_name
FETCH NEXT FROM cursor_name INTO @variable
WHILE @@FETCH_STATUS = 0
BEGIN
     SET @sqlstr = N'
     /* some query that uses '+ str(@variable) +' to do dirty work
     such as: go through all our accounts, if it''s some subset (possible new cursor), 
     go through those accounts and connect this way, 
     map those fields and add it to our big uniform table */
     '

     EXEC sp_executesql @sqlstr
FETCH NEXT FROM cursor_name INTO @variable
END

CLOSE cursor_name
DEALLOCATE cursor_name

Étant donné que tant de personnes sont anti-curseur (avec un clin d'œil à SO: Pourquoi les gens détestent-ils les curseurs ) quel est le remplacement général de l'implémentation générale (de préférence SQL Server)?

6
undrline

Ça dépend ™

La possibilité de contourner un ou plusieurs curseurs dépendra de ce qui va être exécuté à l'intérieur de ce curseur. Sans savoir ce qui s'y passe, il n'y a aucun moyen de le savoir. Il se peut qu'il n'y ait pas de solution de contournement et que vous deviez effectuer le traitement ligne par ligne.

Voici quelques exemples.

Ne fonctionne pas en ensembles

Cet exemple est le plus simple, et est simplement le fait que vous pouvez interroger votre ensemble de données entier ou des parties de votre ensemble de données, mais le curseur a été créé et interroge les données ligne par ligne. Les plus courants pour remplacer ceci sont les JOIN, CROSS APPLY/OUTER APPLY Et autres.

Considérez l'ensemble de données suivant:

CREATE TABLE dbo.Lotr(LotrId int, CharacterName varchar(255), Val varchar(255));
CREATE TABLE dbo.LotrAttributes(LotrATtributeId int, LotrId int, AttrVal varchar(255));

INSERT INTO dbo.Lotr(LotrId,CharacterName,Val)
VALUES(1,'Frodo','Ring')
,(2,'Gandalf','Staff');

INSERT INTO dbo.LotrAttributes(LotrId,LotrATtributeId,AttrVal)
VALUES(1,1,'RingAttribute1')
,(1,2,'RingAttribute2')
,(1,3,'RingAttribute3')
,(2,4,'StaffAttribute1')
,(2,5,'StaffAttribute2');

On pourrait essayer de trouver chaque enregistrement et il correspond séparément, en faisant une boucle sur la table Lotr.

Curseur:

DECLARE @LotrID int
DECLARE C CURSOR FOR SELECT LotrId from dbo.Lotr;
OPEN C
FETCH NEXT FROM C INTO @LotrID;
WHILE @@FETCH_STATUS = 0
BEGIN
SELECT LotrATtributeId from dbo.LotrAttributes where LotrId = @LotrID;
FETCH NEXT FROM C INTO @LotrID;
END
CLOSE C
DEALLOCATE C

Résultat en deux jeux de résultats

LotrATtributeId
1
2
3
LotrATtributeId
4
5

Lorsque ce inner join Est utilisé, nous obtenons le même résultat qu'un jeu de résultats.

SELECT LotrATtributeId from dbo.Lotr L
INNER JOIN dbo.LotrAttributes LA 
ON L.LotrId = LA.LotrId;

LotrATtributeId
1
2
3
4
5

Manipulation de chaînes

Une méthode courante consiste à utiliser FOR XML PATH('') pour remplacer les manipulations de chaînes à l'intérieur des curseurs.

Ensemble de données

CREATE TABLE dbo.Lotr(LotrId int, CharacterName varchar(255), Val varchar(255));
CREATE TABLE dbo.LotrAttributes(LotrATtributeId int, LotrId int, AttrVal varchar(255));

INSERT INTO dbo.Lotr(LotrId,CharacterName,Val)
VALUES(1,'Frodo','Ring');

INSERT INTO dbo.LotrAttributes(LotrId,LotrATtributeId,AttrVal)
VALUES(1,1,'RingAttribute1')
,(1,2,'RingAttribute2')
,(1,3,'RingAttribute3');

Double curseur avec manipulation de chaîne

DECLARE @LotrId int, @CharacterName varchar(255), @Val varchar(255)
DECLARE @LotrATtributeId int, @AttrVal varchar(255)
DECLARE C CURSOR FOR
SELECT LotrId,CharacterName, Val FROM dbo.Lotr
OPEN C
FETCH NEXT FROM C INTO @LotrId,@CharacterName,@Val
WHILE @@FETCH_STATUS = 0
BEGIN

        SET @CharacterName +='|'+ @Val

        DECLARE D CURSOR FOR
        SELECT LotrATtributeId, AttrVal FROM dbo.LotrAttributes where LotrId = @LotrId
        OPEN D
        FETCH NEXT FROM D INTO @LotrATtributeId,@AttrVal
        WHILE @@FETCH_STATUS = 0
        BEGIN
        SET @CharacterName +='['+@AttrVal+ '],'

        FETCH NEXT FROM D INTO @LotrATtributeId,@AttrVal
        END
        CLOSE D 
        DEALLOCATE D

FETCH NEXT FROM C INTO @LotrId,@CharacterName,@Val
END
CLOSE C
DEALLOCATE C
SELECT LEFT(@CharacterName,len(@charactername)-1);

Résultat

(No column name)
Frodo|Ring[RingAttribute1],[RingAttribute2],[RingAttribute3],

Suppression des curseurs avec FOR XML PATH ('')

SELECT L.Charactername +'|'+ L.Val + (SELECT stuff((SELECT ','+QUOTENAME(AttrVal) FROM dbo.LotrAttributes LA WHERE LA.LotrId = L.LotrId FOR XML PATH('')), 1, 1, ''))
FROM
dbo.Lotr L;

*

La vraie solution ici serait de comprendre pourquoi les données sont présentées de cette manière, et de changer l'application/... pour ne pas en avoir besoin dans ce format, de les stocker quelque part, ....

Si vos mains sont liées, ce serait la prochaine meilleure chose.


Insérez les 10 premières valeurs dans une table temporaire en fonction des identifiants dans une autre table

Données

CREATE TABLE dbo.sometable (InsertTableId int, val varchar (255)); CRÉER LA TABLE dbo.Top10Table (Top10TableId int, InsertTableId int, val varchar (255));

INSERT INTO dbo.sometable(InsertTableId,val)
VALUES(1,'bla')
,(2,'blabla');
INSERT INTO dbo.Top10Table(Top10TableId,InsertTableId,Val)
VALUES(1,1,'WUW')
,(2,1,'WUW')
,(3,1,'WUW');

Curseur

CREATE TABLE #Top10Values(Top10TableId int, InsertTableId int, val varchar(255))

    DECLARE @InsertTableId int;
    DECLARE C CURSOR FOR select InsertTableId from dbo.sometable;
    OPEN C
    FETCH NEXT FROM C INTO @InsertTableId;
    WHILE @@FETCH_STATUS =0
    BEGIN
    INSERT INTO #Top10Values(Top10TableId,InsertTableId,val)
    SELECT top(10) Top10TableId,InsertTableId,Val FROM dbo.Top10Table 
    where InsertTableId = @InsertTableId
    ORDER BY Top10TableId 

    FETCH NEXT FROM C INTO @InsertTableId;
    END
    CLOSE C
    DEALLOCATE C

    SELECT * FROM  #Top10Values;
    DROP TABLE #Top10Values;

Résultat

Top10TableId    InsertTableId   val
1   1   WUW
2   1   WUW
3   1   WUW

Remplacement du curseur par CROSS APPLY Et un CTE

CREATE TABLE #Top10Values(Top10TableId int, InsertTableId int, val varchar(255));
;WITH CTE 
AS
(
select InsertTableId  from dbo.sometable
)

INSERT INTO #Top10Values(Top10TableId,InsertTableId,val)
SELECT  T1T.Top10TableId,T1T.InsertTableId,T1T.Val 
FROM 
CTE
CROSS APPLY (SELECT TOP (10) Top10TableId,InsertTableId,Val from dbo.Top10Table T1T
WHERE T1T.InsertTableId = CTE.InsertTableId
) T1T ;

SELECT * FROM  #Top10Values;
DROP TABLE #Top10Values;

Autres exemples

  • Un exemple sur le remplacement d'un curseur pour sélectionner un ensemble dynamique d'articles par fournisseur en utilisant CROSS APPLYici .
  • Un exemple d'utilisation de fonctions de fenêtrage pour remplacer un curseur ici .

Parfois, il n'y a pas d'autre choix

Si vous ne pouvez pas travailler dans des ensembles et devez effectuer un traitement ligne par ligne, vous pouvez toujours optimiser le curseur.

L'un des plus grands changements dans l'accélération du curseur est d'y ajouter LOCAL FAST_FORWARD.

DECLARE C CURSOR LOCAL FAST_FORWARD FOR SELECT LotrId from dbo.Lotr

Jetez un œil à cet article de blog par @AaronBertrand où il explique les différences possibles de performances lors de l'utilisation ou non des paramètres de curseur comme LOCAL & FAST_FORWARD.

6
Randi Vertongen

Il n'y a pas de "remplacement général" - vous avez caché tout le "sale boulot" ici, donc il est difficile de dire s'il y a même un remplacement spécifique dans ce cas . Il y a certainement des cas spécifiques où vous traitez un ensemble de lignes une ligne à la fois, que ce soit à l'aide d'un curseur, d'une boucle while ou de tout autre processus itératif, lors de la conversion en un processus basé sur un ensemble qui traite tous les lignes à la fois est beaucoup mieux. Mais il y a d'autres choses qui doivent être effectuées une ligne à la fois, comme exécuter une procédure stockée ou du SQL dynamique par ligne, la même requête sur plusieurs bases de données, etc.

Curseur ou non, les problèmes auxquels vous faites allusion et la liaison sont les mêmes que vous utilisiez déclarer curseur ou une autre structure en boucle (voir ce post ), et ne sont pas pertinents lorsque la chose que vous devez faire a à faire une ligne à la fois de toute façon. Donc, si vous fournissez des détails spécifiques sur ce que fait ce curseur, vous pouvez obtenir des conseils sur la façon de supprimer le curseur (ou que vous ne pouvez pas), mais votre recherche d'une approche magique d'élimination de tous les curseurs que vous pouvez appliquer à tous les scénarios va être assez frustrant pour vous.

Le conseil général pour les nouvelles personnes entrant dans la langue, à mon humble avis, devrait être de toujours penser à ce que vous devez faire sur un ensemble de lignes , par opposition à ce que vous devez faire pour chaque ligne d'un ensemble . La différence de langue est subtile, mais cruciale. Si les gens considèrent le problème comme un ensemble de données au lieu d'un groupe de lignes individuelles, ils sont moins susceptibles d'utiliser un curseur par défaut. Mais s'ils proviennent de différents types de programmation - où l'itératif est le meilleur/seul moyen - autre que de simplement leur enseigner que SQL Server n'est pas optimisé pour fonctionner de cette façon, je ne sais pas qu'il existe un moyen de le rendre évident ou automatique.

Votre question demande toujours un remplacement général, et je crois toujours qu'il n'y a rien de tel.

6
Aaron Bertrand

Doug Lane a réalisé une série de vidéos intitulée "T-SQL Level Up" qui sont sur YouTube. Une partie de la série explore une approche générale pour supprimer les curseurs qui ressemble à ceci:

  • Supprimez tout le langage du curseur (déclarer le curseur, ouvrir, récupérer, tandis que, fermer, désallouer, etc.) et autres déclarations de variables
  • Identifiez les endroits où les opérations basées sur un ensemble peuvent être combinées (les variables remplies par un SELECT qui sont ensuite utilisées dans un INSERT peuvent être remplacées par un INSERT INTO...SELECT instruction, par exemple)
  • Déplacer la logique conditionnelle (IF...ELSE) en WHERE clauses, CASE instructions, sous-requêtes, etc.

Comme les autres bonnes réponses ici l'ont souligné, il n'y a pas de solution miracle pour cela. Mais ces vidéos sont, à mon avis, une approche vraiment intuitive pour résoudre le problème.

Doug passe par trois remplacements de curseur de complexité croissante dans chaque partie, je recommande fortement de regarder (car l'ensemble de l'accord se présente mieux en vidéo):

1
Josh Darnell