it-swarm.it

C'è un modo per scorrere una variabile di tabella in SQL senza utilizzare un cursore?

Diciamo che ho la seguente variabile di tabella semplice:

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

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

220
Ray Vega

Prima di tutto dovresti essere assolutamente sicuro di aver bisogno di scorrere ogni riga - le operazioni basate su set saranno più veloci in ogni caso a cui riesco a pensare e normalmente userò un codice più semplice.

A seconda dei dati, è possibile eseguire il ciclo semplicemente utilizzando le istruzioni selezionate 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 è usare 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 devi scegliere dipende in realtà dalla struttura e dal volume dei tuoi dati.

Nota: Se stai usando SQL Server, ti conviene utilizzare:

WHILE EXISTS(SELECT * FROM #Temp)

Usando COUNT dovrà toccare ogni singola riga nella tabella, il EXISTS deve solo toccare il primo (vedi la risposta di Josef sotto).

331
Martynnw

Solo una breve nota, se usi SQL Server (2008 e successivi), gli esempi che hanno:

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

Sarebbe meglio servito con

While EXISTS(SELECT * From #Temp)

Il Conteggio dovrà toccare ogni singola riga della tabella, il EXISTS deve solo toccare il primo.

124
Josef

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.

36
Trevor

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

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] Perché probabilmente ho saltato la parola "variabile" quando ho letto la domanda per la prima volta, 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
16
leoinfo

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 nel lungo periodo.

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.

9
Wes Brown

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
SReiderB

Leggero, senza dover creare tabelle aggiuntive, se sulla tabella è presente un ID intero

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

  --Process @anything

END
3
Control Freak

Puoi usare un ciclo while:

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

    Delete From #TempTable Where x = x
End
3
GateKiller
-- [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
3
Syed Umar Ahmed

Non vedo davvero il motivo per cui dovresti ricorrere all'utilizzo di cursor temuta. 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
Sung M. Kim

Funzionerà nella versione SQL SERVER 2012.

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

while( @Rowcount>0)
  begin 
 select @[email protected];
 SELECT * FROM AddressTable order by AddressId desc OFFSET @Rowcount ROWS FETCH NEXT 1 ROWS ONLY;
end 
2
OrganicCoder

Fornirò 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
HLGEM

Questo approccio richiede solo una variabile e non cancella alcuna riga da @databases. So che ci sono molte risposte qui, ma non ne vedo una che usi MIN per ottenere il tuo prossimo ID come questo.

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
Sean

È possibile utilizzare un cursore per fare questo:

create function [dbo] .f_teste_loop restituisce @tabela table (cod int, nome varchar (10)) come inizia

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

creare la procedura [dbo]. [sp_teste_loop] come inizia

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

Ecco la mia soluzione, che utilizza un ciclo infinito, l'istruzione BREAK e la funzione @@ROWCOUNT. Non sono necessari cursori o tabelle temporanee e ho solo bisogno di scrivere una query per ottenere la riga successiva nella tabella @databases:

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
1
Mass Dot Net

Sono d'accordo con il post precedente che le operazioni basate su set di solito funzionano meglio, ma se hai bisogno di scorrere le righe ecco l'approccio che vorrei prendere:

  1. Aggiungi un nuovo campo alla variabile della tabella (Tipo di dati Bit, default 0)
  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 variabile della tabella impostando fUsed = 1 per il record
  6. Seleziona il record successivo non utilizzato 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
Tim Lentine

Preferisco usare Offset Fetch se hai un ID univoco puoi ordinare la tua tabella:

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

1
Yves A Martin

Passo 1: Sotto l'istruzione select viene creata 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 

Step2: dichiarare le variabili richieste

DECLARE @ROWNUMBER INT
DECLARE @ename varchar(100)

Step3: Prendi il numero totale di righe dalla tabella temporanea

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

Passo 4: tabella temporanea loop basata su un numero di riga univoco creato in temp

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

Questo è il codice che sto usando 2008 R2. Questo codice che sto usando è quello di costruire indici su campi chiave (SSNO ed 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
howmnsk

Selezionare @pk = @pk + 1 sarebbe meglio: SET @pk + = @pk. Evitare l'uso di SELECT se non si fa riferimento alle tabelle si stanno solo assegnando valori.

0
Bob Alley