¿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?


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


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


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.


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

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

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.


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

4

Puedes usar un ciclo while:

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

    Delete From #TempTable Where x = x
End

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 

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

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

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

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.


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.


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


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

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

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

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

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 

0
SELECT @pk = @pk + 1

seria mejor:

SET @pk += @pk

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

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.