Comment peut-on 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 spsansà l'aide d'un curseur?
De manière générale, je recherche toujours une approche basée sur un ensemble (parfois au détriment de la modification du 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
Vous pouvez faire quelque chose comme ceci: commandez votre table, par exemple. CustomerID (à l'aide de la table exemple AdventureWorks Sales.Customer
), et effectuez une itération sur les clients 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 BY
sur une colonne.
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, je ne mettrais jamais un tel code en production, mais cela répond à vos besoins.
La réponse de Marc est bonne (je le commenterais si je pouvais trouver un moyen de le faire!)
Je pensais juste que je ferais remarquer qu'il serait peut-être préférable de changer de boucle pour que la SELECT
n'existe qu'une seule fois (dans un cas réel où je devais le faire, la SELECT
était assez complexe, et l'écrire deux fois était risqué problème de maintenance).
-- 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
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 prend un ID client et renvoie la somme.
Et vous pourriez faire ceci:
SELECT CustomerID, CustomerSum.Total
FROM Customers
CROSS APPLY ufn_ComputeCustomerTotal(Customers.CustomerID) AS CustomerSum
Où la fonction ressemblerait à:
CREATE FUNCTION ComputeCustomerTotal
(
@CustomerID INT
)
RETURNS TABLE
AS
RETURN
(
SELECT SUM(CustomerOrder.Amount) AS Total FROM CustomerOrder WHERE CustomerID = @CustomerID
)
Évidemment, l'exemple ci-dessus pourrait être réalisé sans une fonction définie par l'utilisateur dans une requête unique.
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 une fonction ne fonctionne pas toujours.
À partir de SQL Server 2005, vous pouvez le faire avec CROSS APPLY et une fonction table.
Juste pour plus de clarté, je fais référence aux cas où la procédure stockée peut être convertie en une fonction table.
J'utiliserais la réponse acceptée, mais une autre possibilité consisterait à utiliser une variable de table pour contenir un ensemble de valeurs numérotées (dans ce cas, uniquement le champ ID d'une table) et à parcourir celles-ci par Row Number avec un JOIN à 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
Ceci est 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 progresser) doit avoir une contrainte unique. De plus, pour le rendre aussi rapide que possible, l'identifiant client doit être indexé.
-- 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 ID.
Si vous ne savez pas quoi utiliser un curseur, je pense que vous devrez le faire en externe (récupérez la table, puis exécutez-la pour chaque instruction et appelez à chaque fois sp) Cela revient à utiliser un curseur, mais uniquement en dehors de SQL . Pourquoi n'utilisez-vous pas un curseur?
DELIMITEUR //
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;
Je le fais habituellement de cette façon quand il y a pas mal de lignes:
(Sur de plus grands ensembles de données, j’utiliserais l’une des solutions mentionnées ci-dessus).
J'avais un code de production qui ne pouvait gérer que 20 employés à la fois. Vous trouverez ci-dessous le cadre du code. Je viens de copier le code de production et de supprimer des é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
Si 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
Ceci est une variation 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 identifiants (l'hypothèse étant que vous avez des lacunes dans votre liste d'identifiants client).
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
Une meilleure solution pour cela est de
- Code de copie/passé de procédure stockée
- Joignez ce code à la table pour laquelle vous souhaitez l'exécuter à nouveau (pour chaque ligne)
C'était vous obtenez une sortie formatée propre à la table. Tandis que si vous exécutez SP pour chaque ligne, vous obtenez un résultat de requête distinct pour chaque itération qui est moche.