Bagaimana cara menulis foreach di SQL Server?

194

Saya mencoba untuk mencapai sesuatu di sepanjang garis untuk masing-masing, di mana saya ingin mengambil Id pernyataan pilih kembali dan menggunakan masing-masing.

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

Saat ini saya memiliki sesuatu yang terlihat seperti di atas, tetapi saya mendapatkan kesalahan:

Nama kolom 'idx' tidak valid.

Bisakah seseorang

Pomster
sumber
2
Bagaimana cara mengulang melalui hasil yang ditetapkan dengan menggunakan Transact-SQL di SQL Server: support.microsoft.com/kb/111401/nl
Anonymoose
idxadalah @Practitionertidak Practitioner. Ada paling sering alternatif set-based yang unggul untuk setiap pendekatan, jika Anda menunjukkan apa yang Anda lakukan dengan nilai baris, mungkin sebuah alternatif dapat disarankan.
Alex K.
1
Silakan kirim lebih banyak tentang apa yang ingin Anda capai. Hindari RBAR seperti wabah (99% dari waktu). simple-talk.com/sql/t-sql-programming/…
granadaCoder
1
RBAR Buruk, Berbasis baik.
granadaCoder
Jika Anda memberi tahu kami apa yang --Do something with Id hereada, kemungkinan kami dapat menunjukkan kepada Anda bagaimana menyelesaikan masalah ini tanpa ada loop atau kursor. Dalam kebanyakan kasus, Anda ingin menggunakan solusi berbasis set, karena itulah cara SQL Server dioptimalkan untuk bekerja. Berkeliaran dan merawat satu baris pada satu waktu tentu memiliki tempatnya, tetapi saya curiga ini bukan.
Aaron Bertrand

Jawaban:

343

Anda sepertinya ingin menggunakan a CURSOR. Meskipun sebagian besar waktu terbaik untuk menggunakan solusi berbasis set, ada beberapa kali di mana a CURSORadalah solusi terbaik. Tanpa mengetahui lebih banyak tentang masalah Anda yang sebenarnya, kami tidak dapat membantu Anda lebih dari itu:

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
sumber
41
TOLONG jangan mulai menggunakan kursor kiri dan kanan. Mereka dibutuhkan <1% dari waktu. Solusi RBAR (baris demi baris yang menyakitkan) biasanya berkinerja buruk dan menyebabkan sakit kepala. Jika Anda baru, HARAP mencoba mempelajari pelajaran ini lebih awal.
granadaCoder
136

Misalkan kolom PractitionerId adalah unik, maka Anda dapat menggunakan loop berikut

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
sumber
1
Terlalu sederhana untuk menjadi kenyataan. Anda memilih MIN (PractitionerId) selalu di dalam loop. Bagaimana kondisi untuk keluar dari loop? Sepertinya loop tak terbatas bagi saya.
bluelabel
7
@bluelabel untuk keluar dari skrip loop memiliki kondisi berikut JIKA Praktisi AKAN NULL BREAK
Aleksandr Fedorenko
16

Hitungan pilih dan maks pilih Anda harus dari variabel tabel Anda, bukan tabel sebenarnya

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

Ini umumnya (hampir selalu) berkinerja lebih baik daripada kursor dan lebih sederhana:

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

Saya akan mengatakan semuanya mungkin berfungsi kecuali bahwa kolom idxtidak benar-benar ada di tabel yang Anda pilih. Mungkin Anda bermaksud memilih dari @Practitioner:

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

karena itu didefinisikan dalam kode di atas seperti itu:

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

Baris berikut salah dalam versi Anda:

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

(Tidak ada @)

Mungkin ide untuk mengubah konvensi penamaan Anda sehingga tabel lebih berbeda.

Jon Egerton
sumber
2

Meskipun kursor biasanya dianggap jahat yang mengerikan, saya percaya ini adalah kasus untuk kursor FAST_FORWARD - hal terdekat yang bisa Anda dapatkan di FOREACH di TSQL.

Yuriy Galanter
sumber
2

Saya membuat prosedur yang menjalankan FOREACHdengan CURSORuntuk tabel apa pun.

Contoh penggunaan:

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

Hasilnya adalah 2 pilihan untuk setiap baris. Sintaks UPDATEdan break FOREACHditulis dalam petunjuk.

Ini adalah kode 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
sumber
1

Saya datang dengan cara yang sangat efektif, (saya pikir) dapat dibaca untuk melakukan ini.

    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.

Ini kodenya. Maaf, ini menggunakan nama variabel saya dan bukan yang ada di pertanyaan.

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

Inilah salah satu solusi yang lebih baik.

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

Di sini saya telah menambahkan beberapa nilai dalam tabel karena awalnya kosong.

Kita dapat mengakses atau kita dapat melakukan apa saja di tubuh loop dan kita dapat mengakses idx dengan mendefinisikannya di dalam definisi tabel.

              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
sumber