วิธีเขียน foreach ใน SQL Server


194

ฉันพยายามที่จะบรรลุบางสิ่งบางอย่างตามสายงานของแต่ละคนซึ่งฉันต้องการรับ Ids ของคำสั่ง 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
Anonymoose

idxอยู่ในไม่ได้@Practitioner Practitionerมักจะมีทางเลือกชุดที่เหนือกว่าสำหรับแต่ละวิธีหากคุณแสดงสิ่งที่คุณทำกับค่าแถวบางทีอาจมีทางเลือกอื่นแนะนำ
Alex K.

1
โปรดโพสต์เพิ่มเติมเกี่ยวกับสิ่งที่คุณพยายามทำให้สำเร็จ หลีกเลี่ยง RBAR เช่นโรคระบาด (99% ของเวลา) simple-talk.com/sql/t-sql-programming/…
granadaCoder

1
RBAR ไม่ดีชุดดี
granadaCoder

หากคุณบอกเราว่าอะไร--Do something with Id hereคือสิ่งที่เป็นไปได้ที่เราจะแสดงให้คุณเห็นถึงวิธีการแก้ปัญหานี้โดยไม่มีลูปหรือเคอร์เซอร์ ในกรณีส่วนใหญ่คุณต้องการใช้โซลูชันที่ตั้งค่าเนื่องจากเป็นวิธีที่ SQL Server ได้รับการปรับให้เหมาะกับการทำงาน การวนลูปและการรักษาทีละแถวมีที่แน่นอน แต่ฉันคิดว่านี่ไม่ใช่ของมัน
Aaron Bertrand

คำตอบ:


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 เพื่อออกจากสคริปต์ loop มีเงื่อนไขดังต่อไปนี้ IF PractitionerId IS NULL BREAK
Aleksandr Fedorenko

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ถูกเขียนในคำแนะนำ

นี่คือรหัส 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

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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.