¿Hay alguna manera de recorrer una variable de tabla en TSQL sin usar un cursor?

243

Digamos que tengo la siguiente variable de tabla simple:

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

¿Declarar y usar un cursor es mi única opción si quisiera recorrer las filas? ¿Hay otra manera?

Rayo
fuente
3
Aunque no estoy seguro del problema que ves con el enfoque anterior; Vea si esto ayuda ... databasejournal.com/features/mssql/article.php/3111031
Gishu
55
¿Podría proporcionarnos la razón por la que desea iterar sobre las filas? Es posible que exista otra solución que no requiera iteración (y que son más rápidas por un amplio margen en la mayoría de los casos)
Pop Catalin
de acuerdo con pop ... puede no necesitar un cursor dependiendo de la situación. pero no hay problema con el uso de cursores si es necesario
Shawn
3
No declaras por qué quieres evitar un cursor. Tenga en cuenta que un cursor podría ser la forma más sencilla de iterar. Es posible que haya escuchado que los cursores son 'malos', pero es realmente una iteración sobre las tablas lo que es malo en comparación con las operaciones basadas en conjuntos. Si no puede evitar la iteración, un cursor podría ser la mejor manera. El bloqueo es otro problema con los cursores, pero eso no es relevante cuando se usa una variable de tabla.
JacquesB
1
Usar un cursor no es su única opción, pero si no tiene forma de evitar un enfoque fila por fila, será su mejor opción. Los CURSORES son una construcción incorporada que son más eficientes y menos propensos a errores que hacer tu propio bucle WHILE tonto. La mayoría de las veces solo necesita usar la STATICopción para eliminar la revisión constante de las tablas base y el bloqueo que están allí por defecto y hace que la mayoría de las personas crean erróneamente que los CURSORES son malos. @JacquesB muy cerca: volver a verificar para ver si la fila de resultados todavía existe + bloqueo son los problemas. Y STATICgeneralmente arregla eso :-).
Solomon Rutzky

Respuestas:

376

En primer lugar, debe estar absolutamente seguro de que necesita recorrer cada fila: las operaciones basadas en conjuntos funcionarán más rápido en todos los casos que se me ocurran y normalmente utilizarán un código más simple.

Dependiendo de sus datos, puede ser posible realizar un bucle utilizando solo SELECTdeclaraciones como se muestra a continuación:

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

Otra alternativa es usar una tabla temporal:

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

La opción que debe elegir realmente depende de la estructura y el volumen de sus datos.

Nota: Si está utilizando SQL Server, será mejor que utilice:

WHILE EXISTS(SELECT * FROM #Temp)

El uso COUNTtendrá que tocar cada fila de la tabla, la EXISTSúnica necesita tocar la primera (ver la respuesta de Josef a continuación).

Martynnw
fuente
"Seleccione Top 1 @Id = Id de ATable" debería ser "Seleccione Top 1 @Id = Id de ATable donde se
procesó
10
Si usa SQL Server, vea la respuesta de Josef a continuación para un pequeño ajuste a lo anterior.
Polshgiant
3
¿Puedes explicar por qué esto es mejor que usar un cursor?
marco-fiset
55
Le di a éste un voto negativo. ¿Por qué debería evitar usar un cursor? Está hablando de iterar sobre una variable de tabla , no una tabla tradicional. No creo que las desventajas normales de los cursores se apliquen aquí. Si el procesamiento fila por fila es realmente necesario (y como usted señala, él debe estar seguro de eso primero), entonces usar un cursor es una solución mucho mejor que las que describe aquí.
Peter
@peterh Tienes razón. Y, de hecho, generalmente puede evitar esos "inconvenientes normales" mediante el uso de la STATICopción que copia el conjunto de resultados en una tabla temporal y, por lo tanto, ya no está bloqueando o volviendo a verificar las tablas base :-).
Solomon Rutzky
132

Solo una nota rápida, si está utilizando SQL Server (2008 y superior), los ejemplos que tienen:

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

Sería mejor servido con

While EXISTS(SELECT * From #Temp)

El Conde tendrá que tocar cada fila de la tabla, EXISTSsolo necesita tocar la primera.

Josef
fuente
99
Esta no es una respuesta, sino un comentario / mejora en la respuesta de Martynw.
Hammad Khan
77
El contenido de esta nota obliga a una mejor funcionalidad de formato que un comentario, sugeriría agregar en la respuesta.
Custodio
2
En versiones posteriores de SQL, el optimizador de consultas es lo suficientemente inteligente como para saber que cuando escribe lo primero, en realidad quiere decir lo segundo y lo optimiza como tal para evitar el escaneo de la tabla.
Dan Def
39

Así es como lo hago:

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

Sin cursores, sin tablas temporales, sin columnas adicionales. La columna USERID debe ser un número entero único, como lo son la mayoría de las claves principales.

Trevor
fuente
26

Defina su tabla temporal así:

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

Entonces haz esto -

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

Así es como lo haría:

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] Debido a que probablemente omití la palabra "variable" cuando leí la pregunta por primera vez, aquí hay una respuesta actualizada ...


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
fuente
44
así que básicamente estás haciendo un cursor, pero sin todos los beneficios de un cursor
Shawn
1
... sin bloquear las tablas que se utilizan durante el procesamiento ... ya que este es uno de los beneficios de un cursor :)
leoinfo
3
¿Mesas? Es una tabla VARIABLE: no hay acceso simultáneo posible.
DenNukem
DenNukem, tienes razón, creo que "salté" la palabra "variable" cuando leí la pregunta en ese momento ... Agregaré algunas notas a mi respuesta inicial
Leoinfo
Tengo que estar de acuerdo con DenNukem y Shawn. ¿Por qué, por qué, por qué vas a estas longitudes para evitar usar un cursor? De nuevo: ¡él quiere iterar sobre una variable de tabla, no una tabla tradicional!
Peter
10

Si no tiene más remedio que ir fila por fila creando un cursor FAST_FORWARD. Será tan rápido como construir un ciclo while y mucho más fácil de mantener a largo plazo.

FAST_FORWARD Especifica un cursor FORWARD_ONLY, READ_ONLY con optimizaciones de rendimiento habilitadas. FAST_FORWARD no se puede especificar si SCROLL o FOR_UPDATE también se especifica.


fuente
2
¡Si! Como comenté en otra parte, todavía no he visto ningún argumento sobre por qué NO usar un cursor cuando el caso es iterar sobre una variable de tabla . Un FAST_FORWARDcursor es una buena solución. (
voto a favor
5

Otro enfoque sin tener que cambiar su esquema o usar tablas temporales:

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

Puedes usar un ciclo while:

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

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

Esto funcionará en la versión 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
fuente
4

Ligero, sin tener que hacer tablas adicionales, si tiene un número entero IDen la mesa

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
Dominante
fuente
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
fuente
2

Realmente no veo el punto por el que tendrías que recurrir al uso temido cursor. Pero aquí hay otra opción si está utilizando SQL Server versión 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
fuente
2

Voy a proporcionar la solución basada en conjuntos.

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

Esto es mucho más rápido que cualquier técnica de bucle y es más fácil de escribir y mantener.

HLGEM
fuente
2

Prefiero usar Offset Fetch si tiene una identificación única, puede ordenar su tabla 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

De esta manera, no necesito agregar campos a la tabla o usar una función de ventana.

Yves A Martin
fuente
2

Es posible usar un cursor para hacer esto:

la función create [dbo] .f_teste_loop devuelve la tabla @tabela (cod int, nome varchar (10)) como comienzo

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;

final

crear procedimiento [dbo]. [sp_teste_loop] como comenzar

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;

final

Alexandre Pezzutto
fuente
1
¿No era la pregunta original "Sin usar un cursor"?
Fernando González Sánchez
1

Estoy de acuerdo con la publicación anterior en que las operaciones basadas en conjuntos generalmente funcionarán mejor, pero si necesita iterar sobre las filas, este es el enfoque que tomaría:

  1. Agregue un nuevo campo a su variable de tabla (Bit de tipo de datos, valor predeterminado 0)
  2. Inserta tus datos
  3. Seleccione la fila superior 1 donde fUsed = 0 (Nota: fUsed es el nombre del campo en el paso 1)
  4. Realice cualquier procesamiento que necesite hacer
  5. Actualice el registro en su variable de tabla configurando fUsed = 1 para el registro
  6. Seleccione el siguiente registro no utilizado de la tabla y repita el proceso.

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

Paso 1: A continuación, la instrucción select crea una tabla temporal con un número de fila único para cada registro.

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

Paso 2: declara las variables requeridas

DECLARE @ROWNUMBER INT
DECLARE @ename varchar(100)

Paso 3: tome el recuento total de filas de la tabla temporal

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

Paso 4: tabla temporal de bucle basada en un número de fila único creado en 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
fuente
1

Este enfoque solo requiere una variable y no elimina ninguna fila de @databases. Sé que hay muchas respuestas aquí, pero no veo una que use MIN para obtener su próxima identificación como esta.

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

Aquí está mi solución, que utiliza un bucle infinito, la BREAKdeclaración y la @@ROWCOUNTfunción. No se necesitan cursores ni tablas temporales, y solo necesito escribir una consulta para obtener la siguiente fila en la @databasestabla:

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
Mass Dot Net
fuente
Me acabo de dar cuenta de que @ControlFreak me recomendó este enfoque; Simplemente agregué comentarios y un ejemplo más detallado.
Mass Dot Net
0

Este es el código que estoy usando 2008 R2. Este código que estoy usando es para construir índices en campos clave (SSNO y EMPR_NO) en todos los cuentos

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
fuente
0
SELECT @pk = @pk + 1

seria mejor:

SET @pk += @pk

Evite usar SELECT si no hace referencia a tablas, solo asigna valores.

Callejón Bob
fuente