Jak wykonać procedurę przechowywaną raz dla każdego wiersza zwróconego przez zapytanie?

206

Mam procedurę składowaną, która w pewien sposób zmienia dane użytkownika. Podaję user_id i robi to. Chcę uruchomić zapytanie w tabeli, a następnie dla każdego identyfikatora użytkownika znajduję procedurę przechowywaną raz dla tego identyfikatora użytkownika

Jak mam napisać zapytanie o to?

MetaGuru
źródło
5
Musisz określić, jaki RDBMS - odpowiedź będzie inna dla SQL Server, Oracle, MySql itp.
Gary.Ray
5
Możliwe, że wcale nie potrzebujesz procedury składowanej. Czy potrafisz dokładnie opisać „co” robi procedura przechowywana? Być może cały proces można wyrazić w postaci pojedynczej instrukcji aktualizacji. W miarę możliwości należy unikać wzorca „wykonaj raz dla każdego rekordu”.
Tomalak,
Z jakiej bazy danych korzystasz?
Użytkownik SO
1
Powinieneś przeczytać ten artykuł ... punkt 2 mówi, że NIE używaj kursorów codeproject.com/KB/database/sqldodont.aspx..mind jestem również przeciwny przedwczesnej optymalizacji.
Michael Prewecki
7
@MichaelPrewecki: Jeśli przeczytasz dalej w tym źle napisanym artykule, zobaczysz, że pozycja 10 to „NIE używaj kursorów po stronie serwera, chyba że wiesz, co robisz”. Myślę, że jest to przypadek „Wiem, co robię”.
Gabe

Odpowiedzi:

246

użyj kursora

DODATEK: [Przykład kursora MS SQL]

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

w MS SQL, oto przykładowy artykuł

zauważ, że kursory są wolniejsze niż operacje oparte na zestawach, ale szybsze niż ręczne pętle while; więcej szczegółów w tym pytaniu SO

DODATEK 2: jeśli będziesz przetwarzał więcej niż kilka rekordów, najpierw przeciągnij je do tabeli tymczasowej i przesuń kursor nad tabelą tymczasową; Zapobiegnie to eskalacji SQL do blokad tabel i przyspieszy działanie

DODATEK 3: i oczywiście, jeśli możesz wstawić cokolwiek procedura przechowywana robi do każdego identyfikatora użytkownika i uruchomić całość jako pojedynczą instrukcję aktualizacji SQL, byłoby to optymalne

Steven A. Lowe
źródło
21
Brakowało „otwartej waluty” po deklaracji - to dawało mi błędy „kursor nie jest otwarty”. Nie mam przedstawiciela do edycji.
Fiona - myaccessible.website
5
Możesz podziękować ludziom, głosując za ich komentarzem. Kto wie, może w ten sposób będą mieli przedstawiciela, który dokona edycji, następnym razem! :-)
Robino
Upewnij się, że sprawdziłeś indeksy w klauzulach JOINS i WHERE w polach używanych w procedurze przechowywanej. Po dodaniu odpowiednich indeksów znacznie przyspieszyłem wywoływanie mojego SP w pętli.
Matthew
1
Dziękujemy za przypomnienie o użyciu tabeli temp, aby uniknąć potencjalnych problemów z blokowaniem spowodowanych długim wykonaniem.
Tony,
Czasami procedura przechowywana jest zbyt duża lub skomplikowana, aby można ją było wstawić bez ryzyka wprowadzenia błędów. Tam, gdzie wydajność nie jest najwyższym priorytetem, wykonywanie SP w pętli kursora jest często najbardziej praktycznym wyborem.
Suncat2000
55

spróbuj zmienić metodę, jeśli chcesz zapętlić!

w ramach nadrzędnej procedury składowanej utwórz tabelę #temp zawierającą dane, które musisz przetworzyć. Wywołaj potomną procedurę przechowywaną, tablica #temp będzie widoczna i możesz ją przetwarzać, miejmy nadzieję, pracując z całym zestawem danych i bez kursora lub pętli.

to naprawdę zależy od tego, co robi ta potomna procedura przechowywana. Jeśli aktualizujesz, możesz „zaktualizować”, dołączając do tabeli #temp i wykonując całą pracę w jednej instrukcji bez pętli. To samo można zrobić dla INSERT i DELETE. Jeśli potrzebujesz wykonać wiele aktualizacji za pomocą IF, możesz przekonwertować je na wiele UPDATE FROMza pomocą tabeli #temp i użyć instrukcji CASE lub GDZIE.

Podczas pracy w bazie danych staraj się stracić nastawienie do zapętlania, jest to prawdziwy drenaż wydajności, spowoduje blokowanie / blokowanie i spowolni przetwarzanie. Jeśli wszędzie zapętlisz, twój system nie będzie skalował się zbyt dobrze i bardzo trudno będzie przyspieszyć, gdy użytkownicy zaczną narzekać na powolne odświeżanie.

Opublikuj treść tej procedury, którą chcesz wywołać w pętli, a ja postawię 9 na 10 razy, możesz napisać ją, aby działała na zestawie wierszy.

KM.
źródło
3
+1 za bardzo dobre obejście, zakładając, że kontrolujesz sproc dziecięcy
Steven A. Lowe
trochę myślenia, to rozwiązanie jest zdecydowanie lepsze!
Encc
7
Preferowane są operacje oparte na zestawie. Należy jednak pamiętać, że modyfikacja SP nie zawsze jest opcją - pomyśl, że dostawca zapewnił rozwiązania. Niektórzy użytkownicy mogą nawet nie mieć widoczności, pozostawiając jedynie opcje kursora lub pętli. W moim sklepie nasi twórcy widzą wszystko, ale jest wiele przeszkód do rozwiązania, jeśli rozwiązanie jest budowane poza aplikacją dostawcy ze względu na wyzwalacze, zagnieżdżone procy, liczbę przetwarzanych rekordów itp. Wiele razy najlepsza opcja, ze względu na złożonością aplikacji jest po prostu przewijanie rekordów.
Steve Mangiameli
11

Takie tabele i nazwy pól będą potrzebne.

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
źródło
2
podczas gdy pętle są wolniejsze niż kursory
Steven A. Lowe
Konstrukcja lub instrukcja SQL Deklaracja kursora nie jest obsługiwana (??)
MetaGuru
9

Możesz to zrobić za pomocą zapytania dynamicznego.

declare @cadena varchar(max) = ''
select @cadena = @cadena + 'exec spAPI ' + ltrim(id) + ';'
from sysobjects;
exec(@cadena);
Dave Rincon
źródło
6

Czy nie można tego zrobić za pomocą funkcji zdefiniowanej przez użytkownika w celu zreplikowania wszystkiego, co robi procedura przechowywana?

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

gdzie udfMyFunction to twoja funkcja, która pobiera identyfikator użytkownika i robi wszystko, co trzeba z nim zrobić.

Zobacz http://www.sqlteam.com/article/user-defined-functions na

Zgadzam się, że w miarę możliwości należy unikać kursorów. I zwykle jest to możliwe!

(oczywiście moja odpowiedź zakłada, że ​​jesteś zainteresowany tylko uzyskaniem danych wyjściowych z SP i że nie zmieniasz rzeczywistych danych. Uważam, że „w pewien sposób zmienia dane użytkownika” trochę niejednoznaczne z pierwotnego pytania, pomyślałem, że zaproponuję to jako możliwe rozwiązanie. Zależy to całkowicie od tego, co robisz!)

losowa sekwencja
źródło
1
OP: „procedura składowana, która w pewien sposób zmienia dane użytkownika” MSDN : Funkcje zdefiniowane przez użytkownika nie mogą być używane do wykonywania działań zmieniających stan bazy danych. Jednak SQLSVR 2014 nie wydaje się mieć z tym problemu
Johnny 5
6

Użyj zmiennej tabeli lub tabeli tymczasowej.

Jak wspomniano wcześniej, kursor jest ostatecznością. Głównie dlatego, że zużywa dużo zasobów, powoduje blokady i może być znakiem, że po prostu nie rozumiesz, jak prawidłowo używać SQL.

Uwaga dodatkowa: Kiedyś natknąłem się na rozwiązanie wykorzystujące kursory do aktualizacji wierszy w tabeli. Po krótkiej analizie okazało się, że całość można zastąpić jednym poleceniem UPDATE. Jednak w tym przypadku, gdy należy wykonać procedurę przechowywaną, pojedyncze polecenie SQL nie będzie działać.

Utwórz zmienną tabelową taką jak ta (jeśli pracujesz z dużą ilością danych lub brakuje pamięci, użyj tabeli tymczasowej ):

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

To idjest ważne.

Zastąpić parent i childniektórych danych dobrych, np odpowiednimi identyfikatorami lub całego zbioru danych, które mają być eksploatowane na.

Wstaw dane do tabeli, np .:

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

Zadeklaruj niektóre zmienne:

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

Na koniec utwórz pętlę while nad danymi w tabeli:

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

Pierwszy wybór pobiera dane z tabeli tymczasowej. Drugi wybór aktualizuje @id.MINzwraca null, jeśli nie wybrano żadnych wierszy.

Alternatywnym podejściem jest zapętlenie, gdy tabela ma wiersze, SELECT TOP 1i usunięcie wybranego wiersza z tabeli temp:

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
źródło
3

Podoba mi się sposób dynamicznego zapytania Dave'a Rincon, ponieważ nie używa on kursorów i jest mały i łatwy. Dziękuję Dave za udostępnienie.

Ale dla moich potrzeb dotyczących Azure SQL i „wyraźnego” w zapytaniu musiałem zmodyfikować kod w następujący sposób:

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;

Mam nadzieję, że to komuś pomoże ...

Tomek
źródło