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

243

Digamos que tenho 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 percorrer as linhas? Existe outro caminho?

Raio
fonte
3
Embora eu não tenha certeza do problema que você vê com a abordagem acima; Ver se isso ajuda .. databasejournal.com/features/mssql/article.php/3111031
Gishu
5
Você poderia nos fornecer a razão pela qual você quer iterar sobre as linhas, outra solução que não necessitam de iteração poderia existe (e que são mais rápidos por uma larga margem na maioria dos casos)
Pop Catalin
concordo com pop ... pode não precisar de um cursor, dependendo da situação. mas há nenhum problema com o uso de cursores se você precisa
Shawn
3
Você não indica por que deseja evitar um cursor. Esteja ciente de que um cursor pode ser a maneira mais simples de iterar. Você pode ter ouvido falar que os cursores são "ruins", mas é realmente uma iteração sobre tabelas que é ruim em comparação com operações baseadas em conjuntos. Se você não pode evitar a iteração, um cursor pode ser o melhor caminho. O bloqueio é outro problema com os cursores, mas isso não é relevante ao usar uma variável de tabela.
precisa saber é o seguinte
1
Usar um cursor não é sua única opção, mas se você não tem como evitar uma abordagem linha por linha, será a melhor opção. CURSORs são uma construção interna que é mais eficiente e menos propensa a erros do que fazer seu próprio loop bobo de WHILE. Na maioria das vezes, você só precisa usar a STATICopção para remover a constante checagem das tabelas de base e os bloqueios existentes por padrão e fazer com que a maioria das pessoas acredite erroneamente que os CURSORES são maus. @ JacquesB muito perto: verificar novamente se a linha de resultados ainda existe + travar são os problemas. E STATICgeralmente corrige isso :-).
Solomon Rutzky 18/02

Respostas:

376

Antes de tudo, você deve ter certeza absoluta de que precisa iterar através de cada operação baseada em conjunto de linhas, que terá um desempenho mais rápido em todos os casos em que eu possa pensar e normalmente usará um código mais simples.

Dependendo dos seus dados, pode ser possível fazer um loop usando apenas as SELECTinstruções, como 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 realmente depende da estrutura e do volume dos seus dados.

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

WHILE EXISTS(SELECT * FROM #Temp)

O uso COUNTterá que tocar em todas as linhas da tabela, EXISTSbasta tocar na primeira (veja a resposta de Josef abaixo).

Martynnw
fonte
"Select Top 1 @ ID = Id De AQuadro" deve ser "Select Top 1 @ ID = Id De AQuadro Onde Processado = 0"
Amzath
10
Se estiver usando o SQL Server, consulte a resposta de Josef abaixo para um pequeno ajuste ao acima.
Polshgiant
3
Você pode explicar por que isso é melhor do que usar um cursor?
marco-Fiset
5
Deu a este um voto negativo. Por que ele deveria evitar usar um cursor? Ele está falando sobre iterar sobre a variável da tabela , não uma tabela tradicional. Não acredito que as desvantagens normais dos cursores se apliquem aqui. Se o processamento linha a linha for realmente necessário (e, como você aponta, ele deve ter certeza disso primeiro), usar um cursor é uma solução muito melhor do que as descritas aqui.
Peterh
@ Peter Você está correto. E, de fato, você geralmente pode evitar essas "desvantagens normais" usando a STATICopção que copia o conjunto de resultados em uma tabela temporária e, portanto, não está mais bloqueando ou verificando novamente as tabelas base :-).
Solomon Rutzky,
132

Apenas uma observação rápida, se você estiver usando o SQL Server (2008 e superior), os exemplos que possuem:

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

Seria melhor servido com

While EXISTS(SELECT * From #Temp)

O Conde terá que tocar em todas as linhas da tabela, EXISTSbasta tocar na primeira.

Josef
fonte
9
Esta não é uma resposta, mas um comentário / aprimoramento na resposta de Martynw.
Hammad Khan
7
O conteúdo desta nota força uma funcionalidade de formatação melhor do que um comentário, sugiro acrescentar na resposta.
Custodio 26/09
2
Nas versões posteriores do SQL, o otimizador de consulta é inteligente o suficiente para saber que, quando você escreve a primeira coisa, na verdade você quer dizer a segunda e a otimiza como tal para evitar a varredura da tabela.
Dan Def
39

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

Trevor
fonte
26

Defina sua tabela temporária assim -

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
Seibar
fonte
16

Aqui está como eu faria isso:

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

[Editar] Como provavelmente pulei a palavra "variável" quando li a pergunta pela primeira vez, 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
leoinfo
fonte
4
Então, basicamente, você está fazendo um cursor, mas sem todos os benefícios de um cursor
Shawn
1
... sem bloquear as tabelas que são usadas durante o processamento ... como este é um dos benefícios de um cursor :)
leoinfo
3
Mesas? É uma tabela VARIABLE - não há acesso simultâneo possível.
perfil completo de DenNukem
DenNukem, você está certo, eu acho que "ignorado" a palavra "variável" quando li a questão naquele tempo ... vou acrescentar algumas notas para a minha resposta inicial
leoinfo
Eu tenho que concordar com DenNukem e Shawn. Por que, por que, por que você se esforça para evitar o uso de um cursor? Novamente: ele deseja iterar sobre uma variável da tabela, não sobre uma tabela tradicional !!!
Peterh
10

Se você não tem escolha a não ser ir linha por linha, criando um cursor FAST_FORWARD. Será tão rápido quanto criar um loop while e muito mais fácil de manter a longo prazo.

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


fonte
2
sim! Como comentei em outro lugar, ainda não vi argumentos sobre o porquê de NÃO usar um cursor quando o caso é iterar sobre uma variável de tabela . Um FAST_FORWARDcursor é uma boa solução. (upvote)
peterh
5

Outra abordagem sem precisar 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
SReiderB
fonte
4

Você pode usar um loop while:

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

    Delete From #TempTable Where x = x
End
GateKiller
fonte
4

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

Leve, sem precisar criar tabelas extras, se você tiver um número inteiro IDna tabela

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
Control Freak
fonte
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
Syed Umar Ahmed
fonte
2

Realmente não vejo o motivo pelo qual você precisaria recorrer ao temido cursor. Mas aqui está outra opção se você estiver usando o SQL Server versão 2005/2008
Use Recursion

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
dance2die
fonte
2

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 em loop e é mais fácil de escrever e manter.

HLGEM
fonte
2

Prefiro usar a Busca por deslocamento, se você tiver um ID exclusivo, poderá ordenar 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 nem usar uma função de janela.

Yves A Martin
fonte
2

É possível usar um cursor para fazer isso:

função create [dbo] .f_teste_loop retorna a tabela @tabela (cod int, nome varchar (10)) como início

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

crie o procedimento [dbo]. [sp_teste_loop] como iniciar

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

Alexandre Pezzutto
fonte
1
A pergunta original não era "Sem usar um cursor"?
Fernando Gonzalez Sanchez
1

Concordo com a postagem anterior de que as operações baseadas em conjunto normalmente terão um desempenho melhor, mas se você precisar percorrer as linhas, aqui está a abordagem que eu adotaria:

  1. Adicione um novo campo à sua variável de tabela (Data Type Bit, padrão 0)
  2. Insira seus dados
  3. Selecione a primeira linha 1 em que fUsed = 0 (Nota: fUsed é o nome do campo na etapa 1)
  4. Execute qualquer processamento que você precise fazer
  5. Atualize o registro na 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
Tim Lentine
fonte
1

Etapa 1: A instrução select abaixo cria uma tabela temporária com 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 variáveis ​​necessárias

DECLARE @ROWNUMBER INT
DECLARE @ename varchar(100)

Etapa 3: obter a contagem total de linhas da tabela temporária

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

Etapa 4: criar uma tabela temporária de loop com base no número de linha exclusivo criado em 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
Srinivas Maale
fonte
1

Essa abordagem requer apenas uma variável e não exclui nenhuma linha dos @databases. Sei que há muitas respostas aqui, mas não vejo uma que use MIN para obter seu próximo ID como este.

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
Sean
fonte
1

Aqui está minha solução, que utiliza um loop infinito, a BREAKinstrução e a @@ROWCOUNTfunção. Não são necessários cursores ou tabelas temporárias, e eu só preciso escrever uma consulta para obter a próxima linha da @databasestabela:

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
Net de ponto em massa
fonte
Acabei de perceber que o @ControlFreak recomendou essa abordagem antes de mim; Eu simplesmente adicionei comentários e um exemplo mais detalhado.
Mass Dot Net
0

Este é o código que estou usando 2008 R2. Este código que estou usando é para criar índices nos campos-chave (SSNO e EMPR_NO) em 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 
howmnsk
fonte
0
SELECT @pk = @pk + 1

seria melhor:

SET @pk += @pk

Evite usar SELECT se você não estiver fazendo referência a tabelas, apenas atribuindo valores.

Bob Alley
fonte