Comment écrire un foreach dans SQL Server?

194

J'essaie de réaliser quelque chose comme un for-each, où je voudrais prendre les identifiants d'une instruction select retournée et utiliser chacun d'eux.

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

Pour le moment, j'ai quelque chose qui ressemble à ce qui précède, mais j'obtiens l'erreur:

Nom de colonne non valide "idx".

Quelqu'un pourrait-il

Pomster
la source
2
Comment parcourir un jeu de résultats à l'aide de Transact-SQL dans SQL Server: support.microsoft.com/kb/111401/nl
Anonymoose
idxn'est @Practitionerpas Practitioner. Il existe le plus souvent des alternatives basées sur des ensembles supérieures à une approche pour chaque, si vous montrez ce que vous faites avec la valeur de ligne, une alternative peut peut-être être suggérée.
Alex K.
1
Veuillez publier plus d'informations sur ce que vous essayez d'accomplir. Évitez RBAR comme la peste (99% du temps). simple-talk.com/sql/t-sql-programming/…
granadaCoder
1
RBAR Mauvais, bon basé sur les sets.
granadaCoder
Si vous nous dites ce que --Do something with Id herec'est, il est probable que nous puissions vous montrer comment résoudre ce problème sans boucles ni curseurs. Dans la plupart des cas, vous souhaitez utiliser une solution basée sur des ensembles, car c'est ainsi que SQL Server est optimisé pour fonctionner. Boucler et traiter une ligne à la fois a certainement sa place, mais je soupçonne que ce n'est pas ça.
Aaron Bertrand

Réponses:

343

Vous semblez vouloir utiliser un fichier CURSOR. Bien que la plupart du temps, il soit préférable d'utiliser une solution basée sur un ensemble, il y a des moments où a CURSORest la meilleure solution. Sans en savoir plus sur votre vrai problème, nous ne pouvons pas vous aider plus que cela:

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
Lamak
la source
41
VEUILLEZ ne pas commencer à utiliser les curseurs gauche et droit. Ils sont nécessaires <1% du temps. Les solutions RBAR (ligne par ligne angoissante) sont généralement de mauvaises performances et provoquent des maux de tête. Si vous êtes nouveau, VEUILLEZ essayer d'apprendre cette leçon tôt.
granadaCoder
136

Supposons que la colonne PractitionerId soit unique, vous pouvez alors utiliser la boucle suivante

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
Aleksandr Fedorenko
la source
1
Trop simple pour être vrai. Vous sélectionnez MIN (PractitionerId) toujours à l'intérieur de la boucle. Quelle est la condition pour sortir de la boucle? ressemble à une boucle infinie pour moi.
bluelabel
7
@bluelabel pour quitter le script de boucle a la condition suivante IF PractitionerId IS NULL BREAK
Aleksandr Fedorenko
16

Le nombre de sélection et le maximum de sélection doivent provenir de votre variable de table au lieu de la table réelle

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
Grax32
la source
15

Cela fonctionne généralement (presque toujours) mieux qu'un curseur et est plus 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
David Sopko
la source
5

Je dirais que tout fonctionne probablement, sauf que la colonne idxn'existe pas réellement dans le tableau dans lequel vous sélectionnez. Peut-être que vous vouliez choisir parmi @Practitioner:

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

car c'est défini dans le code ci-dessus comme ça:

DECLARE @Practitioner TABLE (
    idx smallint Primary Key IDENTITY(1,1)
    , PractitionerId int
)
Mike Perrenoud
la source
3

La ligne suivante est erronée dans votre version:

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

(Manque le @)

Cela pourrait être une idée de changer votre convention de dénomination afin que les tables soient plus différentes.

Jon Egerton
la source
2

Bien que les curseurs soient généralement considérés comme un mal horrible, je pense que c'est un cas pour le curseur FAST_FORWARD - la chose la plus proche que vous pouvez obtenir de FOREACH dans TSQL.

Yuriy Galanter
la source
2

J'ai fait une procédure qui exécute un FOREACHavecCURSOR pour n'importe quelle table.

Exemple d'utilisation:

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

Le résultat est 2 sélections pour chaque ligne. La syntaxe de UPDATEet casser leFOREACH sont écrites dans les indices.

Voici le code proc:

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
Erick de Vathaire
la source
1

J'ai trouvé un moyen très efficace, (je pense) lisible de le faire.

    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.

Voici le code. Désolé, il utilise mes noms de variables au lieu de ceux de la question.

            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
pdschuller
la source
1

Voici l'une des meilleures solutions.

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

Ici, j'ai ajouté quelques valeurs dans le tableau car il est initialement vide.

Nous pouvons accéder ou nous pouvons faire n'importe quoi dans le corps de la boucle et nous pouvons accéder à l'idx en le définissant dans la définition de la table.

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

                --Do something with Id here

                PRINT @curren_val
                SET @i = @i + 1
            END
Joseph M
la source