Come si può chiamare una procedura memorizzata per ogni riga in una tabella, in cui le colonne di una riga sono parametri di input per lo sp senza usare un cursore?
Come si può chiamare una procedura memorizzata per ogni riga in una tabella, in cui le colonne di una riga sono parametri di input per lo sp senza usare un cursore?
Risposte:
In generale cerco sempre un approccio basato su set (a volte a spese di cambiare lo schema).
Tuttavia, questo frammento ha il suo posto ..
-- 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
Potresti fare qualcosa del genere: ordina la tua tabella per esempio CustomerID (usando la Sales.Customer
tabella di esempio AdventureWorks ) e itera su quei clienti usando un ciclo 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
Dovrebbe funzionare con qualsiasi tabella purché sia possibile definire un tipo di un ORDER BY
su una colonna.
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, quindi non metterei mai questo codice in produzione, ma soddisfa le tue esigenze.
La risposta di Marc è buona (commenterei se potessi capire come fare!)
Ho solo pensato di sottolineare che potrebbe essere meglio cambiare il loop in modo SELECT
che esista una sola volta (in un caso reale in cui dovevo fare questo, SELECT
era piuttosto complesso, e scriverlo due volte era un problema di manutenzione rischioso).
-- 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
Se è possibile trasformare la procedura memorizzata in una funzione che restituisce una tabella, è possibile utilizzare l'applicazione incrociata.
Ad esempio, supponiamo che tu abbia una tabella di clienti e desideri calcolare la somma dei loro ordini, creeresti una funzione che ha preso un ID cliente e ha restituito la somma.
E potresti farlo:
SELECT CustomerID, CustomerSum.Total
FROM Customers
CROSS APPLY ufn_ComputeCustomerTotal(Customers.CustomerID) AS CustomerSum
Dove sarebbe la funzione:
CREATE FUNCTION ComputeCustomerTotal
(
@CustomerID INT
)
RETURNS TABLE
AS
RETURN
(
SELECT SUM(CustomerOrder.Amount) AS Total FROM CustomerOrder WHERE CustomerID = @CustomerID
)
Ovviamente, l'esempio sopra potrebbe essere fatto senza una funzione definita dall'utente in una singola query.
Lo svantaggio è che le funzioni sono molto limitate: molte delle funzionalità di una procedura memorizzata non sono disponibili in una funzione definita dall'utente e la conversione di una procedura memorizzata in una funzione non sempre funziona.
Vorrei utilizzare la risposta accettata, ma un'altra possibilità è quella di utilizzare una variabile di tabella per contenere un set numerato di valori (in questo caso solo il campo ID di una tabella) e scorrere tra quelli per Numero riga con un JOIN alla tabella per recupera tutto ciò di cui hai bisogno per l'azione all'interno del loop.
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
A partire da SQL Server 2005, è possibile farlo con CROSS APPLY e una funzione con valori di tabella.
Per chiarezza, mi riferisco a quei casi in cui la procedura memorizzata può essere convertita in una funzione con valori di tabella.
Questa è una variante della soluzione n3rds sopra. Non è necessario alcun ordinamento utilizzando ORDER BY, poiché viene utilizzato MIN ().
Ricorda che CustomerID (o qualsiasi altra colonna numerica che usi per avanzare) deve avere un vincolo univoco. Inoltre, per renderlo il più veloce possibile, è necessario indicizzare CustomerID.
-- 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
Uso questo approccio su alcuni varchar che devo esaminare, inserendoli prima in una tabella temporanea, per dare loro un ID.
Questa è una variazione delle risposte già fornite, ma dovrebbe avere prestazioni migliori perché non richiede ORDER BY, COUNT o MIN / MAX. L'unico svantaggio di questo approccio è che devi creare una tabella temporanea per contenere tutti gli ID (il presupposto è che hai delle lacune nel tuo elenco di ID cliente).
Detto questo, sono d'accordo con @Mark Powell sebbene, in generale, un approccio basato su set dovrebbe essere ancora migliore.
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
Di solito lo faccio in questo modo quando sono abbastanza poche righe:
(Su set di dati più grandi utilizzerei una delle soluzioni sopra menzionate).
DELIMITATORE //
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;
Una soluzione migliore per questo è
- Codice copia / passato della procedura memorizzata
- Unisci quel codice con la tabella per la quale desideri eseguirlo di nuovo (per ogni riga)
Questo è stato ottenere un output pulito formattato in tabella. Mentre se si esegue SP per ogni riga, si ottiene un risultato di query separato per ogni iterazione che è brutta.
Nel caso in cui l'ordine sia 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
Avevo del codice di produzione che poteva gestire solo 20 dipendenti alla volta, di seguito è riportato il framework per il codice. Ho appena copiato il codice di produzione e rimosso le cose di seguito.
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
Mi piace fare qualcosa di simile a questo (anche se è ancora molto simile all'utilizzo di un cursore)
[codice]
-- 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
[/codice]
Nota che non hai bisogno dell'identità o della colonna isIterated sulla tua tabella temporanea / variabile, preferisco semplicemente farlo in questo modo, quindi non devo eliminare il record più alto dalla raccolta mentre eseguo il ciclo.