Stored procedure SQL Call per ogni riga senza usare un cursore


163

Come si può chiamare una procedura memorizzata per ogni riga in una tabella, in cui le colonne di una riga sono parametri di input per lo sp senza usare un cursore?


3
Quindi, ad esempio, hai una tabella Customer con una colonna customerId e vuoi chiamare SP una volta per ogni riga della tabella, passando l'ID cliente corrispondente come parametro?
Gary McGill,

2
Potresti approfondire il motivo per cui non puoi usare un cursore?
Andomar,

@Gary: forse voglio solo passare il nome del cliente, non necessariamente l'ID. Ma hai ragione.
Johannes Rudolph,

2
@Andomar: puramente scientifico :-)
Johannes Rudolph,

1
Anche questo problema mi infastidisce.
Daniel,

Risposte:


200

In generale cerco sempre un approccio basato su set (a volte a spese di cambiare lo schema).

Tuttavia, questo frammento ha il suo posto ..

-- 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

21
come con la risposta accettata UTILIZZARE CON CATION: A seconda della tabella e della struttura dell'indice, può avere prestazioni molto scarse (O (n ^ 2)) poiché è necessario ordinare e cercare la tabella ogni volta che si enumera.
csauve,

3
Questo non sembra funzionare (l'interruzione non esce mai dal ciclo per me - il lavoro è fatto ma la query gira nel ciclo). Inizializzazione dell'id e controllo di null nella condizione while esce dal ciclo.
dudeNumber4,

8
@@ ROWCOUNT può essere letto solo una volta. Anche le istruzioni IF / PRINT lo imposteranno su 0. Il test per @@ ROWCOUNT deve essere eseguito 'immediatamente' dopo la selezione. Ricontrollerei il tuo codice / ambiente. technet.microsoft.com/en-us/library/ms187316.aspx
Mark Powell

3
Sebbene i loop non siano migliori dei cursori, fai attenzione, possono essere anche peggio: techrepublic.com/blog/the-enterprise-cloud/…
Jaime

1
@Brennan Pope Utilizza l'opzione LOCAL per un CURSORE e verrà distrutta in caso di fallimento. Usa LOCAL FAST_FORWARD e ci sono quasi zero motivi per non usare CURSOR per questo tipo di loop. Sarebbe decisamente superiore a questo ciclo WHILE.
Martin

39

Potresti fare qualcosa del genere: ordina la tua tabella per esempio CustomerID (usando la Sales.Customertabella di esempio AdventureWorks ) e itera su quei clienti usando un ciclo 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

Dovrebbe funzionare con qualsiasi tabella purché sia ​​possibile definire un tipo di un ORDER BYsu una colonna.


@Mitch: sì, vero - un po 'meno sovraccarico. Ma comunque - non è proprio nella mentalità basata su set di SQL
marc_s

6
È anche possibile un'implementazione basata su set?
Johannes Rudolph,

Non conosco alcun modo per raggiungere questo obiettivo, davvero - è un compito molto procedurale per cominciare ....
marc_s

2
@marc_s esegue una funzione / storeprocedure per ogni elemento in una raccolta, che suona come il pane e il burro delle operazioni basate sul set. Il problema probabilmente deriva dal non avere risultati da ciascuno di essi. Vedi "mappa" nella maggior parte dei linguaggi di programmazione funzionale.
Daniel,

4
re: Daniel. Una funzione sì, una procedura memorizzata no. Una procedura memorizzata per definizione può avere effetti collaterali e gli effetti collaterali non sono consentiti nelle query. Allo stesso modo, una "mappa" corretta in un linguaggio funzionale proibisce gli effetti collaterali.
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, quindi non metterei mai questo codice in produzione, ma soddisfa le tue esigenze.


Come fare la stessa cosa quando la procedura restituisce un valore che dovrebbe impostare il valore della riga? (utilizzando una PROCEDURA anziché una funzione perché non è consentita la creazione di funzioni )
user2284570,

@WeihuiGuo perché il codice creato in modo dinamico usando le stringhe è ORRIBILMENTE soggetto al fallimento e al dolore totale nel culo per il debug. Non dovresti assolutamente fare nulla di simile al di fuori di uno che non ha alcuna possibilità di diventare una parte ordinaria di un ambiente di produzione
Marie,

11

La risposta di Marc è buona (commenterei se potessi capire come fare!)
Ho solo pensato di sottolineare che potrebbe essere meglio cambiare il loop in modo SELECTche esista una sola volta (in un caso reale in cui dovevo fare questo, SELECTera piuttosto complesso, e scriverlo due volte era un problema di manutenzione rischioso).

-- 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

L'APPLICAZIONE può essere utilizzato solo con le funzioni ... quindi questo approccio è di gran lunga migliore se non si ha a che fare con le funzioni.
Artur,

Per commentare sono necessarie 50 ripetizioni. Continuate a risolvere tali questioni e si otterrà una maggiore potenza: D stackoverflow.com/help/privileges~~V~~singular~~3rd
SvendK

Penso che questa dovrebbe essere la risposta, semplice e diretta. Grazie mille!
bomba

7

Se è possibile trasformare la procedura memorizzata in una funzione che restituisce una tabella, è possibile utilizzare l'applicazione incrociata.

Ad esempio, supponiamo che tu abbia una tabella di clienti e desideri calcolare la somma dei loro ordini, creeresti una funzione che ha preso un ID cliente e ha restituito la somma.

E potresti farlo:

SELECT CustomerID, CustomerSum.Total

FROM Customers
CROSS APPLY ufn_ComputeCustomerTotal(Customers.CustomerID) AS CustomerSum

Dove sarebbe la funzione:

CREATE FUNCTION ComputeCustomerTotal
(
    @CustomerID INT
)
RETURNS TABLE
AS
RETURN
(
    SELECT SUM(CustomerOrder.Amount) AS Total FROM CustomerOrder WHERE CustomerID = @CustomerID
)

Ovviamente, l'esempio sopra potrebbe essere fatto senza una funzione definita dall'utente in una singola query.

Lo svantaggio è che le funzioni sono molto limitate: molte delle funzionalità di una procedura memorizzata non sono disponibili in una funzione definita dall'utente e la conversione di una procedura memorizzata in una funzione non sempre funziona.


Nel caso non ci siano le autorizzazioni di scrittura per la creazione di una funzione?
user2284570,

7

Vorrei utilizzare la risposta accettata, ma un'altra possibilità è quella di utilizzare una variabile di tabella per contenere un set numerato di valori (in questo caso solo il campo ID di una tabella) e scorrere tra quelli per Numero riga con un JOIN alla tabella per recupera tutto ciò di cui hai bisogno per l'azione all'interno del loop.

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

Questo è meglio perché non assume che il valore che stai cercando sia un numero intero o possa essere confrontato in modo sensato.
philw,

Esattamente quello che stavo cercando.
Raithlin,


3

Questa è una variante della soluzione n3rds sopra. Non è necessario alcun ordinamento utilizzando ORDER BY, poiché viene utilizzato MIN ().

Ricorda che CustomerID (o qualsiasi altra colonna numerica che usi per avanzare) deve avere un vincolo univoco. Inoltre, per renderlo il più veloce possibile, è necessario indicizzare CustomerID.

-- 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

Uso questo approccio su alcuni varchar che devo esaminare, inserendoli prima in una tabella temporanea, per dare loro un ID.


2

Se non sai cosa usare un cursore, penso che dovrai farlo esternamente (prendi la tabella, e poi esegui per ogni istruzione e ogni volta chiama lo sp) È lo stesso che usare un cursore, ma solo all'esterno SQL. Perché non usi un cursore?


2

Questa è una variazione delle risposte già fornite, ma dovrebbe avere prestazioni migliori perché non richiede ORDER BY, COUNT o MIN / MAX. L'unico svantaggio di questo approccio è che devi creare una tabella temporanea per contenere tutti gli ID (il presupposto è che hai delle lacune nel tuo elenco di ID cliente).

Detto questo, sono d'accordo con @Mark Powell sebbene, in generale, un approccio basato su set dovrebbe essere ancora migliore.

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

1

Di solito lo faccio in questo modo quando sono abbastanza poche righe:

  1. Selezionare tutti i parametri sproc in un set di dati con SQL Management Studio
  2. Tasto destro del mouse -> Copia
  3. Incolla per eccellere
  4. Crea istruzioni sql a riga singola con una formula come '= "EXEC schema.mysproc @ param =" & A2' in una nuova colonna di Excel. (Dove A2 è la colonna di Excel che contiene il parametro)
  5. Copia l'elenco di istruzioni Excel in una nuova query in SQL Management Studio ed eseguilo.
  6. Fatto.

(Su set di dati più grandi utilizzerei una delle soluzioni sopra menzionate).


4
Non molto utile in situazioni di programmazione, è un trucco unico.
Warren P

1

DELIMITATORE //

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;

1

Una soluzione migliore per questo è

  1. Codice copia / passato della procedura memorizzata
  2. Unisci quel codice con la tabella per la quale desideri eseguirlo di nuovo (per ogni riga)

Questo è stato ottenere un output pulito formattato in tabella. Mentre se si esegue SP per ogni riga, si ottiene un risultato di query separato per ogni iterazione che è brutta.


0

Nel caso in cui l'ordine sia importante

--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

0

Avevo del codice di produzione che poteva gestire solo 20 dipendenti alla volta, di seguito è riportato il framework per il codice. Ho appena copiato il codice di produzione e rimosso le cose di seguito.

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

-1

Mi piace fare qualcosa di simile a questo (anche se è ancora molto simile all'utilizzo di un cursore)

[codice]

-- 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

[/codice]

Nota che non hai bisogno dell'identità o della colonna isIterated sulla tua tabella temporanea / variabile, preferisco semplicemente farlo in questo modo, quindi non devo eliminare il record più alto dalla raccolta mentre eseguo il ciclo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.