Wie schreibe ich ein foreach in SQL Server?

192

Ich versuche, etwas in der Art eines For-Each zu erreichen, wobei ich die IDs einer zurückgegebenen select-Anweisung nehmen und jede von ihnen verwenden möchte.

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

Im Moment habe ich etwas, das wie oben aussieht, aber ich bekomme den Fehler:

Ungültiger Spaltenname 'idx'.

Könnte jemand

Pomster
quelle
2
So durchlaufen Sie eine Ergebnismenge mithilfe von Transact-SQL in SQL Server: support.microsoft.com/kb/111401/nl
Anonymoose
idxist in @Practitionernicht Practitioner. Es gibt meistens überlegene satzbasierte Alternativen zu einem Ansatz für jeden Ansatz. Wenn Sie zeigen, was Sie mit dem Zeilenwert tun, kann möglicherweise eine Alternative vorgeschlagen werden.
Alex K.
1
Bitte posten Sie mehr darüber, was Sie erreichen möchten. Vermeiden Sie RBAR wie die Pest (99% der Zeit). simple-talk.com/sql/t-sql-programming/…
granadaCoder
1
RBAR Schlecht, Set-basiert gut.
GranadaCoder
Wenn Sie uns sagen, was --Do something with Id hereist, können wir Ihnen wahrscheinlich zeigen, wie Sie dieses Problem ohne Schleifen oder Cursor lösen können. In den meisten Fällen möchten Sie eine satzbasierte Lösung verwenden, da SQL Server auf diese Weise für die Funktionsweise optimiert ist. Das Schleifen und Behandeln einer Reihe nach der anderen hat sicherlich seinen Platz, aber ich vermute, dass dies nicht der Fall ist.
Aaron Bertrand

Antworten:

342

Sie scheinen eine verwenden zu wollen CURSOR. Obwohl es in den meisten Fällen am besten ist, eine satzbasierte Lösung zu verwenden, gibt es manchmal Fälle, in denen a CURSORdie beste Lösung ist. Ohne mehr über Ihr eigentliches Problem zu wissen, können wir Ihnen nicht mehr helfen:

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
quelle
41
BITTE verwenden Sie keine Cursor links und rechts. Sie werden <1% der Zeit benötigt. RBAR-Lösungen (Row by Agonizing Row) sind in der Regel schlecht und verursachen Kopfschmerzen. Wenn Sie neu sind, versuchen Sie bitte, diese Lektion früh zu lernen.
GranadaCoder
135

Angenommen, die Spalte PractitionerId ist eindeutig, dann können Sie die folgende Schleife verwenden

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
quelle
1
Zu einfach um wahr zu sein. Sie wählen MIN (PractitionerId) immer innerhalb der Schleife. Was ist die Bedingung, um die Schleife zu verlassen? sieht für mich wie eine Endlosschleife aus.
Bluelabel
7
@bluelabel zum Beenden des Schleifenskripts hat die folgende Bedingung, WENN PractitionerId NULL BREAK IST
Aleksandr Fedorenko
16

Ihre Auswahlanzahl und Auswahlmaximum sollten aus Ihrer Tabellenvariablen anstelle der tatsächlichen Tabelle stammen

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
quelle
15

Dies ist im Allgemeinen (fast immer) besser als ein Cursor und einfacher:

    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
quelle
5

Ich würde sagen, dass wahrscheinlich alles funktioniert, außer dass die Spalte idxin der Tabelle, aus der Sie auswählen, nicht vorhanden ist. Vielleicht wollten Sie auswählen aus @Practitioner:

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

weil das im obigen Code so definiert ist:

DECLARE @Practitioner TABLE (
    idx smallint Primary Key IDENTITY(1,1)
    , PractitionerId int
)
Mike Perrenoud
quelle
2

Die folgende Zeile ist in Ihrer Version falsch:

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

(Das @ fehlt)

Möglicherweise sollten Sie Ihre Namenskonvention so ändern, dass die Tabellen unterschiedlicher sind.

Jon Egerton
quelle
1

Obwohl Cursor normalerweise als schrecklich böse angesehen werden, glaube ich, dass dies ein Fall für den FAST_FORWARD-Cursor ist - das, was Sie FOREACH in TSQL am nächsten bringen können.

Yuriy Galanter
quelle
1

Ich habe mir einen sehr effektiven (meiner Meinung nach) lesbaren Weg ausgedacht, dies zu tun.

    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.

Hier ist der Code. Entschuldigung, es werden meine Variablennamen anstelle der in der Frage angegebenen verwendet.

            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
quelle
1

Hier ist die eine der besseren Lösungen.

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

Hier habe ich einige Werte in die Tabelle eingefügt, weil sie anfangs leer ist.

Wir können auf den Hauptteil der Schleife zugreifen oder alles tun, und wir können auf den IDX zugreifen, indem wir ihn in der Tabellendefinition definieren.

              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.
quelle
1

Ich habe eine Prozedur erstellt, die ein FOREACHwith CURSORfür jede Tabelle ausführt .

Anwendungsbeispiel:

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

Das Ergebnis sind 2 Auswahlen für jede Zeile. Die Syntax von UPDATEund break the FOREACHsind in den Hinweisen geschrieben.

Dies ist der Proc-Code:

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
quelle