Gespeicherte Prozedur für SQL-Aufrufe für jede Zeile ohne Verwendung eines Cursors

163

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?

Johannes Rudolph
quelle
3
Sie haben beispielsweise eine Kundentabelle mit einer Kunden-ID-Spalte und möchten den SP für jede Zeile in der Tabelle einmal aufrufen und die entsprechende Kunden-ID als Parameter übergeben?
Gary McGill
2
Können Sie erläutern, warum Sie keinen Cursor verwenden können?
Andomar
@ Gary: Vielleicht möchte ich nur den Kundennamen übergeben, nicht unbedingt die ID. Aber Sie haben Recht.
Johannes Rudolph
2
@Andomar: Rein wissenschaftlich :-)
Johannes Rudolph
1
Dieses Problem nervt mich auch sehr.
Daniel

Antworten:

199

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
Mark Powell
quelle
21
Wie bei der akzeptierten Antwort. VERWENDUNG MIT KATION: Abhängig von Ihrer Tabelle und Indexstruktur kann die Leistung sehr schlecht sein (O (n ^ 2)), da Sie Ihre Tabelle bei jeder Aufzählung bestellen und durchsuchen müssen.
Csauve
3
Dies scheint nicht zu funktionieren (break beendet die Schleife für mich nie - die Arbeit ist erledigt, aber die Abfrage dreht sich in der Schleife). Das Initialisieren der ID und das Überprüfen auf Null in der while-Bedingung verlässt die Schleife.
dudeNumber4
8
ROWCOUNT kann nur einmal gelesen werden. Sogar IF / PRINT-Anweisungen setzen es auf 0. Der Test für @@ ROWCOUNT muss 'sofort' nach der Auswahl durchgeführt werden. Ich würde Ihren Code / Ihre Umgebung erneut überprüfen. technet.microsoft.com/en-us/library/ms187316.aspx
Mark Powell
3
Schleifen sind zwar nicht besser als Cursor, aber seien Sie vorsichtig, sie können sogar noch schlimmer sein: techrepublic.com/blog/the-enterprise-cloud/…
Jaime
1
@Brennan Pope Verwenden Sie die Option LOCAL für einen CURSOR und dieser wird bei einem Fehler zerstört. Verwenden Sie LOCAL FAST_FORWARD und es gibt fast keine Gründe, CURSORs für diese Art von Schleifen nicht zu verwenden. Es würde definitiv diese WHILE-Schleife übertreffen.
Martin
39

Sie könnten Folgendes tun: Ordnen Sie Ihre Tabelle nach z. B. CustomerID (mithilfe der AdventureWorks- Sales.CustomerBeispieltabelle) 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 BYSpalte definieren können.

marc_s
quelle
@Mitch: Ja, stimmt - ein bisschen weniger Overhead. Aber dennoch - es ist nicht wirklich in der
satzbasierten
6
Ist eine satzbasierte Implementierung überhaupt möglich?
Johannes Rudolph
Ich weiß wirklich nicht, wie ich das erreichen kann - es ist zunächst eine sehr prozedurale Aufgabe ...
marc_s
2
@marc_s führt für jedes Element in einer Sammlung eine Funktion / Speicherprozedur aus, die wie das Brot und die Butter von satzbasierten Operationen klingt. Das Problem entsteht wahrscheinlich dadurch, dass nicht von jedem Ergebnisse erzielt werden. Siehe "Karte" in den meisten funktionalen Programmiersprachen.
Daniel
4
re: Daniel. Eine Funktion ja, eine gespeicherte Prozedur nein. Eine gespeicherte Prozedur kann per Definition Nebenwirkungen haben, und Nebenwirkungen sind in Abfragen nicht zulässig. Ebenso verhindert eine richtige "Karte" in einer funktionalen Sprache Nebenwirkungen.
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, ich würde niemals solchen Code in die Produktion einbauen, aber er erfüllt Ihre Anforderungen.

Thomas Gabriel
quelle
Wie mache ich dasselbe, wenn die Prozedur einen Wert zurückgibt, der den Zeilenwert festlegen soll? (Verwenden eines VERFAHRENS anstelle einer Funktion, da die Erstellung von Funktionen nicht zulässig ist )
user2284570
@WeihuiGuo, weil Code, der dynamisch unter Verwendung von Zeichenfolgen erstellt wird, SCHRECKLICH anfällig für Fehler und ein totaler Schmerz im Hintern zum Debuggen ist. Sie sollten so etwas auf keinen Fall außerhalb eines Einzelstücks tun, das keine Chance hat, ein routinemäßiger Bestandteil einer Produktionsumgebung zu werden
Marie
11

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 SELECTnur einmal existiert (in einem realen Fall, in dem ich musste Wenn Sie dies tun, SELECTwar 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
Maxxx
quelle
Die Anwendung kann nur mit Funktionen verwendet werden. Dieser Ansatz ist also weitaus besser, wenn Sie nicht mit Funktionen zu tun haben möchten.
Artur
Sie benötigen 50 Wiederholungen, um zu kommentieren. Beantworten Sie diese Fragen weiter und Sie erhalten mehr Leistung: D stackoverflow.com/help/privileges
SvendK
Ich denke, dies sollte die Antwort sein, klar und direkt. Vielen Dank!
Bomblike
7

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.

David Griffiths
quelle
Gibt es nicht die Schreibberechtigungen zum Erstellen einer Funktion?
user2284570
7

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
AjV Jsy
quelle
Dies ist besser, da nicht angenommen wird, dass der gesuchte Wert eine Ganzzahl ist oder sinnvoll verglichen werden kann.
Philw
Genau das, wonach ich gesucht habe.
Raithlin
3

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.

berühmt
quelle
2

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?

Dani
quelle
2

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
Adriaan de Beer
quelle
1

Normalerweise mache ich das so, wenn es einige Zeilen sind:

  1. Wählen Sie mit SQL Management Studio alle Sproc-Parameter in einem Dataset aus
  2. Klicken Sie mit der rechten Maustaste -> Kopieren
  3. Einfügen, um zu übertreffen
  4. Erstellen Sie einzeilige SQL-Anweisungen mit einer Formel wie '= "EXEC schema.mysproc @ param =" & A2' in einer neuen Excel-Spalte. (Wobei A2 Ihre Excel-Spalte ist, die den Parameter enthält)
  5. Kopieren Sie die Liste der Excel-Anweisungen in eine neue Abfrage in SQL Management Studio und führen Sie sie aus.
  6. Getan.

(Bei größeren Datensätzen würde ich jedoch eine der oben genannten Lösungen verwenden).

Jonas Stensved
quelle
4
In Programmiersituationen nicht sehr nützlich, das ist ein einmaliger Hack.
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
quelle
1

Eine bessere Lösung dafür ist zu

  1. Code der gespeicherten Prozedur kopieren / einfügen
  2. 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.

Hammad Khan
quelle
0

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
isxaker
quelle
0

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
William Egge
quelle
-1

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.

Kritner
quelle