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)?
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.
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
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.
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;
CROSS APPLY
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
.
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.
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:
SELECT
qui sont ensuite utilisées dans un INSERT
peuvent être remplacées par un INSERT INTO...SELECT
instruction, par exemple)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):