Procedura składowana wywołań SQL dla każdego wiersza bez użycia kursora

163

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?

Johannes Rudolph
źródło
3
Na przykład masz tabelę Customer z kolumną customerId i chcesz wywołać SP raz dla każdego wiersza w tabeli, przekazując odpowiedni customerId jako parametr?
Gary McGill
2
Czy mógłbyś wyjaśnić, dlaczego nie możesz używać kursora?
Andomar
@Gary: Może po prostu chcę przekazać nazwę klienta, niekoniecznie identyfikator. Ale masz rację.
Johannes Rudolph
2
@Andomar: Czysto naukowy :-)
Johannes Rudolph
1
Ten problem też mnie bardzo wkurza.
Daniel,

Odpowiedzi:

200

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
Mark Powell
źródło
21
tak jak w przypadku zaakceptowanej odpowiedzi UŻYJ Z KACJĄ: W zależności od twojej tabeli i struktury indeksu może to być bardzo słabe (O (n ^ 2)), ponieważ musisz porządkować i przeszukiwać tabelę za każdym razem, gdy wyliczasz.
csauve
3
Wydaje się, że to nie działa (przerwa nigdy nie kończy pętli - praca jest zakończona, ale zapytanie obraca się w pętli). Inicjowanie identyfikatora i sprawdzanie wartości null w warunku while powoduje wyjście z pętli.
dudeNumber4
8
@@ ROWCOUNT można odczytać tylko raz. Nawet instrukcje IF / PRINT ustawią ją na 0. Test dla @@ ROWCOUNT należy wykonać „natychmiast” po dokonaniu wyboru. Sprawdziłbym ponownie twój kod / środowisko. technet.microsoft.com/en-us/library/ms187316.aspx
Mark Powell
3
Natomiast pętle nie są lepsze niż kursory, należy uważać, mogą być jeszcze gorzej: techrepublic.com/blog/the-enterprise-cloud/...
Jaime
1
@Brennan Pope Użyj opcji LOKALNE dla KURSORA, a zostanie on zniszczony w przypadku niepowodzenia. Użyj LOCAL FAST_FORWARD i nie ma prawie żadnych powodów, aby nie używać CURSOR dla tego rodzaju pętli. Zdecydowanie przewyższyłby tę pętlę WHILE.
Martin
39

Możesz zrobić coś takiego: zamów swoją tabelę, np. CustomerID (używając Sales.Customerprzykł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 BYw jakiejś kolumnie.

marc_s
źródło
@Mitch: tak, prawda - trochę mniej narzutów. Ale nadal - tak naprawdę nie jest w mentalności SQL
opartej na zbiorach
6
Czy implementacja oparta na zestawie jest w ogóle możliwa?
Johannes Rudolph
Nie znam żadnego sposobu, aby to osiągnąć, naprawdę - to bardzo proceduralne zadanie na początek ...
marc_s
2
@marc_s wykonuje funkcję / procedurę przechowywania dla każdego elementu w kolekcji, która brzmi jak chleb powszedni operacji opartych na zestawie. Problem prawdopodobnie wynika z braku wyników w każdym z nich. Zobacz „mapę” w większości funkcjonalnych języków programowania.
Daniel,
4
re: Daniel. Funkcja tak, procedura składowana nie. Procedura składowana z definicji może mieć efekty uboczne, a efekty uboczne nie są dozwolone w zapytaniach. Podobnie, właściwa „mapa” w języku funkcjonalnym zabrania efektów ubocznych.
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, więc nigdy bym nie wprowadził takiego kodu do produkcji, ale spełnia on Twoje wymagania.

Thomas Gabriel
źródło
Jak zrobić to samo, gdy procedura zwraca wartość, która powinna ustawić wartość wiersza? (używając PROCEDURY zamiast funkcji, ponieważ tworzenie funkcji jest niedozwolone )
user2284570
@WeihuiGuo, ponieważ kod zbudowany dynamicznie przy użyciu ciągów jest OGROMNIE podatny na awarie i całkowity problem z debugowaniem. Absolutnie nigdy nie powinieneś robić czegoś takiego poza miejscem, które nie ma szans stać się rutynową częścią środowiska produkcyjnego
Marie
11

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 SELECTjedyna istniała raz (w prawdziwym przypadku, gdy musiałem zrobić to, SELECTbył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
Maxxx
źródło
APPLY może być używane tylko z funkcjami ... więc to podejście jest znacznie lepsze, jeśli nie chcesz mieć do czynienia z funkcjami.
Artur
Potrzebujesz 50 powtórzeń, aby skomentować. Odpowiadaj dalej na te pytania, a zyskasz więcej mocy: D stackoverflow.com/help/privileges
SvendK
Myślę, że ta powinna być odpowiedzią, jasną i prostą. Dziękuję Ci bardzo!
bomblike
7

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.

David Griffiths
źródło
W przypadku braku uprawnień do zapisu do tworzenia funkcji?
user2284570
7

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
AjV Jsy
źródło
Jest to lepsze, ponieważ nie zakłada, że ​​wartość, której szukasz, jest liczbą całkowitą lub można ją rozsądnie porównać.
philw
Dokładnie to, czego szukałem.
Raithlin
6

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.

Mitch Wheat
źródło
12
Niezły pomysł, ale funkcja nie może wywołać procedury składowanej
Andomar
3

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.

beruic
źródło
2

Jeśli nie wiesz, czego użyć kursora, myślę, że będziesz musiał to zrobić zewnętrznie (pobierz tabelę, a następnie uruchom dla każdej instrukcji i za każdym razem wywołaj sp) to jest to samo, co użycie kursora, ale tylko na zewnątrz SQL. Dlaczego nie użyjesz kursora?

Dani
źródło
2

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
Adriaan de Beer
źródło
1

Zwykle robię to w ten sposób, gdy jest kilka rzędów:

  1. Wybierz wszystkie parametry SPRO w zestawie danych za pomocą SQL Management Studio
  2. Kliknij prawym przyciskiem myszy -> Kopiuj
  3. Wklej do programu Excel
  4. Utwórz jednowierszowe instrukcje sql z formułą taką jak '= "schemat EXEC.mysproc @ param =" & A2' w nowej kolumnie programu Excel. (Gdzie A2 to kolumna programu Excel zawierająca parametr)
  5. Skopiuj listę instrukcji programu Excel do nowego zapytania w SQL Management Studio i wykonaj.
  6. Gotowe.

(W przypadku większych zbiorów danych użyłbym jednak jednego z wyżej wymienionych rozwiązań).

Jonas Stensved
źródło
4
Niezbyt przydatne w sytuacjach programistycznych, to jednorazowy 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
źródło
1

Lepszym rozwiązaniem jest

  1. Skopiuj / wklej kod procedury składowanej
  2. 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.

Hammad Khan
źródło
0

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

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
William Egge
źródło
-1

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.

Kritner
źródło