it-swarm-pt.tech

Existe uma maneira de percorrer uma variável de tabela no SQL sem usar um cursor?

Digamos que eu tenha a seguinte variável de tabela simples:

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

Declarar e usar um cursor é minha única opção se eu quiser iterar pelas linhas? Existe outro caminho?

220
Ray Vega

Antes de mais nada, você deve ter absoluta certeza de que precisa fazer uma iteração por meio de cada operação baseada em set de linha. Ela terá um desempenho mais rápido em todos os casos em que eu possa pensar e normalmente utilizará um código mais simples.

Dependendo dos seus dados, pode ser possível fazer um loop apenas usando as instruções selecionadas, conforme mostrado abaixo:

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

Outra alternativa é usar uma tabela temporária:

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

A opção que você deve escolher depende da estrutura e do volume dos seus dados.

Nota: Se você estiver usando o SQL Server, você seria melhor servido usando:

WHILE EXISTS(SELECT * FROM #Temp)

Usando COUNT terá que tocar em todas as linhas da tabela, o EXISTS precisa apenas tocar na primeira (veja resposta de Josef abaixo).

331
Martynnw

Apenas uma nota rápida, se você estiver usando o SQL Server (2008 e acima), os exemplos que possuem:

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

Seria melhor servido com

While EXISTS(SELECT * From #Temp)

O Count terá que tocar em todas as linhas da tabela, o EXISTS só precisa tocar no primeiro.

124
Josef

É assim que eu faço:

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

Sem Cursores, sem tabelas temporárias, sem colunas extras. A coluna USERID deve ser um número inteiro exclusivo, como a maioria das chaves primárias.

36
Trevor

Defina sua tabela temporária como esta -

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

Então faça isso

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

Aqui está como eu faria:

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

[Edit] Porque eu provavelmente pulei a palavra "variável" quando li pela primeira vez a pergunta, aqui está uma resposta atualizada ...


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 você não tiver escolha, vá linha por linha criando um cursor FAST_FORWARD. Será tão rápido quanto construir um loop while e muito mais fácil de manter a longo prazo.

FAST_FORWARD Especifica um cursor FORWARD_ONLY, READ_ONLY com otimizações de desempenho ativadas. FAST_FORWARD não pode ser especificado se SCROLL ou FOR_UPDATE também forem especificados.

9
Wes Brown

Outra abordagem sem ter que alterar seu esquema ou usar tabelas temporárias:

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

Leve, sem ter que criar tabelas extras, se você tiver um inteiro ID na tabela

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

Você pode usar um loop 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

Eu realmente não vejo o ponto por que você precisaria recorrer ao uso de cursor temido. Mas aqui está outra opção se você estiver usando o SQL Server versão 2005/2008
Use Recursão

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

Isso funcionará na versão 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

Eu vou fornecer a solução baseada em conjunto.

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

Isso é muito mais rápido do que qualquer técnica de loop e é mais fácil de escrever e manter.

2
HLGEM

Essa abordagem requer apenas uma variável e não exclui nenhuma linha de @databases. Eu sei que há muitas respostas aqui, mas eu não vejo uma que use MIN para obter sua próxima ID como essa.

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

É possível usar um cursor para fazer isso:

create function [dbo] .f_teste_loop retorna a tabela @tabela (cod int, nome varchar (10)) as begin

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;

fim

criar procedimento [dbo]. [sp_teste_loop] como começar

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;

fim

1
Alexandre Pezzutto

Aqui está minha solução, que faz uso de um loop infinito, a instrução BREAK e a função @@ROWCOUNT. Nenhum cursor ou tabela temporária são necessários, e eu só preciso escrever uma consulta para obter a próxima linha na tabela @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

Concordo com a postagem anterior de que as operações baseadas em configurações normalmente terão um desempenho melhor, mas se você precisar fazer uma iteração nas linhas, aqui está a abordagem que eu tomaria:

  1. Adicione um novo campo à sua variável de tabela (Tipo de Dados Bit, padrão 0)
  2. Insira seus dados
  3. Selecione a linha superior 1 onde fUsed = 0 (Nota: fUsed é o nome do campo no passo 1)
  4. Execute o processamento que você precisa fazer
  5. Atualize o registro em sua variável de tabela, definindo fUsed = 1 para o registro
  6. Selecione o próximo registro não utilizado da tabela e repita o 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

Eu prefiro usar o Offset Fetch se você tiver um ID único, você pode classificar sua tabela por:

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

Dessa forma, não preciso adicionar campos à tabela ou usar uma função de janela.

1
Yves A Martin

Etapa 1: A instrução below select cria uma tabela temporária com um número de linha exclusivo para cada registro.

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

Etapa 2: declarar as variáveis ​​necessárias

DECLARE @ROWNUMBER INT
DECLARE @ename varchar(100)

Passo 3: Ter contagem total de linhas da tabela temporária

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

Etapa4: Tabela temporária de loop com base no número de linha exclusivo criado em 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

Este é o código que estou usando 2008 R2. Este código que estou usando é para construir índices em campos chave (SSNO & EMPR_NO) n todos os contos

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

Selecione @pk = @pk + 1 seria melhor: SET @pk + = @pk. Evite usar SELECT se você não estiver referenciando as tabelas que estão apenas atribuindo valores.

0
Bob Alley