Procédure stockée d'appel SQL pour chaque ligne sans utiliser de curseur

163

Comment appeler une procédure stockée pour chaque ligne d'une table, où les colonnes d'une ligne sont des paramètres d'entrée dans le sp sans utiliser de curseur?

Johannes Rudolph
la source
3
Ainsi, par exemple, vous avez une table Customer avec une colonne customerId, et vous souhaitez appeler le SP une fois pour chaque ligne de la table, en passant le customerId correspondant en tant que paramètre?
Gary McGill
2
Pourriez-vous expliquer pourquoi vous ne pouvez pas utiliser de curseur?
Andomar
@Gary: Peut-être que je veux juste transmettre le nom du client, pas nécessairement l'ID. Mais tu as raison.
Johannes Rudolph
2
@Andomar: Purement scientifique :-)
Johannes Rudolph
1
Ce problème me dérange beaucoup aussi.
Daniel

Réponses:

200

D'une manière générale, je recherche toujours une approche basée sur les ensembles (parfois au détriment du changement de schéma).

Cependant, cet extrait a sa place.

-- Declare & init (2008 syntax)
DECLARE @CustomerID INT = 0

-- Iterate over all customers
WHILE (1 = 1) 
BEGIN  

  -- Get next customerId
  SELECT TOP 1 @CustomerID = CustomerID
  FROM Sales.Customer
  WHERE CustomerID > @CustomerId 
  ORDER BY CustomerID

  -- Exit loop if no more customers
  IF @@ROWCOUNT = 0 BREAK;

  -- call your sproc
  EXEC dbo.YOURSPROC @CustomerId

END
Mark Powell
la source
21
comme avec la réponse acceptée UTILISER AVEC CATION: En fonction de votre table et de la structure de l'index, il peut être très peu performant (O (n ^ 2)) car vous devez commander et rechercher votre table à chaque fois que vous énumérez.
csauve
3
Cela ne semble pas fonctionner (break ne quitte jamais la boucle pour moi - le travail est terminé mais la requête tourne dans la boucle). L'initialisation de l'id et la vérification de null dans la condition while quitte la boucle.
dudeNumber4
8
@@ ROWCOUNT ne peut être lu qu'une seule fois. Même les instructions IF / PRINT le mettront à 0. Le test pour @@ ROWCOUNT doit être effectué «immédiatement» après la sélection. Je revérifierais votre code / environnement. technet.microsoft.com/en-us/library/ms187316.aspx
Mark Powell
3
Bien que les boucles ne soient pas meilleures que les curseurs, attention, elles peuvent être encore pires: techrepublic.com/blog/the-enterprise-cloud
Jaime
1
@Brennan Pope Utilisez l'option LOCAL pour un CURSEUR et il sera détruit en cas d'échec. Utilisez LOCAL FAST_FORWARD et il n'y a pratiquement aucune raison de ne pas utiliser les CURSOR pour ce type de boucles. Il surpasserait certainement cette boucle WHILE.
Martin
39

Vous pouvez faire quelque chose comme ceci: classer votre table par exemple par CustomerID (en utilisant l' Sales.Customerexemple de table AdventureWorks ), et parcourir ces clients en utilisant une boucle WHILE:

-- define the last customer ID handled
DECLARE @LastCustomerID INT
SET @LastCustomerID = 0

-- define the customer ID to be handled now
DECLARE @CustomerIDToHandle INT

-- select the next customer to handle    
SELECT TOP 1 @CustomerIDToHandle = CustomerID
FROM Sales.Customer
WHERE CustomerID > @LastCustomerID
ORDER BY CustomerID

-- as long as we have customers......    
WHILE @CustomerIDToHandle IS NOT NULL
BEGIN
    -- call your sproc

    -- set the last customer handled to the one we just handled
    SET @LastCustomerID = @CustomerIDToHandle
    SET @CustomerIDToHandle = NULL

    -- select the next customer to handle    
    SELECT TOP 1 @CustomerIDToHandle = CustomerID
    FROM Sales.Customer
    WHERE CustomerID > @LastCustomerID
    ORDER BY CustomerID
END

Cela devrait fonctionner avec n'importe quelle table tant que vous pouvez définir une sorte de ORDER BYsur une colonne.

marc_s
la source
@Mitch: oui, c'est vrai - un peu moins de frais généraux. Mais encore - ce n'est pas vraiment dans la mentalité basée sur les ensembles de SQL
marc_s
6
Une implémentation basée sur un ensemble est-elle même possible?
Johannes Rudolph
Je ne connais aucun moyen d'y parvenir, vraiment - c'est une tâche très procédurale pour commencer ...
marc_s
2
@marc_s exécute une fonction / procédure de magasin pour chaque élément d'une collection, qui sonne comme le pain et le beurre des opérations basées sur des ensembles. Le problème vient probablement de ne pas avoir de résultats pour chacun d'eux. Voir «carte» dans la plupart des langages de programmation fonctionnels.
Daniel
4
re: Daniel. Une fonction oui, une procédure stockée non. Une procédure stockée par définition peut avoir des effets secondaires, et les effets secondaires ne sont pas autorisés dans les requêtes. De même, une «carte» appropriée dans un langage fonctionnel interdit les effets secondaires.
csauve
28
DECLARE @SQL varchar(max)=''

-- MyTable has fields fld1 & fld2

Select @SQL = @SQL + 'exec myproc ' + convert(varchar(10),fld1) + ',' 
                   + convert(varchar(10),fld2) + ';'
From MyTable

EXEC (@SQL)

Ok, donc je ne mettrais jamais un tel code en production, mais il répond à vos exigences.

Thomas Gabriel
la source
Comment faire la même chose lorsque la procédure retourne une valeur qui doit définir la valeur de la ligne? (en utilisant une PROCÉDURE au lieu d'une fonction car la création de fonction n'est pas autorisée )
user2284570
@WeihuiGuo parce que le code construit dynamiquement à l'aide de chaînes est HORRIBLEMENT sujet à l'échec et une douleur totale à déboguer. Vous ne devriez absolument jamais rien faire de tel en dehors d'un événement ponctuel qui n'a aucune chance de devenir une partie de routine d'un environnement de production
Marie
11

La réponse de Marc est bonne (je la commenterais si je pouvais trouver comment faire!)
Je pensais juste souligner qu'il serait peut-être préférable de changer la boucle pour SELECTque la boucle n'existe qu'une seule fois (dans un cas réel où je devais faire cela, SELECTc'était assez complexe et l'écrire deux fois était un problème de maintenance risqué).

-- define the last customer ID handled
DECLARE @LastCustomerID INT
SET @LastCustomerID = 0
-- define the customer ID to be handled now
DECLARE @CustomerIDToHandle INT
SET @CustomerIDToHandle = 1

-- as long as we have customers......    
WHILE @LastCustomerID <> @CustomerIDToHandle
BEGIN  
  SET @LastCustomerId = @CustomerIDToHandle
  -- select the next customer to handle    
  SELECT TOP 1 @CustomerIDToHandle = CustomerID
  FROM Sales.Customer
  WHERE CustomerID > @LastCustomerId 
  ORDER BY CustomerID

  IF @CustomerIDToHandle <> @LastCustomerID
  BEGIN
      -- call your sproc
  END

END
Maxxx
la source
APPLY ne peut être utilisé qu'avec des fonctions ... donc cette approche est de loin meilleure si vous ne voulez pas avoir à faire avec des fonctions.
Artur
Vous avez besoin de 50 répétitions pour commenter. Continuez à répondre à ces questions et vous obtiendrez plus de puissance: D stackoverflow.com/help/privileges
SvendK
Je pense que celui-ci devrait être la réponse, claire et directe. Merci beaucoup!
bomblike
7

Si vous pouvez transformer la procédure stockée en une fonction qui renvoie une table, vous pouvez utiliser l'application croisée.

Par exemple, supposons que vous ayez une table de clients et que vous souhaitiez calculer la somme de leurs commandes, vous créeriez une fonction qui a pris un CustomerID et renvoyé la somme.

Et vous pouvez faire ceci:

SELECT CustomerID, CustomerSum.Total

FROM Customers
CROSS APPLY ufn_ComputeCustomerTotal(Customers.CustomerID) AS CustomerSum

À quoi ressemblerait la fonction:

CREATE FUNCTION ComputeCustomerTotal
(
    @CustomerID INT
)
RETURNS TABLE
AS
RETURN
(
    SELECT SUM(CustomerOrder.Amount) AS Total FROM CustomerOrder WHERE CustomerID = @CustomerID
)

De toute évidence, l'exemple ci-dessus pourrait être fait sans fonction définie par l'utilisateur dans une seule requête.

L'inconvénient est que les fonctions sont très limitées - de nombreuses fonctionnalités d'une procédure stockée ne sont pas disponibles dans une fonction définie par l'utilisateur et la conversion d'une procédure stockée en fonction ne fonctionne pas toujours.

David Griffiths
la source
Dans le cas où il n'y a pas les autorisations d'écriture pour créer une fonction?
user2284570
7

J'utiliserais la réponse acceptée, mais une autre possibilité consiste à utiliser une variable de table pour contenir un ensemble numéroté de valeurs (dans ce cas, juste le champ ID d'une table) et à parcourir celles-ci par numéro de ligne avec une jointure à la table pour récupérez tout ce dont vous avez besoin pour l'action dans la boucle.

DECLARE @RowCnt int; SET @RowCnt = 0 -- Loop Counter

-- Use a table variable to hold numbered rows containg MyTable's ID values
DECLARE @tblLoop TABLE (RowNum int IDENTITY (1, 1) Primary key NOT NULL,
     ID INT )
INSERT INTO @tblLoop (ID)  SELECT ID FROM MyTable

  -- Vars to use within the loop
  DECLARE @Code NVarChar(10); DECLARE @Name NVarChar(100);

WHILE @RowCnt < (SELECT COUNT(RowNum) FROM @tblLoop)
BEGIN
    SET @RowCnt = @RowCnt + 1
    -- Do what you want here with the data stored in tblLoop for the given RowNum
    SELECT @Code=Code, @Name=LongName
      FROM MyTable INNER JOIN @tblLoop tL on MyTable.ID=tL.ID
      WHERE tl.RowNum=@RowCnt
    PRINT Convert(NVarChar(10),@RowCnt) +' '+ @Code +' '+ @Name
END
AjV Jsy
la source
C'est mieux car cela ne suppose pas que la valeur que vous recherchez est un entier ou peut être comparée raisonnablement.
philw
Exactement ce que je cherchais.
Raithlin
6

À partir de SQL Server 2005, vous pouvez le faire avec CROSS APPLY et une fonction table.

Pour plus de clarté, je fais référence aux cas où la procédure stockée peut être convertie en une fonction table.

Blé Mitch
la source
12
Bonne idée, mais une fonction ne peut pas appeler une procédure stockée
Andomar
3

Il s'agit d'une variante de la solution n3rds ci-dessus. Aucun tri à l'aide de ORDER BY n'est nécessaire, car MIN () est utilisé.

N'oubliez pas que CustomerID (ou toute autre colonne numérique que vous utilisez pour la progression) doit avoir une contrainte unique. De plus, pour le rendre aussi rapide que possible, CustomerID doit être indexé sur.

-- Declare & init
DECLARE @CustomerID INT = (SELECT MIN(CustomerID) FROM Sales.Customer); -- First ID
DECLARE @Data1 VARCHAR(200);
DECLARE @Data2 VARCHAR(200);

-- Iterate over all customers
WHILE @CustomerID IS NOT NULL
BEGIN  

  -- Get data based on ID
  SELECT @Data1 = Data1, @Data2 = Data2
    FROM Sales.Customer
    WHERE [ID] = @CustomerID ;

  -- call your sproc
  EXEC dbo.YOURSPROC @Data1, @Data2

  -- Get next customerId
  SELECT @CustomerID = MIN(CustomerID)
    FROM Sales.Customer
    WHERE CustomerID > @CustomerId 

END

J'utilise cette approche sur certains varchars que je dois examiner, en les mettant d'abord dans une table temporaire, pour leur donner un identifiant.

beruic
la source
2

Si vous ne savez pas quoi utiliser un curseur, je pense que vous devrez le faire en externe (obtenir la table, puis exécuter pour chaque instruction et à chaque fois appeler le sp) c'est la même chose que d'utiliser un curseur, mais seulement à l'extérieur SQL. Pourquoi n'utilisez-vous pas de curseur?

Dani
la source
2

Ceci est une variante des réponses déjà fournies, mais devrait être plus performant car il ne nécessite pas ORDER BY, COUNT ou MIN / MAX. Le seul inconvénient de cette approche est que vous devez créer une table temporaire pour contenir tous les ID (l'hypothèse est que vous avez des lacunes dans votre liste de CustomerID).

Cela dit, je suis d'accord avec @Mark Powell bien que, d'une manière générale, une approche basée sur un ensemble devrait toujours être meilleure.

DECLARE @tmp table (Id INT IDENTITY(1,1) PRIMARY KEY NOT NULL, CustomerID INT NOT NULL)
DECLARE @CustomerId INT 
DECLARE @Id INT = 0

INSERT INTO @tmp SELECT CustomerId FROM Sales.Customer

WHILE (1=1)
BEGIN
    SELECT @CustomerId = CustomerId, @Id = Id
    FROM @tmp
    WHERE Id = @Id + 1

    IF @@rowcount = 0 BREAK;

    -- call your sproc
    EXEC dbo.YOURSPROC @CustomerId;
END
Adriaan de Beer
la source
1

Je le fais généralement de cette façon quand il y a pas mal de lignes:

  1. Sélectionnez tous les paramètres de sproc dans un ensemble de données avec SQL Management Studio
  2. Clic droit -> Copier
  3. Coller pour exceller
  4. Créez des instructions SQL à une seule ligne avec une formule telle que '= "EXEC schema.mysproc @ param =" & A2' dans une nouvelle colonne Excel. (Où A2 est votre colonne Excel contenant le paramètre)
  5. Copiez la liste des instructions Excel dans une nouvelle requête dans SQL Management Studio et exécutez-la.
  6. Terminé.

(Sur de plus grands ensembles de données, j'utiliserais l'une des solutions mentionnées ci-dessus).

Jonas Stensved
la source
4
Pas très utile dans les situations de programmation, c'est un hack ponctuel.
Warren P
1

DELIMITER //

CREATE PROCEDURE setFakeUsers (OUT output VARCHAR(100))
BEGIN

    -- define the last customer ID handled
    DECLARE LastGameID INT;
    DECLARE CurrentGameID INT;
    DECLARE userID INT;

    SET @LastGameID = 0; 

    -- define the customer ID to be handled now

    SET @userID = 0;

    -- select the next game to handle    
    SELECT @CurrentGameID = id
    FROM online_games
    WHERE id > LastGameID
    ORDER BY id LIMIT 0,1;

    -- as long as we have customers......    
    WHILE (@CurrentGameID IS NOT NULL) 
    DO
        -- call your sproc

        -- set the last customer handled to the one we just handled
        SET @LastGameID = @CurrentGameID;
        SET @CurrentGameID = NULL;

        -- select the random bot
        SELECT @userID = userID
        FROM users
        WHERE FIND_IN_SET('bot',baseInfo)
        ORDER BY RAND() LIMIT 0,1;

        -- update the game
        UPDATE online_games SET userID = @userID WHERE id = @CurrentGameID;

        -- select the next game to handle    
        SELECT @CurrentGameID = id
         FROM online_games
         WHERE id > LastGameID
         ORDER BY id LIMIT 0,1;
    END WHILE;
    SET output = "done";
END;//

CALL setFakeUsers(@status);
SELECT @status;
radixxko
la source
1

Une meilleure solution pour cela est de

  1. Copier / coller le code de la procédure stockée
  2. Joignez ce code à la table pour laquelle vous souhaitez l'exécuter à nouveau (pour chaque ligne)

C'est ainsi que vous obtenez une sortie au format table propre. Alors que si vous exécutez SP pour chaque ligne, vous obtenez un résultat de requête distinct pour chaque itération, ce qui est laid.

Hammad Khan
la source
0

Dans le cas où la commande est importante

--declare counter
DECLARE     @CurrentRowNum BIGINT = 0;
--Iterate over all rows in [DataTable]
WHILE (1 = 1)
    BEGIN
        --Get next row by number of row
        SELECT TOP 1 @CurrentRowNum = extendedData.RowNum
                    --here also you can store another values
                    --for following usage
                    --@MyVariable = extendedData.Value
        FROM    (
                    SELECT 
                        data.*
                        ,ROW_NUMBER() OVER(ORDER BY (SELECT 0)) RowNum
                    FROM [DataTable] data
                ) extendedData
        WHERE extendedData.RowNum > @CurrentRowNum
        ORDER BY extendedData.RowNum

        --Exit loop if no more rows
        IF @@ROWCOUNT = 0 BREAK;

        --call your sproc
        --EXEC dbo.YOURSPROC @MyVariable
    END
isxaker
la source
0

J'avais un code de production qui ne pouvait gérer que 20 employés à la fois, voici le cadre du code. Je viens de copier le code de production et de supprimer les éléments ci-dessous.

ALTER procedure GetEmployees
    @ClientId varchar(50)
as
begin
    declare @EEList table (employeeId varchar(50));
    declare @EE20 table (employeeId varchar(50));

    insert into @EEList select employeeId from Employee where (ClientId = @ClientId);

    -- Do 20 at a time
    while (select count(*) from @EEList) > 0
    BEGIN
      insert into @EE20 select top 20 employeeId from @EEList;

      -- Call sp here

      delete @EEList where employeeId in (select employeeId from @EE20)
      delete @EE20;
    END;

  RETURN
end
William Egge
la source
-1

J'aime faire quelque chose de similaire (même si c'est toujours très similaire à l'utilisation d'un curseur)

[code]

-- Table variable to hold list of things that need looping
DECLARE @holdStuff TABLE ( 
    id INT IDENTITY(1,1) , 
    isIterated BIT DEFAULT 0 , 
    someInt INT ,
    someBool BIT ,
    otherStuff VARCHAR(200)
)

-- Populate your @holdStuff with... stuff
INSERT INTO @holdStuff ( 
    someInt ,
    someBool ,
    otherStuff
)
SELECT  
    1 , -- someInt - int
    1 , -- someBool - bit
    'I like turtles'  -- otherStuff - varchar(200)
UNION ALL
SELECT  
    42 , -- someInt - int
    0 , -- someBool - bit
    'something profound'  -- otherStuff - varchar(200)

-- Loop tracking variables
DECLARE @tableCount INT
SET     @tableCount = (SELECT COUNT(1) FROM [@holdStuff])

DECLARE @loopCount INT
SET     @loopCount = 1

-- While loop variables
DECLARE @id INT
DECLARE @someInt INT
DECLARE @someBool BIT
DECLARE @otherStuff VARCHAR(200)

-- Loop through item in @holdStuff
WHILE (@loopCount <= @tableCount)
    BEGIN

        -- Increment the loopCount variable
        SET @loopCount = @loopCount + 1

        -- Grab the top unprocessed record
        SELECT  TOP 1 
            @id = id ,
            @someInt = someInt ,
            @someBool = someBool ,
            @otherStuff = otherStuff
        FROM    @holdStuff
        WHERE   isIterated = 0

        -- Update the grabbed record to be iterated
        UPDATE  @holdAccounts
        SET     isIterated = 1
        WHERE   id = @id

        -- Execute your stored procedure
        EXEC someRandomSp @someInt, @someBool, @otherStuff

    END

[/code]

Notez que vous n'avez pas besoin de l'identité ou de la colonne isIterated sur votre table temp / variable, je préfère simplement le faire de cette façon afin de ne pas avoir à supprimer le premier enregistrement de la collection lorsque je parcours la boucle.

Kritner
la source