C'è un modo per eseguire il ciclo attraverso una variabile di tabella in TSQL senza utilizzare un cursore?


243

Diciamo che ho la seguente semplice tabella variabile:

declare @databases table
(
    DatabaseID    int,
    Name        varchar(15),   
    Server      varchar(15)
)
-- insert a bunch rows into @databases

Dichiarare e utilizzare un cursore è la mia unica opzione se volessi scorrere le righe? C'è un altro modo?


3
Anche se non sono sicuro del problema che vedi con l'approccio sopra; Vedi se questo aiuta .. databasejournal.com/features/mssql/article.php/3111031
Gishu

5
Potresti fornirci il motivo per cui vuoi iterare sulle righe, potrebbe esistere un'altra soluzione che non richiede iterazione (e che sono più veloci di un ampio margine nella maggior parte dei casi)
Pop Catalin

d'accordo con pop ... potrebbe non essere necessario un cursore a seconda della situazione. ma non c'è alcun problema con l'uso dei cursori se è necessario
Shawn,

3
Non dichiari perché vuoi evitare un cursore. Tieni presente che un cursore potrebbe essere il modo più semplice per iterare. Potresti aver sentito che i cursori sono "cattivi", ma è davvero un'iterazione su tabelle che è cattiva rispetto alle operazioni basate su set. Se non riesci a evitare l'iterazione, un cursore potrebbe essere il modo migliore. Il blocco è un altro problema con i cursori, ma non è rilevante quando si utilizza una variabile di tabella.
Jacques B

1
L'uso di un cursore non è la tua unica opzione, ma se non hai modo di evitare un approccio riga per riga, sarà l'opzione migliore. I CURSORI sono un costrutto integrato che è più efficiente e meno soggetto a errori rispetto al fare il proprio sciocco ciclo WHILE. La maggior parte delle volte devi solo usare l' STATICopzione per rimuovere il costante ricontrollo delle tabelle di base e il blocco che sono lì di default e fanno credere erroneamente che i CURSORI siano cattivi. @JacquesB molto vicino: ricontrollare per vedere se la riga dei risultati esiste ancora + blocco sono i problemi. E di STATICsolito risolve questo :-).
Solomon Rutzky,

Risposte:


376

Prima di tutto dovresti essere assolutamente sicuro di dover scorrere ogni riga - le operazioni basate su set eseguiranno più velocemente in ogni caso che mi viene in mente e normalmente userò un codice più semplice.

A seconda dei tuoi dati, potrebbe essere possibile eseguire il loop utilizzando solo le SELECTistruzioni come mostrato di seguito:

Declare @Id int

While (Select Count(*) From ATable Where Processed = 0) > 0
Begin
    Select Top 1 @Id = Id From ATable Where Processed = 0

    --Do some processing here

    Update ATable Set Processed = 1 Where Id = @Id 

End

Un'altra alternativa è utilizzare una tabella temporanea:

Select *
Into   #Temp
From   ATable

Declare @Id int

While (Select Count(*) From #Temp) > 0
Begin

    Select Top 1 @Id = Id From #Temp

    --Do some processing here

    Delete #Temp Where Id = @Id

End

L'opzione che dovresti scegliere dipende davvero dalla struttura e dal volume dei tuoi dati.

Nota: se si utilizza SQL Server, sarebbe meglio servirsi utilizzando:

WHILE EXISTS(SELECT * FROM #Temp)

L'uso COUNTdovrà toccare ogni singola riga della tabella, l' EXISTSunica necessità di toccare la prima (vedere la risposta di Josef di seguito).


"Seleziona Top 1 @Id = ID da ATable" deve essere "Seleziona Top 1 @Id = Id da ATable Dove elaborato = 0"
Amzath

10
Se si utilizza SQL Server, vedere la risposta di Josef di seguito per una piccola modifica a quanto sopra.
Polshgiant

3
Puoi spiegare perché è meglio che usare un cursore?
marco-fiset,

5
Ho dato a questo un voto negativo. Perché dovrebbe evitare di usare un cursore? Sta parlando dell'iterazione sulla variabile table , non di una tabella tradizionale. Non credo che i normali aspetti negativi dei cursori si applichino qui. Se è veramente necessaria l'elaborazione riga per riga (e come fai notare, dovrebbe esserne certo prima) allora usare un cursore è una soluzione molto migliore di quelle che descrivi qui.
Pietro,

@peterh Hai ragione. E infatti, di solito puoi evitare quei "lati negativi normali" usando l' STATICopzione che copia il set di risultati in una tabella temporanea, e quindi non stai più bloccando o ricontrollando le tabelle di base :-).
Solomon Rutzky,

132

Solo una breve nota, se si utilizza SQL Server (2008 e versioni successive), gli esempi che hanno:

While (Select Count(*) From #Temp) > 0

Sarebbe meglio servito con

While EXISTS(SELECT * From #Temp)

Il Conte dovrà toccare ogni singola riga della tabella, l' EXISTSunica necessità di toccare la prima.


9
Questa non è una risposta ma un commento / miglioramento sulla risposta di Martynw.
Hammad Khan,

7
Il contenuto di questa nota impone una migliore funzionalità di formattazione rispetto a un commento, che suggerirei di aggiungere alla Risposta.
Custodio,

2
Nelle versioni successive di SQL, Query Optimizer è abbastanza intelligente da sapere che quando scrivi la prima cosa, in realtà intendi la seconda e la ottimizza come tale per evitare la scansione della tabella.
Dan Def,

39

Ecco come lo faccio:

declare @RowNum int, @CustId nchar(5), @Name1 nchar(25)

select @CustId=MAX(USERID) FROM UserIDs     --start with the highest ID
Select @RowNum = Count(*) From UserIDs      --get total number of records
WHILE @RowNum > 0                          --loop until no more records
BEGIN   
    select @Name1 = username1 from UserIDs where USERID= @CustID    --get other info from that row
    print cast(@RowNum as char(12)) + ' ' + @CustId + ' ' + @Name1  --do whatever

    select top 1 @CustId=USERID from UserIDs where USERID < @CustID order by USERID desc--get the next one
    set @RowNum = @RowNum - 1                               --decrease count
END

Nessun cursore, nessuna tabella temporanea, nessuna colonna aggiuntiva. La colonna USERID deve essere un numero intero univoco, come la maggior parte delle chiavi primarie.


26

Definisci la tua tabella temporanea in questo modo -

declare @databases table
(
    RowID int not null identity(1,1) primary key,
    DatabaseID    int,
    Name        varchar(15),   
    Server      varchar(15)
)

-- insert a bunch rows into @databases

Quindi fai questo -

declare @i int
select @i = min(RowID) from @databases
declare @max int
select @max = max(RowID) from @databases

while @i <= @max begin
    select DatabaseID, Name, Server from @database where RowID = @i --do some stuff
    set @i = @i + 1
end

16

Ecco come lo farei:

Select Identity(int, 1,1) AS PK, DatabaseID
Into   #T
From   @databases

Declare @maxPK int;Select @maxPK = MAX(PK) From #T
Declare @pk int;Set @pk = 1

While @pk <= @maxPK
Begin

    -- Get one record
    Select DatabaseID, Name, Server
    From @databases
    Where DatabaseID = (Select DatabaseID From #T Where PK = @pk)

    --Do some processing here
    -- 

    Select @pk = @pk + 1
End

[Modifica] Poiché probabilmente ho saltato la parola "variabile" la prima volta che ho letto la domanda, ecco una risposta aggiornata ...


declare @databases table
(
    PK            int IDENTITY(1,1), 
    DatabaseID    int,
    Name        varchar(15),   
    Server      varchar(15)
)
-- insert a bunch rows into @databases
--/*
INSERT INTO @databases (DatabaseID, Name, Server) SELECT 1,'MainDB', 'MyServer'
INSERT INTO @databases (DatabaseID, Name, Server) SELECT 1,'MyDB',   'MyServer2'
--*/

Declare @maxPK int;Select @maxPK = MAX(PK) From @databases
Declare @pk int;Set @pk = 1

While @pk <= @maxPK
Begin

    /* Get one record (you can read the values into some variables) */
    Select DatabaseID, Name, Server
    From @databases
    Where PK = @pk

    /* Do some processing here */
    /* ... */ 

    Select @pk = @pk + 1
End

4
quindi sostanzialmente stai facendo un cursore, ma senza tutti i vantaggi di un cursore
Shawn,

1
... senza bloccare le tabelle utilizzate durante l'elaborazione ... poiché questo è uno dei vantaggi di un cursore :)
leoinfo,

3
Tabelle? È una tabella VARIABILE - non è possibile accedere contemporaneamente.
DenNukem,

DenNukem, hai ragione, penso di aver "saltato" la parola "variabile" quando ho letto la domanda in quel momento ... Aggiungerò alcune note alla mia risposta iniziale
leoinfo,

Sono d'accordo con DenNukem e Shawn. Perché, perché, perché vai su queste lunghezze per evitare di usare un cursore? Ancora: vuole iterare su una variabile di tabella, non su una tabella tradizionale !!!
Pietro,

10

Se non hai altra scelta che andare riga per riga creando un cursore FAST_FORWARD. Sarà veloce come costruire un ciclo while e molto più facile da mantenere a lungo raggio.

FAST_FORWARD Specifica un cursore FORWARD_ONLY, READ_ONLY con le ottimizzazioni delle prestazioni abilitate. FAST_FORWARD non può essere specificato se è specificato anche SCROLL o FOR_UPDATE.


2
Si! Come ho commentato altrove, non ho ancora visto alcun argomento sul perché NON usare un cursore quando il caso deve scorrere su una variabile di tabella . Un FAST_FORWARDcursore è un'ottima soluzione. (votazione)
peterh,

5

Un altro approccio senza dover modificare lo schema o utilizzare le tabelle temporanee:

DECLARE @rowCount int = 0
  ,@currentRow int = 1
  ,@databaseID int
  ,@name varchar(15)
  ,@server varchar(15);

SELECT @rowCount = COUNT(*)
FROM @databases;

WHILE (@currentRow <= @rowCount)
BEGIN
  SELECT TOP 1
     @databaseID = rt.[DatabaseID]
    ,@name = rt.[Name]
    ,@server = rt.[Server]
  FROM (
    SELECT ROW_NUMBER() OVER (
        ORDER BY t.[DatabaseID], t.[Name], t.[Server]
       ) AS [RowNumber]
      ,t.[DatabaseID]
      ,t.[Name]
      ,t.[Server]
    FROM @databases t
  ) rt
  WHERE rt.[RowNumber] = @currentRow;

  EXEC [your_stored_procedure] @databaseID, @name, @server;

  SET @currentRow = @currentRow + 1;
END

4

Puoi usare un ciclo while:

While (Select Count(*) From #TempTable) > 0
Begin
    Insert Into @Databases...

    Delete From #TempTable Where x = x
End

4

Funzionerà nella versione SQL SERVER 2012.

declare @Rowcount int 
select @Rowcount=count(*) from AddressTable;

while( @Rowcount>0)
  begin 
 select @Rowcount=@Rowcount-1;
 SELECT * FROM AddressTable order by AddressId desc OFFSET @Rowcount ROWS FETCH NEXT 1 ROWS ONLY;
end 

4

Leggero, senza dover creare tabelle extra, se si dispone di un numero intero IDsulla tabella

Declare @id int = 0, @anything nvarchar(max)
WHILE(1=1) BEGIN
  Select Top 1 @anything=[Anything],@id=@id+1 FROM Table WHERE ID>@id
  if(@@ROWCOUNT=0) break;

  --Process @anything

END

3
-- [PO_RollBackOnReject]  'FININV10532'
alter procedure PO_RollBackOnReject
@CaseID nvarchar(100)

AS
Begin
SELECT  *
INTO    #tmpTable
FROM   PO_InvoiceItems where CaseID = @CaseID

Declare @Id int
Declare @PO_No int
Declare @Current_Balance Money


While (Select ROW_NUMBER() OVER(ORDER BY PO_LineNo DESC) From #tmpTable) > 0
Begin
        Select Top 1 @Id = PO_LineNo, @Current_Balance = Current_Balance,
        @PO_No = PO_No
        From #Temp
        update PO_Details
        Set  Current_Balance = Current_Balance + @Current_Balance,
            Previous_App_Amount= Previous_App_Amount + @Current_Balance,
            Is_Processed = 0
        Where PO_LineNumber = @Id
        AND PO_No = @PO_No
        update PO_InvoiceItems
        Set IsVisible = 0,
        Is_Processed= 0
        ,Is_InProgress = 0 , 
        Is_Active = 0
        Where PO_LineNo = @Id
        AND PO_No = @PO_No
End
End

2

Davvero non capisco il motivo per cui dovresti ricorrere all'uso del temuto cursor. Ma ecco un'altra opzione se si utilizza SQL Server versione 2005/2008
Usa ricorsione

declare @databases table
(
    DatabaseID    int,
    Name        varchar(15),   
    Server      varchar(15)
)

--; Insert records into @databases...

--; Recurse through @databases
;with DBs as (
    select * from @databases where DatabaseID = 1
    union all
    select A.* from @databases A 
        inner join DBs B on A.DatabaseID = B.DatabaseID + 1
)
select * from DBs

2

Ho intenzione di fornire la soluzione basata su set.

insert  @databases (DatabaseID, Name, Server)
select DatabaseID, Name, Server 
From ... (Use whatever query you would have used in the loop or cursor)

Questo è molto più veloce di qualsiasi tecnica di looping ed è più facile da scrivere e mantenere.


2

Preferisco utilizzare Offset Fetch se si dispone di un ID univoco che è possibile ordinare la tabella in base a:

DECLARE @TableVariable (ID int, Name varchar(50));
DECLARE @RecordCount int;
SELECT @RecordCount = COUNT(*) FROM @TableVariable;

WHILE @RecordCount > 0
BEGIN
SELECT ID, Name FROM @TableVariable ORDER BY ID OFFSET @RecordCount - 1 FETCH NEXT 1 ROW;
SET @RecordCount = @RecordCount - 1;
END

In questo modo non ho bisogno di aggiungere campi alla tabella o utilizzare una funzione di finestra.


2

È possibile utilizzare un cursore per fare ciò:

create function [dbo] .f_teste_loop restituisce la tabella @tabela (cod int, nome varchar (10)) come inizio

insert into @tabela values (1, 'verde');
insert into @tabela values (2, 'amarelo');
insert into @tabela values (3, 'azul');
insert into @tabela values (4, 'branco');

return;

fine

create procedure [dbo]. [sp_teste_loop] come inizio

DECLARE @cod int, @nome varchar(10);

DECLARE curLoop CURSOR STATIC LOCAL 
FOR
SELECT  
    cod
   ,nome
FROM 
    dbo.f_teste_loop();

OPEN curLoop;

FETCH NEXT FROM curLoop
           INTO @cod, @nome;

WHILE (@@FETCH_STATUS = 0)
BEGIN
    PRINT @nome;

    FETCH NEXT FROM curLoop
           INTO @cod, @nome;
END

CLOSE curLoop;
DEALLOCATE curLoop;

fine


1
La domanda originale non era "Senza usare un cursore"?
Fernando Gonzalez Sanchez,

1

Sono d'accordo con il post precedente che le operazioni basate su set in genere funzioneranno meglio, ma se hai bisogno di scorrere le righe ecco l'approccio che seguirei:

  1. Aggiungi un nuovo campo alla variabile della tabella (Bit di tipo di dati, 0 predefinito)
  2. Inserisci i tuoi dati
  3. Seleziona la prima riga in cui fUsed = 0 (Nota: fUsed è il nome del campo nel passaggio 1)
  4. Esegui qualsiasi elaborazione tu debba fare
  5. Aggiorna il record nella tua variabile di tabella impostando fUsed = 1 per il record
  6. Seleziona il successivo record inutilizzato dalla tabella e ripeti il ​​processo

    DECLARE @databases TABLE  
    (  
        DatabaseID  int,  
        Name        varchar(15),     
        Server      varchar(15),   
        fUsed       BIT DEFAULT 0  
    ) 
    
    -- insert a bunch rows into @databases
    
    DECLARE @DBID INT
    
    SELECT TOP 1 @DBID = DatabaseID from @databases where fUsed = 0 
    
    WHILE @@ROWCOUNT <> 0 and @DBID IS NOT NULL  
    BEGIN  
        -- Perform your processing here  
    
        --Update the record to "used" 
    
        UPDATE @databases SET fUsed = 1 WHERE DatabaseID = @DBID  
    
        --Get the next record  
        SELECT TOP 1 @DBID = DatabaseID from @databases where fUsed = 0   
    END

1

Passaggio 1: Sotto l'istruzione select crea una tabella temporanea con un numero di riga univoco per ciascun record.

select eno,ename,eaddress,mobno int,row_number() over(order by eno desc) as rno into #tmp_sri from emp 

Passaggio 2: dichiarare le variabili richieste

DECLARE @ROWNUMBER INT
DECLARE @ename varchar(100)

Passaggio 3: Prendi il conteggio totale delle righe dalla tabella temporanea

SELECT @ROWNUMBER = COUNT(*) FROM #tmp_sri
declare @rno int

Step4: Loop tabella temporanea basata su un numero di riga univoco creato in temp

while @rownumber>0
begin
  set @rno=@rownumber
  select @ename=ename from #tmp_sri where rno=@rno  **// You can take columns data from here as many as you want**
  set @rownumber=@rownumber-1
  print @ename **// instead of printing, you can write insert, update, delete statements**
end

1

Questo approccio richiede solo una variabile e non elimina alcuna riga da @database. So che ci sono molte risposte qui, ma non ne vedo una che utilizza MIN per ottenere il tuo prossimo ID in questo modo.

DECLARE @databases TABLE
(
    DatabaseID    int,
    Name        varchar(15),   
    Server      varchar(15)
)

-- insert a bunch rows into @databases

DECLARE @CurrID INT

SELECT @CurrID = MIN(DatabaseID)
FROM @databases

WHILE @CurrID IS NOT NULL
BEGIN

    -- Do stuff for @CurrID

    SELECT @CurrID = MIN(DatabaseID)
    FROM @databases
    WHERE DatabaseID > @CurrID

END

1

Ecco la mia soluzione, che utilizza un ciclo infinito, l' BREAKistruzione e la @@ROWCOUNTfunzione. Non sono necessari cursori o tabelle temporanee e ho solo bisogno di scrivere una query per ottenere la riga successiva nella @databasestabella:

declare @databases table
(
    DatabaseID    int,
    [Name]        varchar(15),   
    [Server]      varchar(15)
);


-- Populate the [@databases] table with test data.
insert into @databases (DatabaseID, [Name], [Server])
select X.DatabaseID, X.[Name], X.[Server]
from (values 
    (1, 'Roger', 'ServerA'),
    (5, 'Suzy', 'ServerB'),
    (8675309, 'Jenny', 'TommyTutone')
) X (DatabaseID, [Name], [Server])


-- Create an infinite loop & ensure that a break condition is reached in the loop code.
declare @databaseId int;

while (1=1)
begin
    -- Get the next database ID.
    select top(1) @databaseId = DatabaseId 
    from @databases 
    where DatabaseId > isnull(@databaseId, 0);

    -- If no rows were found by the preceding SQL query, you're done; exit the WHILE loop.
    if (@@ROWCOUNT = 0) break;

    -- Otherwise, do whatever you need to do with the current [@databases] table row here.
    print 'Processing @databaseId #' + cast(@databaseId as varchar(50));
end

Mi sono appena reso conto che @ControlFreak mi ha raccomandato questo approccio; Ho semplicemente aggiunto commenti e un esempio più dettagliato.
Mass Dot Net

0

Questo è il codice che sto usando 2008 R2. Questo codice che sto usando è per costruire indici su campi chiave (SSNO e EMPR_NO) in tutti i racconti

if object_ID('tempdb..#a')is not NULL drop table #a

select 'IF EXISTS (SELECT name FROM sysindexes WHERE name ='+CHAR(39)+''+'IDX_'+COLUMN_NAME+'_'+SUBSTRING(table_name,5,len(table_name)-3)+char(39)+')' 
+' begin DROP INDEX [IDX_'+COLUMN_NAME+'_'+SUBSTRING(table_name,5,len(table_name)-3)+'] ON '+table_schema+'.'+table_name+' END Create index IDX_'+COLUMN_NAME+'_'+SUBSTRING(table_name,5,len(table_name)-3)+ ' on '+ table_schema+'.'+table_name+' ('+COLUMN_NAME+') '   'Field'
,ROW_NUMBER() over (order by table_NAMe) as  'ROWNMBR'
into #a
from INFORMATION_SCHEMA.COLUMNS
where (COLUMN_NAME like '%_SSNO_%' or COLUMN_NAME like'%_EMPR_NO_')
    and TABLE_SCHEMA='dbo'

declare @loopcntr int
declare @ROW int
declare @String nvarchar(1000)
set @loopcntr=(select count(*)  from #a)
set @ROW=1  

while (@ROW <= @loopcntr)
    begin
        select top 1 @String=a.Field 
        from #A a
        where a.ROWNMBR = @ROW
        execute sp_executesql @String
        set @ROW = @ROW + 1
    end 

0
SELECT @pk = @pk + 1

sarebbe meglio:

SET @pk += @pk

Evitare di utilizzare SELEZIONA se non si fa riferimento a tabelle, si stanno semplicemente assegnando valori.

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.