2008-09-15 22 views
186

Digamos que tengo el siguiente variable de tabla simple:¿Hay alguna forma de recorrer una variable de tabla en TSQL sin usar un cursor?

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

está declarando y utilizando un cursor mi única opción si quería iterar por las filas? ¿Hay otra manera?

+3

¿Nos puede proporcionar la razón por la que desea iterar sobre las filas, otra solución que no es necesario que exista iteración (y que son más rápidos por un margen grande en la mayoría de los casos) –

+0

de acuerdo con el pop ... puede no necesitar un cursor según la situación. pero no hay problema con el uso de cursores si necesita – Shawn

+0

http://wiki.lessthandot.com/index.php/Cursors_and_How_to_Avoid_Them – HLGEM

Respuesta

267

En primer lugar usted debe estar absolutamente seguro de que necesita para recorrer cada fila - operaciones basadas en conjunto llevarán a cabo más rápido en todos los casos que se me ocurre, y normalmente usa un código más simple.

En función de los datos que puede ser posible bucle simplemente usando select 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 utilizar 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 que sería mejor servido usando:

WHILE EXISTS(SELECT * FROM #Temp) 

Usando COUNT tendrá que tocar cada hilera en la tabla, el EXISTS sólo tiene que tocar el primero (ver Josef's answer a continuación).

+0

"Seleccionar Top 1 @Id = Id de ATable" debería ser "Seleccionar Top 1 @Id = Id de ATable Donde procesado = 0" – Amzath

+9

Si usa SQL Server, consulte la respuesta de Josef a continuación para una pequeña modificación de la anterior. – Polshgiant

+2

¿Puedes explicar por qué esto es mejor que usar un cursor? –

2

Puede utilizar un bucle while:

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

    Delete From #TempTable Where x = x 
End 
14

Aquí es cómo 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 salté la palabra "variable" cuando leí por primera vez la cuestión, aquí es 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 
+4

así que básicamente estás haciendo un cursor, pero sin todas las ventajas de un cursor – Shawn

+1

... sin bloquear las tablas que se usan durante el procesamiento ... ya que este es uno de los * beneficios * de un cursor :) – leoinfo

+3

¿Tablas? Es una tabla VARIABLE - no hay acceso concurrente posible. – DenNukem

0

Estoy de acuerdo con el comentario anterior que estableció operaciones basadas normalmente se obtienen mejores resultados, pero si usted necesita para repetir las filas aquí está el enfoque me gustaría tener:

  1. Agregar un nuevo campo a la variable de tabla (tipo de datos bit, por defecto 0)
  2. introducir sus datos de
  3. Seleccione el Top 1 Fila donde fusionados = 0 (Nota: fusionado es el nombre del campo en el paso 1)
  4. realizar cualquier procesamiento que tiene que hacer
  5. actualizar el registro en la variable de tabla asignando fusionados = 1 para el registro
  6. Seleccione el siguiente registro no utilizado de la mesa y de repetición 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 
    
108

Sólo una nota rápida, si está utilizando SQL Server, 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 hilera en la tabla, el EXISTS sólo tiene que tocar la primera.

+6

Esto no es una respuesta sino un comentario/mejora en la respuesta de Martynw. –

+6

El contenido de esta nota obliga a una mejor funcionalidad de formateo que un comentario, sugiero que se agregue en la respuesta. – Custodio

+1

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 el segundo y lo optimiza para evitar el escaneo de tabla. –

7

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 también se especifica SCROLL o FOR_UPDATE.

+1

¡Sí! Como comenté en otro lugar, aún no he visto ningún argumento sobre por qué ** NO ** usar un cursor cuando el caso es para iterar sobre una ** variable de tabla **. Un cursor 'FAST_FORWARD' es una buena solución. (upvote) – peterh

15

definir su tabla temporal como este -

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 hacer 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 
2

Realmente no veo el punto por qué tendría que recurrir al uso de cursor temido. Pero aquí es otra opción si está utilizando la versión de SQL Server 2005/2008
Uso recursividad

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 
31

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 

No hay cursores, no hay tablas temporales , sin columnas adicionales La columna USERID debe ser un entero único, como la mayoría de las claves primarias.

1

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
-- [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

Otro enfoque sin tener que cambiar el esquema o el uso de 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 
1

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

Este es el código que estoy usando 2008 R2. Este código que estoy usando es la construcción de índices en campos clave (SSNO & EMPR_NO) n todas 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

Seleccionar @pk = @pk + 1 sería mejor: SET @pk + = @pk. Evite usar SELECT si no está haciendo referencia a las tablas que están asignando valores.

0

Paso 1: A continuación, la instrucción select crea una tabla temporal con un número de fila exclusivo 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: Las variables Declarar requeridos

DECLARE @ROWNUMBER INT 
DECLARE @ename varchar(100) 

Paso 3: Tome filas recuento total de la tabla temporal

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

Paso 4: Bucle tabla temporal basado en el número de fila única a crear en 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 
2

Ligera, sin tener que hacer mesas adicionales, si usted tiene un entero ID sobre la mesa

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 
0

Este método sólo requiere una variable y no elimina las filas de @databases. Sé que hay muchas respuestas aquí, pero no veo una que use MIN para obtener su próximo ID así.

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

Yo prefiero usar el Offset Fetch si tiene un identificador único que puede ordenar la 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 añadir campos a la tabla o utilizar una ventana función.

+0

me muestra un error de sintaxis en "FETCH NEXT 1 ROW"; –

1

Es posible utilizar un cursor para hacer esto:

crear la función [dbo] .f_teste_loop vuelve @tabela mesa ( bacalao int, varchar Nome (10) ) como comienzan

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; 

extremo

crear procedimiento [dbo].[Sp_teste_loop] como comienzan

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

+0

¿No fue la pregunta original "Sin usar un cursor"? –

1

aquí está mi solución, que hace uso de un bucle infinito, la declaración BREAK, y la función @@ROWCOUNT. No hay cursores o tabla temporal son necesarios, y sólo tienen que escribir una consulta para obtener el siguiente registro en la tabla @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 
+0

Me acabo de dar cuenta de que ** @ ControlFreak ** me recomendó este enfoque; Simplemente agregué comentarios y un ejemplo más detallado. –

Cuestiones relacionadas