Wie kann man eine gespeicherte Prozedur für jede Zeile in einer Tabelle aufrufen, wobei die Spalten einer Zeile Eingabeparameter für sp sind, ohne einen Cursor zu verwenden?
Wie kann man eine gespeicherte Prozedur für jede Zeile in einer Tabelle aufrufen, wobei die Spalten einer Zeile Eingabeparameter für sp sind, ohne einen Cursor zu verwenden?
Antworten:
Im Allgemeinen suche ich immer nach einem satzbasierten Ansatz (manchmal auf Kosten der Änderung des Schemas).
Dieses Snippet hat jedoch seinen Platz.
-- 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
Sie könnten Folgendes tun: Ordnen Sie Ihre Tabelle nach z. B. CustomerID (mithilfe der AdventureWorks- Sales.Customer
Beispieltabelle) und durchlaufen Sie diese Kunden mithilfe einer WHILE-Schleife:
-- 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
Das sollte mit jeder Tabelle funktionieren, solange Sie eine Art ORDER BY
Spalte definieren können.
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, ich würde niemals solchen Code in die Produktion einbauen, aber er erfüllt Ihre Anforderungen.
Marc's Antwort ist gut (ich würde es kommentieren, wenn ich herausfinden könnte, wie es geht!)
Ich dachte nur, ich würde darauf hinweisen, dass es besser sein könnte, die Schleife so zu ändern, dass die SELECT
nur einmal existiert (in einem realen Fall, in dem ich musste Wenn Sie dies tun, SELECT
war das ziemlich komplex und das zweimalige Schreiben war ein riskantes Wartungsproblem.
-- 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
Wenn Sie die gespeicherte Prozedur in eine Funktion verwandeln können, die eine Tabelle zurückgibt, können Sie cross-apply verwenden.
Angenommen, Sie haben eine Kundentabelle und möchten die Summe ihrer Bestellungen berechnen. Sie würden eine Funktion erstellen, die eine Kunden-ID verwendet und die Summe zurückgibt.
Und Sie könnten dies tun:
SELECT CustomerID, CustomerSum.Total
FROM Customers
CROSS APPLY ufn_ComputeCustomerTotal(Customers.CustomerID) AS CustomerSum
Wo die Funktion aussehen würde:
CREATE FUNCTION ComputeCustomerTotal
(
@CustomerID INT
)
RETURNS TABLE
AS
RETURN
(
SELECT SUM(CustomerOrder.Amount) AS Total FROM CustomerOrder WHERE CustomerID = @CustomerID
)
Offensichtlich könnte das obige Beispiel ohne eine benutzerdefinierte Funktion in einer einzelnen Abfrage durchgeführt werden.
Der Nachteil ist, dass die Funktionen sehr eingeschränkt sind - viele der Funktionen einer gespeicherten Prozedur sind in einer benutzerdefinierten Funktion nicht verfügbar, und die Konvertierung einer gespeicherten Prozedur in eine Funktion funktioniert nicht immer.
Ich würde die akzeptierte Antwort verwenden, aber eine andere Möglichkeit besteht darin, eine Tabellenvariable zu verwenden, um einen nummerierten Satz von Werten (in diesem Fall nur das ID-Feld einer Tabelle) zu speichern und diese nach Zeilennummer mit einem JOIN zur Tabelle zu durchlaufen Rufen Sie alles ab, was Sie für die Aktion innerhalb der Schleife benötigen.
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
Ab SQL Server 2005 können Sie dies mit CROSS APPLY und einer Tabellenwertfunktion tun .
Aus Gründen der Klarheit beziehe ich mich auf die Fälle, in denen die gespeicherte Prozedur in eine Tabellenwertfunktion konvertiert werden kann.
Dies ist eine Variation der obigen n3rds-Lösung. Es ist keine Sortierung mit ORDER BY erforderlich, da MIN () verwendet wird.
Denken Sie daran, dass die Kunden-ID (oder eine andere numerische Spalte, die Sie für den Fortschritt verwenden) eine eindeutige Einschränkung haben muss. Um es so schnell wie möglich zu machen, muss außerdem die Kunden-ID indiziert werden.
-- 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
Ich verwende diesen Ansatz bei einigen Varchars, die ich überprüfen muss, indem ich sie zuerst in eine temporäre Tabelle lege, um ihnen eine ID zu geben.
Wenn Sie nicht wissen, was Sie mit einem Cursor verwenden sollen, müssen Sie dies meiner Meinung nach extern tun (die Tabelle abrufen und dann für jede Anweisung ausführen und jedes Mal den sp aufrufen). Dies entspricht der Verwendung eines Cursors, jedoch nur außerhalb SQL. Warum benutzt du keinen Cursor?
Dies ist eine Variation der bereits bereitgestellten Antworten, sollte jedoch eine bessere Leistung erbringen, da ORDER BY, COUNT oder MIN / MAX nicht erforderlich sind. Der einzige Nachteil bei diesem Ansatz besteht darin, dass Sie eine temporäre Tabelle erstellen müssen, um alle IDs zu speichern (die Annahme ist, dass Sie Lücken in Ihrer Liste der Kunden-IDs haben).
Trotzdem stimme ich @Mark Powell zu, obwohl ein satzbasierter Ansatz im Allgemeinen immer noch besser sein sollte.
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
Normalerweise mache ich das so, wenn es einige Zeilen sind:
(Bei größeren Datensätzen würde ich jedoch eine der oben genannten Lösungen verwenden).
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;
Eine bessere Lösung dafür ist zu
- Code der gespeicherten Prozedur kopieren / einfügen
- Verbinden Sie diesen Code mit der Tabelle, für die Sie ihn erneut ausführen möchten (für jede Zeile).
Auf diese Weise erhalten Sie eine saubere, tabellenformatierte Ausgabe. Wenn Sie SP für jede Zeile ausführen, erhalten Sie für jede hässliche Iteration ein separates Abfrageergebnis.
Falls die Reihenfolge wichtig ist
--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
Ich hatte einen Produktionscode, der nur 20 Mitarbeiter gleichzeitig verarbeiten konnte. Nachfolgend finden Sie den Rahmen für den Code. Ich habe gerade den Produktionscode kopiert und das unten stehende Material entfernt.
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
Ich mache gerne etwas Ähnliches (obwohl es der Verwendung eines Cursors immer noch sehr ähnlich ist)
[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]
Beachten Sie, dass Sie weder die Identität noch die Spalte isIterated in Ihrer temporären / variablen Tabelle benötigen. Ich bevorzuge es einfach so, damit ich den obersten Datensatz nicht aus der Sammlung löschen muss, während ich durch die Schleife iteriere.