¿Cómo escribir un foreach en SQL Server?


192

Estoy tratando de lograr algo similar a un para cada uno, donde me gustaría tomar los ID de una declaración de selección devuelta y usar cada uno de ellos.

DECLARE @i int
DECLARE @PractitionerId int
DECLARE @numrows int
DECLARE @Practitioner TABLE (
    idx smallint Primary Key IDENTITY(1,1)
    , PractitionerId int
)

INSERT @Practitioner
SELECT distinct PractitionerId FROM Practitioner

SET @i = 1
SET @numrows = (SELECT COUNT(*) FROM Practitioner)
IF @numrows > 0
    WHILE (@i <= (SELECT MAX(idx) FROM Practitioner))
    BEGIN

        SET @PractitionerId = (SELECT PractitionerId FROM @Practitioner WHERE idx = @i)

        --Do something with Id here
        PRINT @PractitionerId

        SET @i = @i + 1
    END

Por el momento tengo algo parecido a lo anterior, pero recibo el error:

Nombre de columna no válido 'idx'.

Alguien podria


2
Cómo iterar a través de un conjunto de resultados utilizando Transact-SQL en SQL Server: support.microsoft.com/kb/111401/nl
Anonymoose

idxestá en @Practitionerno Practitioner. La mayoría de las veces, existen alternativas superiores basadas en conjuntos para un enfoque para cada uno; si muestra lo que hace con el valor de la fila, tal vez se le pueda sugerir una alternativa.
Alex K.

1
Por favor, publique más sobre lo que está tratando de lograr. Evite RBAR como la peste (99% del tiempo). simple-talk.com/sql/t-sql-programming/…
granadaCoder

1
RBAR Malo, bien basado en conjuntos.
granadaCoder

Si nos dice qué --Do something with Id herees, es probable que podamos mostrarle cómo resolver este problema sin ningún bucle o cursores. En la mayoría de los casos, desea utilizar una solución basada en conjuntos, ya que así es como SQL Server está optimizado para funcionar. Recorrer y tratar una fila a la vez ciertamente tiene su lugar, pero sospecho que no es así.
Aaron Bertrand

Respuestas:


342

Parece que quieres usar a CURSOR. Aunque la mayoría de las veces es mejor usar una solución basada en un conjunto, hay algunas veces donde a CURSORes la mejor solución. Sin saber más sobre su problema real, no podemos ayudarlo más que eso:

DECLARE @PractitionerId int

DECLARE MY_CURSOR CURSOR 
  LOCAL STATIC READ_ONLY FORWARD_ONLY
FOR 
SELECT DISTINCT PractitionerId 
FROM Practitioner

OPEN MY_CURSOR
FETCH NEXT FROM MY_CURSOR INTO @PractitionerId
WHILE @@FETCH_STATUS = 0
BEGIN 
    --Do something with Id here
    PRINT @PractitionerId
    FETCH NEXT FROM MY_CURSOR INTO @PractitionerId
END
CLOSE MY_CURSOR
DEALLOCATE MY_CURSOR

41
POR FAVOR, no comience a usar cursores a izquierda y derecha. Se necesitan <1% del tiempo. Las soluciones RBAR (fila por fila agonizante) suelen tener un mal desempeño y causar dolores de cabeza. Si eres nuevo, POR FAVOR intenta aprender esta lección temprano.
granadaCoder

135

Supongamos que la columna PractitionerId es única, entonces puede usar el siguiente ciclo

DECLARE @PractitionerId int = 0
WHILE(1 = 1)
BEGIN
  SELECT @PractitionerId = MIN(PractitionerId)
  FROM dbo.Practitioner WHERE PractitionerId > @PractitionerId
  IF @PractitionerId IS NULL BREAK
  SELECT @PractitionerId
END

1
Demasiado simple para ser verdad. Está seleccionando MIN (PractitionerId) siempre dentro del bucle. ¿Cuál es la condición para salir del bucle? me parece un bucle infinito.
bluelabel

77
@bluelabel para salir del script de bucle tiene la siguiente condición SI PractitionerId ES NULL BREAK
Aleksandr Fedorenko

16

Su selección de conteo y selección máxima deben ser de su variable de tabla en lugar de la tabla real

DECLARE @i int
DECLARE @PractitionerId int
DECLARE @numrows int
DECLARE @Practitioner TABLE (
    idx smallint Primary Key IDENTITY(1,1)
    , PractitionerId int
)

INSERT @Practitioner
SELECT distinct PractitionerId FROM Practitioner

SET @i = 1
SET @numrows = (SELECT COUNT(*) FROM @Practitioner)
IF @numrows > 0
    WHILE (@i <= (SELECT MAX(idx) FROM @Practitioner))
    BEGIN

        SET @PractitionerId = (SELECT PractitionerId FROM @Practitioner WHERE idx = @i)

        --Do something with Id here
        PRINT @PractitionerId

        SET @i = @i + 1
    END

15

Esto generalmente (casi siempre) funciona mejor que un cursor y es más simple:

    DECLARE @PractitionerList TABLE(PracticionerID INT)
    DECLARE @PractitionerID INT

    INSERT @PractitionerList(PracticionerID)
    SELECT PracticionerID
    FROM Practitioner

    WHILE(1 = 1)
    BEGIN

        SET @PracticionerID = NULL
        SELECT TOP(1) @PracticionerID = PracticionerID
        FROM @PractitionerList

        IF @PracticionerID IS NULL
            BREAK

        PRINT 'DO STUFF'

        DELETE TOP(1) FROM @PractitionerList

    END

5

Diría que todo probablemente funciona, excepto que la columna idxno existe realmente en la tabla de la que está seleccionando. Tal vez quisiste seleccionar entre @Practitioner:

WHILE (@i <= (SELECT MAX(idx) FROM @Practitioner))

porque eso está definido en el código anterior así:

DECLARE @Practitioner TABLE (
    idx smallint Primary Key IDENTITY(1,1)
    , PractitionerId int
)

2

La siguiente línea es incorrecta en su versión:

WHILE (@i <= (SELECT MAX(idx) FROM @Practitioner))

(Falta la @)

Puede ser una idea cambiar su convención de nomenclatura para que las tablas sean más diferentes.


1

Aunque los cursores generalmente se consideran un mal horrible, creo que este es un caso para el cursor FAST_FORWARD: lo más cercano que puede llegar a FOREACH en TSQL.


1

Se me ocurrió una forma muy efectiva, (creo) legible de hacer esto.

    1. create a temp table and put the records you want to iterate in there
    2. use WHILE @@ROWCOUNT <> 0 to do the iterating
    3. to get one row at a time do, SELECT TOP 1 <fieldnames>
        b. save the unique ID for that row in a variable
    4. Do Stuff, then delete the row from the temp table based on the ID saved at step 3b.

Aquí está el código. Lo siento, está usando mis nombres de variables en lugar de los de la pregunta.

            declare @tempPFRunStops TABLE (ProformaRunStopsID int,ProformaRunMasterID int, CompanyLocationID int, StopSequence int );    

        INSERT @tempPFRunStops (ProformaRunStopsID,ProformaRunMasterID, CompanyLocationID, StopSequence) 
        SELECT ProformaRunStopsID, ProformaRunMasterID, CompanyLocationID, StopSequence from ProformaRunStops 
        WHERE ProformaRunMasterID IN ( SELECT ProformaRunMasterID FROM ProformaRunMaster WHERE ProformaId = 15 )

    -- SELECT * FROM @tempPFRunStops

    WHILE @@ROWCOUNT <> 0  -- << I dont know how this works
        BEGIN
            SELECT TOP 1 * FROM @tempPFRunStops
            -- I could have put the unique ID into a variable here
            SELECT 'Ha'  -- Do Stuff
            DELETE @tempPFRunStops WHERE ProformaRunStopsID = (SELECT TOP 1 ProformaRunStopsID FROM @tempPFRunStops)
        END

1

Aquí está una de las mejores soluciones.

DECLARE @i int
            DECLARE @curren_val int
            DECLARE @numrows int
            create table #Practitioner (idx int IDENTITY(1,1), PractitionerId int)
            INSERT INTO #Practitioner (PractitionerId) values (10),(20),(30)
            SET @i = 1
            SET @numrows = (SELECT COUNT(*) FROM #Practitioner)
            IF @numrows > 0
            WHILE (@i <= (SELECT MAX(idx) FROM #Practitioner))
            BEGIN

                SET @curren_val = (SELECT PractitionerId FROM #Practitioner WHERE idx = @i)

                --Do something with Id here
                PRINT @curren_val
                SET @i = @i + 1
            END

Aquí agregué algunos valores en la tabla porque inicialmente está vacía.

Podemos acceder o podemos hacer cualquier cosa en el cuerpo del bucle y podemos acceder al idx definiéndolo dentro de la definición de la tabla.

              BEGIN
                SET @curren_val = (SELECT PractitionerId FROM #Practitioner WHERE idx = @i)

                --Do something with Id here

                PRINT @curren_val
                SET @i = @i + 1
            END

1

Hice un procedimiento que ejecuta un FOREACHcon CURSORpara cualquier tabla.

Ejemplo de uso:

CREATE TABLE #A (I INT, J INT)
INSERT INTO #A VALUES (1, 2), (2, 3)
EXEC PRC_FOREACH
    #A --Table we want to do the FOREACH
    , 'SELECT @I, @J' --The execute command, each column becomes a variable in the same type, so DON'T USE SPACES IN NAMES
   --The third variable is the database, it's optional because a table in TEMPB or the DB of the proc will be discovered in code

El resultado es 2 selecciones para cada fila. La sintaxis UPDATEy la ruptura FOREACHse escriben en las sugerencias.

Este es el código de proceso:

CREATE PROC [dbo].[PRC_FOREACH] (@TBL VARCHAR(100) = NULL, @EXECUTE NVARCHAR(MAX)=NULL, @DB VARCHAR(100) = NULL) AS BEGIN

    --LOOP BETWEEN EACH TABLE LINE            

IF @TBL + @EXECUTE IS NULL BEGIN
    PRINT '@TBL: A TABLE TO MAKE OUT EACH LINE'
    PRINT '@EXECUTE: COMMAND TO BE PERFORMED ON EACH FOREACH TRANSACTION'
    PRINT '@DB: BANK WHERE THIS TABLE IS (IF NOT INFORMED IT WILL BE DB_NAME () OR TEMPDB)' + CHAR(13)
    PRINT 'ROW COLUMNS WILL VARIABLE WITH THE SAME NAME (COL_A = @COL_A)'
    PRINT 'THEREFORE THE COLUMNS CANT CONTAIN SPACES!' + CHAR(13)
    PRINT 'SYNTAX UPDATE:

UPDATE TABLE
SET COL = NEW_VALUE
WHERE CURRENT OF MY_CURSOR

CLOSE CURSOR (BEFORE ALL LINES):

IF 1 = 1 GOTO FIM_CURSOR'
    RETURN
END
SET @DB = ISNULL(@DB, CASE WHEN LEFT(@TBL, 1) = '#' THEN 'TEMPDB' ELSE DB_NAME() END)

    --Identifies the columns for the variables (DECLARE and INTO (Next cursor line))

DECLARE @Q NVARCHAR(MAX)
SET @Q = '
WITH X AS (
    SELECT
        A = '', @'' + NAME
        , B = '' '' + type_name(system_type_id)
        , C = CASE
            WHEN type_name(system_type_id) IN (''VARCHAR'', ''CHAR'', ''NCHAR'', ''NVARCHAR'') THEN ''('' + REPLACE(CONVERT(VARCHAR(10), max_length), ''-1'', ''MAX'') + '')''
            WHEN type_name(system_type_id) IN (''DECIMAL'', ''NUMERIC'') THEN ''('' + CONVERT(VARCHAR(10), precision) + '', '' + CONVERT(VARCHAR(10), scale) + '')''
            ELSE ''''
        END
    FROM [' + @DB + '].SYS.COLUMNS C WITH(NOLOCK)
    WHERE OBJECT_ID = OBJECT_ID(''[' + @DB + '].DBO.[' + @TBL + ']'')
    )
SELECT
    @DECLARE = STUFF((SELECT A + B + C FROM X FOR XML PATH('''')), 1, 1, '''')
    , @INTO = ''--Read the next line
FETCH NEXT FROM MY_CURSOR INTO '' + STUFF((SELECT A + '''' FROM X FOR XML PATH('''')), 1, 1, '''')'

DECLARE @DECLARE NVARCHAR(MAX), @INTO NVARCHAR(MAX)
EXEC SP_EXECUTESQL @Q, N'@DECLARE NVARCHAR(MAX) OUTPUT, @INTO NVARCHAR(MAX) OUTPUT', @DECLARE OUTPUT, @INTO OUTPUT

    --PREPARE TO QUERY

SELECT
    @Q = '
DECLARE ' + @DECLARE + '
-- Cursor to scroll through object names
DECLARE MY_CURSOR CURSOR FOR
    SELECT *
    FROM [' + @DB + '].DBO.[' + @TBL + ']

-- Opening Cursor for Reading
OPEN MY_CURSOR
' + @INTO + '

-- Traversing Cursor Lines (While There)
WHILE @@FETCH_STATUS = 0
BEGIN
    ' + @EXECUTE + '
    -- Reading the next line
    ' + @INTO + '
END
FIM_CURSOR:
-- Closing Cursor for Reading
CLOSE MY_CURSOR

DEALLOCATE MY_CURSOR'

EXEC SP_EXECUTESQL @Q --MAGIA
END
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.