Як написати foreach у SQL Server?


194

Я намагаюся домогтися чогось узгодженого для кожного, де я хотів би взяти ідентифікатори повернутого оператора select і використовувати кожне з них.

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

На даний момент у мене щось схоже на вищезазначене, але я отримую помилку:

Недійсна назва стовпця "idx".

Може хтось


2
Як повторити набір результатів за допомогою Transact-SQL на SQL Server: support.microsoft.com/kb/111401/nl
Anonymousmoose

idxє в @Practitionerне Practitioner. Найчастіше є найвищі альтернативи набору для кожного підходу, якщо ви показуєте, що ви робите зі значенням рядка, можливо, може бути запропонована альтернатива.
Алекс К.

1
Будь ласка, опублікуйте більше про те, що ви намагаєтеся зробити. Уникайте РБАР, як чума (99% часу). simple-talk.com/sql/t-sql-programming/…
granadaCoder

1
RBAR Погано, Набір на основі добра.
granadaCoder

Якщо ви скажете нам, що --Do something with Id hereце таке, швидше за все, ми можемо показати вам, як вирішити цю проблему без петель чи курсорів. У більшості випадків ви хочете скористатися набором рішення, оскільки саме так оптимізовано роботу SQL Server. Цикл та обробка одного ряду одночасно, безумовно, має своє місце, але я підозрюю, що це не так.
Аарон Бертран

Відповіді:


343

Ви, здається, хочете використовувати CURSOR. Хоча в більшості випадків найкраще використовувати набір на основі рішення, є випадки, коли CURSORце найкраще рішення. Не знаючи більше про вашу справжню проблему, ми не можемо допомогти вам більше ніж:

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
БУДЬТЕ, не починайте використовувати курсори вліво та вправо. Вони потрібні <1% часу. Рішення RBAR (рядок за агонізуючим рядком), як правило, є поганими виконавцями та викликають головні болі. Якщо ви новачок, ЗАБУДУЙТЕ, навчитися вивчити цей урок рано.
granadaCoder

136

Припустимо, що стовпець PractitionerId є унікальним, тоді ви можете використовувати наступний цикл

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
Занадто просто, щоб бути правдою. Ви вибираєте MIN (PractitionerId) завжди всередині циклу. Яка умова виходу з циклу? мені схоже на нескінченну петлю.
bluelabel

7
@bluelabel для виходу з циклу циклу має таку умову, ЯКЩО ПрактикуючийДІЙСЬ НІЛЬКИЙ НАРУШЕННЯ
Олександр Федоренко

16

Ваша кількість вибору та максимум вибору повинні залежати від змінної таблиці замість фактичної таблиці

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

Це, як правило, (майже завжди) краще, ніж курсор, і простіше:

    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

Я б сказав, що все, ймовірно, працює, за винятком того, що стовпець idxнасправді не існує в таблиці, з якої ви вибираєте. Можливо, ви мали намір вибрати з @Practitioner:

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

тому що це визначено в наведеному вище коді так:

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

3

У вашій версії неправильний наступний рядок:

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

(Відсутність @)

Можливо, буде ідея змінити умову іменування, щоб таблиці були більш різними.


2

Хоча курсори зазвичай вважають жахливим злом, я вважаю, що це стосується курсору FAST_FORWARD - найближчого, що ви можете дістатись до FOREACH в TSQL.


2

Я зробив процедуру, виконати FOREACHз CURSORбудь-таблицею.

Приклад використання:

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

Результат - 2 вибору для кожного ряду. Синтаксис UPDATEі розрив FOREACHнаписано в підказках.

Це прок-код:

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

1

Я придумав дуже ефективний (я думаю) читабельний спосіб зробити це.

    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.

Ось код. Вибачте, він використовує мої назви змінних замість тих, що містяться в питанні.

            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

Ось одне з кращих рішень.

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

Тут я додав деякі значення в таблицю, оскільки вона спочатку порожня.

Ми можемо отримати доступ або ми можемо зробити все, що є в тілі циклу, і ми можемо отримати доступ до idx, визначивши його всередині визначення таблиці.

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

                --Do something with Id here

                PRINT @curren_val
                SET @i = @i + 1
            END
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.