web-dev-qa-db-fra.com

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

J'ai une procédure stockée qui modifie les données utilisateur d'une certaine manière. Je le passe user_id et il fait sa chose. Je veux exécuter une requête sur une table et puis pour chaque user_id je trouve exécuter la procédure stockée une fois sur ce user_id

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

188
MetaGuru

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

en MS SQL, voici un exemple d'article

notez que les curseurs sont plus lents que les opérations basées sur les ensembles, mais plus rapides que les boucles While en boucle; plus de détails dans cette SO question

ADDENDA 2: si vous souhaitez traiter plus que quelques enregistrements, extrayez-les d'abord dans une table temporaire et passez le curseur sur la table temporaire; cela empêchera SQL d'entrer dans les verrous de table et d'accélérer les opérations

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

229
Steven A. Lowe

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

au sein de la procédure stockée parent, créez une table #temp contenant les données à traiter. Appelez la procédure stockée enfant, la table #temp sera visible et vous pourrez la traiter en utilisant, espérons-le, l'ensemble des données sans curseur ni boucle.

cela dépend vraiment de ce que fait cette procédure stockée enfant. Si vous utilisez UPDATE-ing, vous pouvez "mettre à jour à partir de" en rejoignant la table #temp et effectuer 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 FI, vous pouvez les convertir en plusieurs UPDATE FROM avec 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’esprit de boucle, c’est un véritable gouffre en termes de performances, il provoque le verrouillage/blocage et ralentit le traitement. Si vous effectuez une boucle partout, votre système ne sera pas très évolutif et il sera très difficile d'accélérer lorsque les utilisateurs commenceront à se plaindre des actualisations lentes.

Publiez le contenu de cette procédure que vous souhaitez appeler en boucle, et je parie que 9 fois sur 10, vous pouvez l'écrire pour qu'il fonctionne sur un ensemble de lignes.

53
KM.

Quelque chose comme cette substitution sera nécessaire pour vos noms de tables et 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
9
u07ch

Vous pouvez le faire avec une requête dynamique.

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

Cela ne peut-il pas être fait avec une fonction définie par l'utilisateur pour répliquer quelle que soit la 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'identifiant de l'utilisateur et fait tout ce que vous devez faire avec.

Voir http://www.sqlteam.com/article/user-defined-functions pour un peu plus de contexte

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

(Bien entendu, ma réponse suppose que vous ne souhaitez obtenir que la sortie du SP et que vous ne modifiez pas les données réelles. Je trouve que "modifie les données utilisateur d'une certaine manière" un peu ambiguë par rapport à la question initiale, alors j'ai pensé proposer cette solution comme une solution possible. Cela dépend totalement de ce que vous faites!)

6
randomsequence

Utilisez une variable de table ou une table temporaire.

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

Note latérale: Une fois, je suis tombé sur 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. Toutefois, dans le cas où 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 plutôt table temporaire ):

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

La id est importante.

Remplacez parent et child par de bonnes données, par exemple. identifiants pertinents ou l’ensemble des données à exploiter.

Insérer des données dans la table, 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 de la table:

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 extrait les données de la table temporaire. La deuxième sélection met à jour le @id. MIN renvoie null si aucune ligne n'a été sélectionnée.

Une autre approche consiste à boucler lorsque la table a des lignes, SELECT TOP 1 et à 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;
5
Erk

J'aime la méthode 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, je devais modifier le code comme suit:

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...

3
Tom