W jaki sposób można wywołać procedurę składowaną dla każdego wiersza w tabeli, w której kolumny wiersza są parametrami wejściowymi do sp bez użycia kursora?
W jaki sposób można wywołać procedurę składowaną dla każdego wiersza w tabeli, w której kolumny wiersza są parametrami wejściowymi do sp bez użycia kursora?
Odpowiedzi:
Ogólnie rzecz biorąc, zawsze szukam podejścia opartego na zbiorach (czasami kosztem zmiany schematu).
Jednak ten fragment ma swoje miejsce.
-- 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
Możesz zrobić coś takiego: zamów swoją tabelę, np. CustomerID (używając Sales.Customer
przykładowej tabeli AdventureWorks ) i iteruj po tych klientach, używając pętli 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
To powinno działać z każdą tabelą, o ile możesz zdefiniować jakiś rodzaj ORDER BY
w jakiejś kolumnie.
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, więc nigdy bym nie wprowadził takiego kodu do produkcji, ale spełnia on Twoje wymagania.
Odpowiedź Marca jest dobra (skomentowałbym ją, gdybym mógł wymyślić, jak to zrobić!)
Pomyślałem, że wskazałbym, że może lepiej zmienić pętlę, aby SELECT
jedyna istniała raz (w prawdziwym przypadku, gdy musiałem zrobić to, SELECT
było dość skomplikowane, a dwukrotne napisanie go było ryzykownym problemem związanym z konserwacją).
-- 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
Jeśli możesz przekształcić procedurę składowaną w funkcję zwracającą tabelę, możesz użyć zastosowania krzyżowego.
Na przykład, załóżmy, że masz tabelę klientów i chcesz obliczyć sumę ich zamówień, utworzysz funkcję, która pobierze CustomerID i zwróci sumę.
Możesz to zrobić:
SELECT CustomerID, CustomerSum.Total
FROM Customers
CROSS APPLY ufn_ComputeCustomerTotal(Customers.CustomerID) AS CustomerSum
Gdzie funkcja wyglądałaby tak:
CREATE FUNCTION ComputeCustomerTotal
(
@CustomerID INT
)
RETURNS TABLE
AS
RETURN
(
SELECT SUM(CustomerOrder.Amount) AS Total FROM CustomerOrder WHERE CustomerID = @CustomerID
)
Oczywiście powyższy przykład można wykonać bez funkcji zdefiniowanej przez użytkownika w pojedynczym zapytaniu.
Wadą jest to, że funkcje są bardzo ograniczone - wiele funkcji procedury składowanej nie jest dostępnych w funkcji zdefiniowanej przez użytkownika, a konwertowanie procedury składowanej na funkcję nie zawsze działa.
Użyłbym akceptowanej odpowiedzi, ale inną możliwością jest użycie zmiennej tabeli do przechowywania ponumerowanego zestawu wartości (w tym przypadku tylko pola ID tabeli) i przechodzenie przez te według numeru wiersza z JOIN do tabeli, aby pobierz wszystko, czego potrzebujesz do działania w pętli.
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
W przypadku SQL Server 2005 i nowszych można to zrobić za pomocą CROSS APPLY i funkcji wycenianej w tabeli.
Dla jasności odnoszę się do tych przypadków, w których procedura składowana może zostać przekonwertowana na funkcję wartościowaną w tabeli.
To jest odmiana powyższego rozwiązania n3rds. Nie jest potrzebne sortowanie za pomocą ORDER BY, ponieważ używana jest funkcja MIN ().
Pamiętaj, że ID klienta (lub jakakolwiek inna kolumna liczbowa, której używasz do postępu) musi mieć unikalne ograniczenie. Ponadto, aby było to tak szybkie, jak to możliwe CustomerID musi być indeksowane.
-- 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
Używam tego podejścia na niektórych varcharach, które muszę przejrzeć, umieszczając je najpierw w tabeli tymczasowej, aby nadać im identyfikator.
Jest to odmiana udzielonych już odpowiedzi, ale powinna być skuteczniejsza, ponieważ nie wymaga ORDER BY, COUNT ani MIN / MAX. Jedyną wadą tego podejścia jest to, że musisz utworzyć tabelę tymczasową do przechowywania wszystkich identyfikatorów (założenie jest takie, że masz luki na liście IDklientów).
To powiedziawszy, zgadzam się z @Mark Powell, chociaż, ogólnie rzecz biorąc, podejście oparte na zestawie powinno być nadal lepsze.
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
Zwykle robię to w ten sposób, gdy jest kilka rzędów:
(W przypadku większych zbiorów danych użyłbym jednak jednego z wyżej wymienionych rozwiązań).
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;
Lepszym rozwiązaniem jest
- Skopiuj / wklej kod procedury składowanej
- Połącz ten kod z tabelą, dla której chcesz go ponownie uruchomić (dla każdego wiersza)
To był czysty wynik w formacie tabeli. Chociaż jeśli uruchomisz SP dla każdego wiersza, otrzymasz oddzielny wynik zapytania dla każdej iteracji, co jest brzydkie.
Na wypadek, gdyby kolejność była ważna
--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
Miałem kod produkcyjny, który mógł obsługiwać tylko 20 pracowników jednocześnie, poniżej znajduje się struktura kodu. Właśnie skopiowałem kod produkcyjny i usunąłem rzeczy poniżej.
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
Lubię robić coś podobnego do tego (chociaż nadal jest to bardzo podobne do używania kursora)
[kod]
-- 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
[/kod]
Zauważ, że nie potrzebujesz tożsamości ani kolumny isIterated w tabeli temp / variable, po prostu wolę to zrobić w ten sposób, aby nie musieć usuwać najwyższego rekordu z kolekcji podczas iteracji w pętli.