Bucle de SQL Server: ¿cómo recorro un conjunto de registros?

151

¿Cómo recorro un conjunto de registros de una selección?

Digamos, por ejemplo, que tengo algunos registros que deseo recorrer y hacer algo con cada registro. Aquí hay una versión primitiva de mi selección:

select top 1000 * from dbo.table
where StatusID = 7 

Gracias

Miedoso
fuente
55
¿Qué quieres hacer con cada disco? La preferencia sería hacer el trabajo en una consulta SQL. A menos que necesite usar T-SQL, tal vez con cursores.
Gordon Linoff
2
Yo usaría un cursor.
FloChanz
55
Eso será bastante lento: ¿no es posible volver a escribir el proceso almacenado o mover parte de la lógica para que funcione de una manera basada en conjuntos?
Puente
2
@Funky, ¿qué hace el sproc? A menudo, el código se puede volver a escribir de una manera basada en un conjunto (es decir, evitar bucles). Si eres inflexible y quieres realizar una operación RBAR ( simple-talk.com/sql/t-sql-programming/… ), entonces un cursor es lo que quieres investigar.
gvee
1
Quizás pueda explicar lo que hará con estos datos con más detalle. En la mayoría de los casos, puede escribir fácilmente una sola consulta SQL que hará lo que necesita hacer en una sola acción en lugar de recorrer los registros individuales.
Alan Barber

Respuestas:

212

Al usar T-SQL y cursores como este:

DECLARE @MyCursor CURSOR;
DECLARE @MyField YourFieldDataType;
BEGIN
    SET @MyCursor = CURSOR FOR
    select top 1000 YourField from dbo.table
        where StatusID = 7      

    OPEN @MyCursor 
    FETCH NEXT FROM @MyCursor 
    INTO @MyField

    WHILE @@FETCH_STATUS = 0
    BEGIN
      /*
         YOUR ALGORITHM GOES HERE   
      */
      FETCH NEXT FROM @MyCursor 
      INTO @MyField 
    END; 

    CLOSE @MyCursor ;
    DEALLOCATE @MyCursor;
END;
FloChanz
fuente
55
Lo correcto es reescribir el proceso para que no sea necesario hacer un bucle. El bucle es una elección extremadamente mala en una base de datos.
HLGEM
23
Quizás tenga razón, pero con la información dada en la pregunta en el momento en que escribí la respuesta, el usuario solo desea recorrer un conjunto de datos ... y un Cursor es una forma de hacerlo.
FloChanz
16
Los cursores son solo una herramienta, generalmente no tienen nada de correcto o incorrecto. Observa el desempeño y decide. Esta respuesta (cursores) es una opción posible. También puede utilizar un bucle while, CTE, etc.
Cadenas
2
@FrenkyB Sí, puedes. Mire de esta manera ... stackoverflow.com/questions/11035187/…
sam yi
2
Felicitaciones, su solución está incluso en msdn: msdn.microsoft.com/en-us/library/… y realmente me gusta cómo usa el Tipo de datos de campo.
Pete
111

Esto es lo que he estado haciendo si necesita hacer algo iterativo ... pero sería prudente buscar primero las operaciones establecidas.

select top 1000 TableID
into #ControlTable 
from dbo.table
where StatusID = 7

declare @TableID int

while exists (select * from #ControlTable)
begin

    select top 1 @TableID = TableID
    from #ControlTable
    order by TableID asc

    -- Do something with your TableID

    delete #ControlTable
    where TableID = @TableID

end

drop table #ControlTable
sam yi
fuente
44
Usar un CURSOR (ver la respuesta a continuación) parece ser una solución mucho más elegante.
Mikhail Glukhov 01 de
¿Por qué esta respuesta tiene más votos positivos que la solución del cursor?
ataravati
29
@ataravati Porque esta solución se lee más limpiamente para muchos programadores que los cursores. La sintaxis de los cursores es bastante incómoda para algunos.
Brian Webster
¡Gracias! Mi ejemplo con actualización y grupo por lógica usando el código anterior: pastebin.com/GAjUNNi9 . Quizás sea útil para cualquiera.
Nigrimmist
¿Se puede usar la variable como nombre de columna en la instrucción de actualización dentro del bucle? Algo así como "Actualizar TableName SET @ ColumnName = 2"
MH
28

Pequeño cambio en la respuesta de sam yi (para una mejor legibilidad):

select top 1000 TableID
into #ControlTable 
from dbo.table
where StatusID = 7

declare @TableID int

while exists (select * from #ControlTable)
begin

    select @TableID = (select top 1 TableID
                       from #ControlTable
                       order by TableID asc)

    -- Do something with your TableID

    delete #ControlTable
    where TableID = @TableID

end

drop table #ControlTable
Precepto
fuente
1
@ azulado, esta respuesta está corrigiendo la respuesta de sam yi. Esta corrección está principalmente dentro de la select @TableID = (...)declaración.
Sandman simple
Creo que esta respuesta debe seleccionarse para esta pregunta
sajadre
14

Al usar el cursor, puede iterar fácilmente a través de registros individualmente e imprimir registros por separado o como un solo mensaje que incluye todos los registros.

DECLARE @CustomerID as INT;
declare @msg varchar(max)
DECLARE @BusinessCursor as CURSOR;

SET @BusinessCursor = CURSOR FOR
SELECT CustomerID FROM Customer WHERE CustomerID IN ('3908745','3911122','3911128','3911421')

OPEN @BusinessCursor;
    FETCH NEXT FROM @BusinessCursor INTO @CustomerID;
    WHILE @@FETCH_STATUS = 0
        BEGIN
            SET @msg = '{
              "CustomerID": "'+CONVERT(varchar(10), @CustomerID)+'",
              "Customer": {
                "LastName": "LastName-'+CONVERT(varchar(10), @CustomerID) +'",
                "FirstName": "FirstName-'+CONVERT(varchar(10), @CustomerID)+'",    
              }
            }|'
        print @msg
    FETCH NEXT FROM @BusinessCursor INTO @CustomerID;
END
Agnel Amodia
fuente
1
Esto se ve interesante. Me pregunto qué significa el identificador @.
netskink
@ es solo para diferenciar como variables.
Agnel Amodia
9

Otro enfoque si está bien usando tablas temporales. Lo he probado personalmente y no causará ninguna excepción (incluso si la tabla temporal no tiene ningún dato).

CREATE TABLE #TempTable
(
    ROWID int identity(1,1) primary key,
    HIERARCHY_ID_TO_UPDATE int,
)

--create some testing data
--INSERT INTO #TempTable VALUES(1)
--INSERT INTO #TempTable VALUES(2)
--INSERT INTO #TempTable VALUES(4)
--INSERT INTO #TempTable VALUES(6)
--INSERT INTO #TempTable VALUES(8)

DECLARE @MAXID INT, @Counter INT

SET @COUNTER = 1
SELECT @MAXID = COUNT(*) FROM #TempTable

WHILE (@COUNTER <= @MAXID)
BEGIN
    --DO THE PROCESSING HERE 
    SELECT @HIERARCHY_ID_TO_UPDATE = PT.HIERARCHY_ID_TO_UPDATE
    FROM #TempTable AS PT
    WHERE ROWID = @COUNTER

    SET @COUNTER = @COUNTER + 1
END


IF (OBJECT_ID('tempdb..#TempTable') IS NOT NULL)
BEGIN
    DROP TABLE #TempTable
END
Sandeep
fuente
Esto es muy extraño. Contiene muchos errores, también el uso de dos variables donde una va de 1 a 1 COUNT(*)y la segunda va de COUNT(*)1 a extraño.
David Ferenczy Rogožan
La variable MAXID se usa para recorrer. La variable CONTADOR se usa para realizar una operación en un registro particular de la tabla. Si leo la pregunta, se refiere a "tener algunos registros que deseo recorrer y hacer algo con cada registro". Puedo estar equivocado, pero por favor señale qué está mal arriba de @DAWID
Sandeep
2
Creo que es obvio cómo usas esas variables en tu código. Simplemente puede tener WHILE (@COUTNER <= @ROWID)y no necesita disminuir @ROWIDen cada iteración. Por cierto, qué sucede si los mensajes de correo electrónico ROWIDen su tabla no son continuos (algunas filas se eliminaron previamente).
David Ferenczy Rogožan
1
¿Cuándo sugeriría usar una tabla temporal en lugar de usar un cursor? ¿Es esto simplemente una elección de diseño, o tiene uno un mejor rendimiento?
h0r53
4

Puede elegir clasificar sus datos y agregar un ROW_NUMBER y hacer una cuenta regresiva hasta cero mientras itera su conjunto de datos.

-- Get your dataset and rank your dataset by adding a new row_number
SELECT  TOP 1000 A.*, ROW_NUMBER() OVER(ORDER BY A.ID DESC) AS ROW
INTO #TEMPTABLE 
FROM DBO.TABLE AS A
WHERE STATUSID = 7;

--Find the highest number to start with
DECLARE @COUNTER INT = (SELECT MAX(ROW) FROM #TEMPTABLE);
DECLARE @ROW INT;

-- Loop true your data until you hit 0
WHILE (@COUNTER != 0)
BEGIN

    SELECT @ROW = ROW
    FROM #TEMPTABLE
    WHERE ROW = @COUNTER
    ORDER BY ROW DESC

    --DO SOMTHING COOL  

    -- SET your counter to -1
    SET @COUNTER = @ROW -1
END

DROP TABLE #TEMPTABLE
Bunkerbuster
fuente
2

De esta manera podemos iterar en los datos de la tabla.

DECLARE @_MinJobID INT
DECLARE @_MaxJobID INT
CREATE  TABLE #Temp (JobID INT)

INSERT INTO #Temp SELECT * FROM DBO.STRINGTOTABLE(@JobID,',')
SELECT @_MinJID = MIN(JobID),@_MaxJID = MAX(JobID)  FROM #Temp

    WHILE @_MinJID <= @_MaxJID
    BEGIN

        INSERT INTO Mytable        
        (        
            JobID,        
        )        

        VALUES        
        (        
            @_MinJobID,        
        ) 

        SET @_MinJID = @_MinJID + 1;
    END

DROP TABLE #Temp

STRINGTOTABLE es una función definida por el usuario que analizará los datos separados por comas y devolverá la tabla. Gracias

Monojit Sarkar
fuente
1

Creo que este es el ejemplo de manera fácil de iterar un elemento.

declare @cateid int
select CateID into [#TempTable] from Category where GroupID = 'STOCKLIST'

while (select count(*) from #TempTable) > 0
begin
    select top 1 @cateid = CateID from #TempTable
    print(@cateid)

    --DO SOMETHING HERE

    delete #TempTable where CateID = @cateid
end

drop table #TempTable
江明哲
fuente