Wie führe ich eine gespeicherte Prozedur einmal für jede von der Abfrage zurückgegebene Zeile aus?

206

Ich habe eine gespeicherte Prozedur, die Benutzerdaten auf eine bestimmte Weise ändert. Ich übergebe es user_id und es macht es. Ich möchte eine Abfrage für eine Tabelle ausführen und dann für jede Benutzer-ID die gespeicherte Prozedur einmal für diese Benutzer-ID ausführen

Wie würde ich eine Abfrage dafür schreiben?

MetaGuru
quelle
5
Sie müssen angeben, welches RDBMS - die Antwort wird für SQL Server, Oracle, MySql usw. unterschiedlich sein
Gary.Ray
5
Möglicherweise benötigen Sie überhaupt keine gespeicherte Prozedur. Können Sie genau beschreiben, was die gespeicherte Prozedur tut? Möglicherweise kann der gesamte Prozess als einzelne Aktualisierungsanweisung ausgedrückt werden. Das Muster "Einmal für jeden Datensatz ausführen" sollte nach Möglichkeit generell vermieden werden.
Tomalak
Welche Datenbank verwenden Sie?
SO Benutzer
1
Sie sollten diesen Artikel lesen ... Punkt 2 besagt, dass Sie KEINE Cursor verwenden müssen. Codeproject.com/KB/database/sqldodont.aspx...mind Ich bin auch gegen vorzeitige Optimierung.
Michael Prewecki
7
@MichaelPrewecki: Wenn Sie in diesem schlecht geschriebenen Artikel weiterlesen, werden Sie feststellen, dass Punkt 10 lautet: "Verwenden Sie keine serverseitigen Cursor, es sei denn, Sie wissen, was Sie tun." Ich denke, dies ist ein Fall von "Ich weiß, was ich tue".
Gabe

Antworten:

245

Verwenden Sie einen Cursor

ADDENDUM: [Beispiel für einen MS SQL-Cursor]

declare @field1 int
declare @field2 int
declare cur CURSOR LOCAL for
    select field1, field2 from sometable where someotherfield is null

open cur

fetch next from cur into @field1, @field2

while @@FETCH_STATUS = 0 BEGIN

    --execute your sproc on each row
    exec uspYourSproc @field1, @field2

    fetch next from cur into @field1, @field2
END

close cur
deallocate cur

In MS SQL finden Sie hier einen Beispielartikel

Beachten Sie, dass Cursor langsamer sind als satzbasierte Operationen, aber schneller als manuelle while-Schleifen. Weitere Details in dieser SO-Frage

ADDENDUM 2: Wenn Sie mehr als nur einige Datensätze verarbeiten, ziehen Sie diese zuerst in eine temporäre Tabelle und bewegen Sie den Cursor über die temporäre Tabelle. Dies verhindert, dass SQL in Tabellensperren eskaliert, und beschleunigt den Betrieb

ADDENDUM 3: Wenn Sie natürlich inline setzen können, was Ihre gespeicherte Prozedur mit jeder Benutzer-ID tut, und das Ganze als einzelne SQL-Update-Anweisung ausführen können, wäre dies optimal

Steven A. Lowe
quelle
21
Sie haben 'open cur' nach der Deklaration verpasst - dies gab mir die Fehlermeldung 'Der Cursor ist nicht offen'. Ich habe nicht den Repräsentanten, um eine Bearbeitung durchzuführen.
Fiona - myaccessible.website
5
Sie können sich bei den Leuten bedanken, indem Sie ihren Kommentar hochstimmen. Wer weiß, vielleicht haben sie auf diese Weise den Repräsentanten, der das nächste Mal die Bearbeitung vornimmt! :-)
Robino
Stellen Sie sicher, dass Sie Ihre Indizes für die Klauseln JOINS und WHERE in den Feldern überprüfen, die in Ihrer gespeicherten Prozedur verwendet werden. Ich habe das Aufrufen meines SP in einer Schleife dramatisch beschleunigt, nachdem ich entsprechende Indizes hinzugefügt habe.
Matthew
1
Vielen Dank für die Erinnerung an die Verwendung der temporären Tabelle, um mögliche Sperrprobleme durch lange Ausführung zu vermeiden.
Tony
Manchmal ist die gespeicherte Prozedur zu umfangreich oder kompliziert, um inline zu sein, ohne das Risiko einzugehen, Fehler einzuführen. Wenn die Leistung nicht die höchste Priorität hat, ist die Ausführung des SP in einer Cursorschleife häufig die praktischste Wahl.
Suncat2000
55

Versuchen Sie, Ihre Methode zu ändern, wenn Sie eine Schleife benötigen!

Erstellen Sie in der übergeordneten gespeicherten Prozedur eine # temp-Tabelle, die die Daten enthält, die Sie verarbeiten müssen. Wenn Sie die untergeordnete gespeicherte Prozedur aufrufen, wird die Tabelle #temp angezeigt und Sie können sie verarbeiten. Sie arbeiten hoffentlich mit dem gesamten Datensatz und ohne Cursor oder Schleife.

Dies hängt wirklich davon ab, was diese untergeordnete gespeicherte Prozedur tut. Wenn Sie UPDATE-ing sind, können Sie die Verknüpfung mit der # temp-Tabelle "aktualisieren" und die gesamte Arbeit in einer Anweisung ohne Schleife ausführen. Gleiches gilt für INSERT und DELETEs. Wenn Sie mehrere Aktualisierungen mit IFs durchführen müssen, können Sie diese UPDATE FROMmit der Tabelle #temp in mehrere konvertieren und CASE-Anweisungen oder WHERE-Bedingungen verwenden.

Wenn Sie in einer Datenbank arbeiten und versuchen, die Einstellung zum Schleifen zu verlieren, ist dies ein echter Leistungsverlust, der zum Sperren / Blockieren führt und die Verarbeitung verlangsamt. Wenn Sie überall eine Schleife ausführen, lässt sich Ihr System nicht sehr gut skalieren und ist nur schwer zu beschleunigen, wenn Benutzer sich über langsame Aktualisierungen beschweren.

Veröffentlichen Sie den Inhalt dieser Prozedur, die Sie aufrufen möchten, in einer Schleife, und ich wette 9 von 10 Mal, Sie könnten ihn schreiben, um an einer Reihe von Zeilen zu arbeiten.

KM.
quelle
3
+1 für eine sehr gute Problemumgehung, vorausgesetzt, Sie kontrollieren das Kind sproc
Steven A. Lowe
ein bisschen nachdenken, diese sollution ist bei weitem überlegen!
Encc
7
Set-basierte Operationen sind immer vorzuziehen. Beachten Sie jedoch, dass das Ändern eines SP nicht immer eine Option ist - denken Sie an vom Anbieter bereitgestellte Lösungen. Einige Benutzer haben möglicherweise nicht einmal Sichtbarkeit und lassen nur die Cursor- oder Schleifenoptionen übrig. In meinem Shop können unsere Entwickler alles sehen, aber es gibt viele Hürden zu überwinden, wenn eine Lösung außerhalb der Anwendung des Anbieters aufgrund von Triggern, verschachtelten Prozessen, Anzahl der manipulierten Datensätze usw. erstellt wird Die Komplexität der Anwendung besteht darin, einfach durch die Datensätze zu navigieren.
Steve Mangiameli
11

So etwas wie diese Ersetzungen werden für Ihre Tabellen und Feldnamen benötigt.

Declare @TableUsers Table (User_ID, MyRowCount Int Identity(1,1)
Declare @i Int, @MaxI Int, @UserID nVarchar(50)

Insert into @TableUser
Select User_ID
From Users 
Where (My Criteria)
Select @MaxI = @@RowCount, @i = 1

While @i <= @MaxI
Begin
Select @UserID = UserID from @TableUsers Where MyRowCount = @i
Exec prMyStoredProc @UserID
Select

 @i = @i + 1, @UserID = null
End
u07ch
quelle
2
während Schleifen langsamer sind als Cursor
Steven A. Lowe
Das Declare Cursor SQL Konstrukt oder die Anweisung wird nicht unterstützt (??)
MetaGuru
9

Sie können dies mit einer dynamischen Abfrage tun.

declare @cadena varchar(max) = ''
select @cadena = @cadena + 'exec spAPI ' + ltrim(id) + ';'
from sysobjects;
exec(@cadena);
Dave Rincon
quelle
6

Kann dies nicht mit einer benutzerdefinierten Funktion durchgeführt werden, um zu replizieren, was auch immer Ihre gespeicherte Prozedur tut?

SELECT udfMyFunction(user_id), someOtherField, etc FROM MyTable WHERE WhateverCondition

Dabei ist udfMyFunction eine von Ihnen erstellte Funktion, die die Benutzer-ID aufnimmt und alles tut, was Sie dazu benötigen.

Weitere Hintergrundinformationen finden Sie unter http://www.sqlteam.com/article/user-defined-functions

Ich stimme zu, dass Cursor nach Möglichkeit wirklich vermieden werden sollten. Und normalerweise ist es möglich!

(Natürlich setzt meine Antwort voraus, dass Sie nur daran interessiert sind, die Ausgabe vom SP zu erhalten, und dass Sie die tatsächlichen Daten nicht ändern. Ich finde, "ändert Benutzerdaten auf eine bestimmte Weise" ein wenig mehrdeutig gegenüber der ursprünglichen Frage. Ich dachte, ich würde dies als mögliche Lösung anbieten. Kommt ganz darauf an, was du tust!)

zufällige Folge
quelle
1
OP: "Gespeicherte Prozedur, die Benutzerdaten auf bestimmte Weise ändert" MSDN : Benutzerdefinierte Funktionen können nicht zum Ausführen von Aktionen verwendet werden, die den Datenbankstatus ändern. Allerdings scheint SQLSVR 2014 kein Problem damit zu haben
Johnny 5
6

Verwenden Sie eine Tabellenvariable oder eine temporäre Tabelle.

Wie bereits erwähnt, ist ein Cursor das letzte Mittel. Meistens, weil es viele Ressourcen verwendet, Sperren ausgibt und möglicherweise ein Zeichen dafür ist, dass Sie nicht verstehen, wie SQL richtig verwendet wird.

Randnotiz: Ich bin einmal auf eine Lösung gestoßen, bei der Cursor zum Aktualisieren von Zeilen in einer Tabelle verwendet wurden. Nach einiger Prüfung stellte sich heraus, dass das Ganze durch einen einzigen UPDATE-Befehl ersetzt werden konnte. In diesem Fall, in dem eine gespeicherte Prozedur ausgeführt werden soll, funktioniert ein einzelner SQL-Befehl jedoch nicht.

Erstellen Sie eine Tabellenvariable wie diese (wenn Sie mit vielen Daten arbeiten oder wenig Speicher haben, verwenden Sie stattdessen eine temporäre Tabelle ):

DECLARE @menus AS TABLE (
    id INT IDENTITY(1,1),
    parent NVARCHAR(128),
    child NVARCHAR(128));

Das idist wichtig.

Ersetzen Sie parentund childmit einigen guten Daten, z. B. relevanten Kennungen oder dem gesamten Datensatz, mit dem gearbeitet werden soll.

Fügen Sie Daten in die Tabelle ein, z.

INSERT INTO @menus (parent, child) 
  VALUES ('Some name',  'Child name');
...
INSERT INTO @menus (parent,child) 
  VALUES ('Some other name', 'Some other child name');

Deklarieren Sie einige Variablen:

DECLARE @id INT = 1;
DECLARE @parentName NVARCHAR(128);
DECLARE @childName NVARCHAR(128);

Erstellen Sie abschließend eine while-Schleife über die Daten in der Tabelle:

WHILE @id IS NOT NULL
BEGIN
    SELECT @parentName = parent,
           @childName = child 
        FROM @menus WHERE id = @id;

    EXEC myProcedure @parent=@parentName, @child=@childName;

    SELECT @id = MIN(id) FROM @menus WHERE id > @id;
END

Die erste Auswahl ruft Daten aus der temporären Tabelle ab. Die zweite Auswahl aktualisiert die @id. MINGibt null zurück, wenn keine Zeilen ausgewählt wurden.

Ein alternativer Ansatz besteht darin, eine Schleife zu erstellen, während die Tabelle Zeilen enthält, SELECT TOP 1und die ausgewählte Zeile aus der temporären Tabelle zu entfernen:

WHILE EXISTS(SELECT 1 FROM @menuIDs) 
BEGIN
    SELECT TOP 1 @menuID = menuID FROM @menuIDs;

    EXEC myProcedure @menuID=@menuID;

    DELETE FROM @menuIDs WHERE menuID = @menuID;
END;
Erk
quelle
3

Ich mag die dynamische Abfrage von Dave Rincon, da sie keine Cursor verwendet und klein und einfach ist. Vielen Dank, Dave, für das Teilen.

Aber für meine Anforderungen an Azure SQL und mit einem "eindeutigen" in der Abfrage musste ich den Code wie folgt ändern:

Declare @SQL nvarchar(max);
-- Set SQL Variable
-- Prepare exec command for each distinctive tenantid found in Machines 
SELECT @SQL = (Select distinct 'exec dbo.sp_S2_Laser_to_cache ' + 
              convert(varchar(8),tenantid) + ';' 
              from Dim_Machine
              where iscurrent = 1
              FOR XML PATH(''))

--for debugging print the sql 
print @SQL;

--execute the generated sql script
exec sp_executesql @SQL;

Ich hoffe das hilft jemandem ...

Tom
quelle