Comment exécuter une procédure stockée une fois pour chaque ligne renvoyée par la requête?

206

J'ai une procédure stockée qui modifie les données utilisateur d'une certaine manière. Je lui passe user_id et ça fait son truc. Je veux exécuter une requête sur une table, puis pour chaque user_id, je trouve exécuter la procédure stockée une fois sur cet user_id

Comment pourrais-je écrire une requête pour cela?

MetaGuru
la source
5
Vous devez spécifier ce RDBMS - la réponse sera différente pour SQL Server, Oracle, MySql, etc.
Gary.Ray
5
Les chances sont que vous n'avez pas du tout besoin d'une procédure stockée. Pouvez-vous décrire "ce que" fait exactement la procédure stockée? Peut-être que l'ensemble du processus peut être exprimé en une seule déclaration de mise à jour. Le modèle "ne faire qu'une fois pour chaque enregistrement" devrait généralement être évité, si possible.
Tomalak
Quelle base de données utilisez-vous?
Utilisateur SO
1
Vous devriez lire cet article ... l'article 2 dit NE PAS utiliser les curseurs codeproject.com/KB/database/sqldodont.aspx...mind Je suis également contre l'optimisation prématurée.
Michael Prewecki
7
@MichaelPrewecki: Si vous lisez plus loin dans cet article mal écrit, vous verrez que le point 10 est "NE PAS utiliser les curseurs côté serveur à moins que vous sachiez ce que vous faites." Je pense que c'est un cas de "je sais ce que je fais".
Gabe

Réponses:

246

utiliser un curseur

ADDENDUM: [exemple de curseur MS SQL]

declare @field1 int
declare @field2 int
declare cur CURSOR LOCAL for
    select field1, field2 from sometable where someotherfield is null

open cur

fetch next from cur into @field1, @field2

while @@FETCH_STATUS = 0 BEGIN

    --execute your sproc on each row
    exec uspYourSproc @field1, @field2

    fetch next from cur into @field1, @field2
END

close cur
deallocate cur

dans MS SQL, voici un exemple d'article

notez que les curseurs sont plus lents que les opérations basées sur un ensemble, mais plus rapides que les boucles while manuelles; plus de détails dans cette question SO

ADDENDUM 2: si vous allez traiter plus que quelques enregistrements, insérez-les d'abord dans une table temporaire et passez le curseur sur la table temporaire; cela empêchera SQL de dégénérer en verrous de table et accélérera le fonctionnement

ADDENDUM 3: et bien sûr, si vous pouvez intégrer ce que votre procédure stockée fait à chaque ID utilisateur et exécuter le tout comme une seule instruction de mise à jour SQL, ce serait optimal

Steven A. Lowe
la source
21
Vous avez raté "open cur" après la déclaration - cela me donnait des erreurs "le curseur n'est pas ouvert". Je n'ai pas le représentant pour faire un montage.
Fiona - myaccessible.website
5
Vous pouvez remercier les gens en votant pour leur commentaire. Qui sait, peut-être que de cette façon, ils auront le représentant pour faire le montage, la prochaine fois! :-)
Robino
Assurez-vous de vérifier vos index sur les clauses JOINS et WHERE dans les champs utilisés dans votre procédure stockée. J'ai considérablement accéléré l'appel de mon SP dans une boucle après avoir ajouté les index appropriés.
Matthew
1
Merci pour le rappel de l'utilisation de la table temporaire pour éviter les problèmes de verrouillage potentiels causés par une longue exécution.
Tony
Parfois, la procédure stockée est trop volumineuse ou compliquée à intégrer sans risquer d'introduire des bogues. Lorsque les performances ne sont pas la priorité ultime, l'exécution du SP dans une boucle de curseur est souvent le choix le plus pratique.
Suncat2000
55

essayez de changer votre méthode si vous avez besoin de boucler!

dans la procédure stockée parent, créez une table #temp qui contient les données que vous devez traiter. Appelez la procédure stockée enfant, la table #temp sera visible et vous pouvez la traiter, en espérant travailler avec l'ensemble des données et sans curseur ni boucle.

cela dépend vraiment de ce que fait cette procédure stockée enfant. Si vous effectuez une MISE À JOUR, vous pouvez "mettre à jour à partir de" rejoindre la table #temp et faire tout le travail en une seule instruction sans boucle. La même chose peut être faite pour INSERT et DELETE. Si vous devez effectuer plusieurs mises à jour avec des IF, vous pouvez les convertir en plusieurs UPDATE FROMavec la table #temp et utiliser des instructions CASE ou des conditions WHERE.

Lorsque vous travaillez dans une base de données, essayez de perdre l'état d'esprit de la boucle, c'est un véritable drain de performance, provoquera un verrouillage / blocage et ralentira le traitement. Si vous bouclez partout, votre système ne sera pas très évolutif et sera très difficile à accélérer lorsque les utilisateurs commenceront à se plaindre de rafraîchissements lents.

Publiez le contenu de cette procédure que vous souhaitez appeler dans une boucle, et je parierai 9 fois sur 10, vous pouvez l'écrire pour travailler sur un ensemble de lignes.

KM.
la source
3
+1 pour une très bonne solution de contournement, en supposant que vous contrôlez le sproc enfant
Steven A. Lowe
un peu de réflexion, cette sollution est de loin supérieure!
encc
7
Les opérations basées sur des ensembles sont toujours préférables. Cependant, gardez à l'esprit que la modification d'un SP n'est pas toujours une option - pensez aux solutions fournies par le fournisseur. Certains utilisateurs peuvent même ne pas avoir de visibilité, ne laissant que les options de curseur ou de boucle. Dans ma boutique, nos développeurs peuvent tout voir, mais il y a beaucoup d'obstacles à franchir si une solution est construite en dehors de l'application du fournisseur en raison de déclencheurs, de processus imbriqués, du nombre d'enregistrements manipulés, etc. Souvent, leur meilleure option, en raison de complexité de l'application, consiste simplement à faire défiler les enregistrements.
Steve Mangiameli
11

Quelque chose comme cette substitution sera nécessaire pour vos tables et noms de champs.

Declare @TableUsers Table (User_ID, MyRowCount Int Identity(1,1)
Declare @i Int, @MaxI Int, @UserID nVarchar(50)

Insert into @TableUser
Select User_ID
From Users 
Where (My Criteria)
Select @MaxI = @@RowCount, @i = 1

While @i <= @MaxI
Begin
Select @UserID = UserID from @TableUsers Where MyRowCount = @i
Exec prMyStoredProc @UserID
Select

 @i = @i + 1, @UserID = null
End
u07ch
la source
2
tandis que les boucles sont plus lentes que les curseurs
Steven A. Lowe
La construction ou l'instruction Déclarer le curseur SQL n'est pas prise en charge (??)
MetaGuru
9

Vous pouvez le faire avec une requête dynamique.

declare @cadena varchar(max) = ''
select @cadena = @cadena + 'exec spAPI ' + ltrim(id) + ';'
from sysobjects;
exec(@cadena);
Dave Rincon
la source
6

Cela ne peut-il pas être fait avec une fonction définie par l'utilisateur pour répliquer tout ce que fait votre procédure stockée?

SELECT udfMyFunction(user_id), someOtherField, etc FROM MyTable WHERE WhateverCondition

où udfMyFunction est une fonction que vous créez qui prend l'ID utilisateur et fait tout ce que vous devez en faire.

Voir http://www.sqlteam.com/article/user-defined-functions pour un peu plus d'informations

Je conviens que les curseurs doivent vraiment être évités dans la mesure du possible. Et c'est généralement possible!

(bien sûr, ma réponse présuppose que vous êtes uniquement intéressé à obtenir la sortie du SP et que vous ne modifiez pas les données réelles. Je trouve "modifie les données utilisateur d'une certaine manière" un peu ambiguë par rapport à la question d'origine, alors j'ai pensé que j'offrirais cela comme une solution possible. Tout dépend de ce que vous faites!)

séquence aléatoire
la source
1
OP: "procédure stockée qui modifie les données utilisateur d'une certaine manière" MSDN : les fonctions définies par l'utilisateur ne peuvent pas être utilisées pour effectuer des actions qui modifient l'état de la base de données. Cependant, SQLSVR 2014 ne semble pas avoir de problème avec lui
johnny 5
6

Utilisez une variable de table ou une table temporaire.

Comme cela a été mentionné précédemment, un curseur est un dernier recours. Principalement parce qu'il utilise beaucoup de ressources, émet des verrous et peut être un signe que vous ne comprenez pas comment utiliser SQL correctement.

Note latérale: J'ai rencontré une fois une solution qui utilisait des curseurs pour mettre à jour les lignes d'un tableau. Après un examen minutieux, il s'est avéré que le tout pouvait être remplacé par une seule commande UPDATE. Cependant, dans ce cas, lorsqu'une procédure stockée doit être exécutée, une seule commande SQL ne fonctionnera pas.

Créez une variable de table comme celle-ci (si vous travaillez avec beaucoup de données ou si vous manquez de mémoire, utilisez un plutôt table temporaire ):

DECLARE @menus AS TABLE (
    id INT IDENTITY(1,1),
    parent NVARCHAR(128),
    child NVARCHAR(128));

C'est idimportant.

Remplacez parentet childpar de bonnes données, par exemple des identifiants pertinents ou l'ensemble des données à exploiter.

Insérez des données dans le tableau, par exemple:

INSERT INTO @menus (parent, child) 
  VALUES ('Some name',  'Child name');
...
INSERT INTO @menus (parent,child) 
  VALUES ('Some other name', 'Some other child name');

Déclarez quelques variables:

DECLARE @id INT = 1;
DECLARE @parentName NVARCHAR(128);
DECLARE @childName NVARCHAR(128);

Et enfin, créez une boucle while sur les données du tableau:

WHILE @id IS NOT NULL
BEGIN
    SELECT @parentName = parent,
           @childName = child 
        FROM @menus WHERE id = @id;

    EXEC myProcedure @parent=@parentName, @child=@childName;

    SELECT @id = MIN(id) FROM @menus WHERE id > @id;
END

La première sélection récupère les données de la table temporaire. La seconde sélection met à jour le @id. MINrenvoie null si aucune ligne n'a été sélectionnée.

Une autre approche consiste à boucler pendant que la table contient des lignes SELECT TOP 1et à supprimer la ligne sélectionnée de la table temporaire:

WHILE EXISTS(SELECT 1 FROM @menuIDs) 
BEGIN
    SELECT TOP 1 @menuID = menuID FROM @menuIDs;

    EXEC myProcedure @menuID=@menuID;

    DELETE FROM @menuIDs WHERE menuID = @menuID;
END;
Erk
la source
3

J'aime la manière de requête dynamique de Dave Rincon car elle n'utilise pas de curseurs et est petite et facile. Merci Dave pour le partage.

Mais pour mes besoins sur Azure SQL et avec un "distinct" dans la requête, j'ai dû modifier le code comme ceci:

Declare @SQL nvarchar(max);
-- Set SQL Variable
-- Prepare exec command for each distinctive tenantid found in Machines 
SELECT @SQL = (Select distinct 'exec dbo.sp_S2_Laser_to_cache ' + 
              convert(varchar(8),tenantid) + ';' 
              from Dim_Machine
              where iscurrent = 1
              FOR XML PATH(''))

--for debugging print the sql 
print @SQL;

--execute the generated sql script
exec sp_executesql @SQL;

J'espère que ça aidera quelqu'un...

À M
la source