Apakah ada cara untuk mengulang melalui variabel tabel di TSQL tanpa menggunakan kursor?


243

Katakanlah saya memiliki variabel tabel sederhana berikut:

declare @databases table
(
    DatabaseID    int,
    Name        varchar(15),   
    Server      varchar(15)
)
-- insert a bunch rows into @databases

Apakah mendeklarasikan dan menggunakan kursor adalah satu-satunya pilihan saya jika saya ingin mengulangi baris tersebut? Apakah ada cara lain?


3
Meskipun saya tidak yakin masalah yang Anda lihat dengan pendekatan di atas; Lihat apakah ini membantu .. databasejournal.com/features/mssql/article.php/3111031
Gishu

5
Bisakah Anda memberi kami alasan mengapa Anda ingin beralih ke baris, solusi lain yang tidak memerlukan iterasi mungkin ada (dan yang lebih cepat dengan margin besar dalam kebanyakan kasus)
Pop Catalin

setuju dengan pop ... mungkin tidak memerlukan kursor tergantung pada situasinya. tetapi tidak ada masalah dengan menggunakan kursor jika Anda perlu
Shawn

3
Anda tidak menyatakan mengapa Anda ingin menghindari kursor. Ketahuilah bahwa kursor mungkin merupakan cara paling sederhana untuk beralih. Anda mungkin pernah mendengar bahwa kursor 'buruk', tetapi sebenarnya iterasi lebih dari tabel yang buruk dibandingkan dengan operasi berbasis set. Jika Anda tidak dapat menghindari iterasi, kursor mungkin merupakan cara terbaik. Mengunci adalah masalah lain dengan kursor, tetapi itu tidak relevan ketika menggunakan variabel tabel.
JacquesB

1
Menggunakan kursor bukan satu - satunya pilihan Anda, tetapi jika Anda tidak memiliki cara untuk menghindari pendekatan baris-demi-baris, maka itu akan menjadi pilihan terbaik Anda. CURSOR adalah konstruksi bawaan yang lebih efisien dan lebih rentan kesalahan daripada melakukan loop WHILE konyol Anda sendiri. Sebagian besar waktu Anda hanya perlu menggunakan STATICopsi untuk menghapus pemeriksaan ulang terus-menerus dari tabel dasar dan penguncian yang ada secara default dan menyebabkan sebagian besar orang secara keliru percaya bahwa CURSOR jahat. @ JacquesB sangat dekat: periksa kembali untuk melihat apakah baris hasil masih ada + penguncian adalah masalah. Dan STATICbiasanya memperbaikinya :-).
Solomon Rutzky

Jawaban:


376

Pertama-tama Anda harus benar-benar yakin Anda perlu mengulangi melalui setiap baris-set operasi berbasis akan melakukan lebih cepat dalam setiap kasus yang dapat saya pikirkan dan biasanya akan menggunakan kode yang lebih sederhana.

Tergantung pada data Anda, dimungkinkan untuk mengulang menggunakan SELECTpernyataan adil seperti yang ditunjukkan di bawah ini:

Declare @Id int

While (Select Count(*) From ATable Where Processed = 0) > 0
Begin
    Select Top 1 @Id = Id From ATable Where Processed = 0

    --Do some processing here

    Update ATable Set Processed = 1 Where Id = @Id 

End

Alternatif lain adalah dengan menggunakan tabel sementara:

Select *
Into   #Temp
From   ATable

Declare @Id int

While (Select Count(*) From #Temp) > 0
Begin

    Select Top 1 @Id = Id From #Temp

    --Do some processing here

    Delete #Temp Where Id = @Id

End

Opsi yang harus Anda pilih benar-benar tergantung pada struktur dan volume data Anda.

Catatan: Jika Anda menggunakan SQL Server, Anda akan lebih baik dilayani menggunakan:

WHILE EXISTS(SELECT * FROM #Temp)

Menggunakan COUNTharus menyentuh setiap baris dalam tabel, EXISTShanya perlu menyentuh yang pertama (lihat jawaban Josef di bawah).


"Pilih Atas 1 @Id = Id Dari ATable" harus "Pilih Atas 1 @Id = Id Dari ATable Di mana Diproses = 0"
Amzath

10
Jika menggunakan SQL Server, lihat jawaban Josef di bawah ini untuk perubahan kecil ke atas.
Polshgiant

3
Bisakah Anda menjelaskan mengapa ini lebih baik daripada menggunakan kursor?
marco-fiset

5
Memberi ini downvote. Kenapa dia harus menghindari menggunakan kursor? Dia berbicara tentang iterasi variabel tabel , bukan tabel tradisional. Saya tidak percaya kelemahan normal kursor berlaku di sini. Jika pemrosesan baris demi baris benar-benar diperlukan (dan seperti yang Anda tunjukkan, ia harus yakin tentang hal itu terlebih dahulu) maka menggunakan kursor adalah solusi yang jauh lebih baik daripada yang Anda jelaskan di sini.
peterh

@peterh Anda benar. Dan pada kenyataannya, Anda biasanya dapat menghindari "kerugian normal" tersebut dengan menggunakan STATICopsi yang menyalin hasil yang disetel ke tabel temp, dan karenanya Anda tidak lagi mengunci atau memeriksa ulang tabel dasar :-).
Solomon Rutzky

132

Hanya catatan singkat, jika Anda menggunakan SQL Server (2008 dan di atas), contoh-contoh yang ada:

While (Select Count(*) From #Temp) > 0

Akan lebih baik disajikan bersama

While EXISTS(SELECT * From #Temp)

Hitungan harus menyentuh setiap baris dalam tabel, EXISTShanya perlu menyentuh yang pertama.


9
Ini bukan jawaban tetapi komentar / peningkatan jawaban Martynw.
Hammad Khan

7
Isi catatan ini memaksa fungsionalitas pemformatan yang lebih baik daripada komentar, saya sarankan untuk menambahkan di Jawaban.
Custodio

2
Dalam versi SQL yang lebih baru, pengoptimal kueri cukup pintar untuk mengetahui bahwa ketika Anda menulis hal pertama, sebenarnya yang Anda maksud adalah yang kedua dan mengoptimalkannya untuk menghindari pemindaian tabel.
Dan Def

39

Beginilah cara saya melakukannya:

declare @RowNum int, @CustId nchar(5), @Name1 nchar(25)

select @CustId=MAX(USERID) FROM UserIDs     --start with the highest ID
Select @RowNum = Count(*) From UserIDs      --get total number of records
WHILE @RowNum > 0                          --loop until no more records
BEGIN   
    select @Name1 = username1 from UserIDs where USERID= @CustID    --get other info from that row
    print cast(@RowNum as char(12)) + ' ' + @CustId + ' ' + @Name1  --do whatever

    select top 1 @CustId=USERID from UserIDs where USERID < @CustID order by USERID desc--get the next one
    set @RowNum = @RowNum - 1                               --decrease count
END

Tidak ada kursor, tidak ada tabel sementara, tidak ada kolom tambahan. Kolom USERID harus berupa bilangan bulat unik, seperti kebanyakan Kunci Utama.


26

Tentukan tabel temp Anda seperti ini -

declare @databases table
(
    RowID int not null identity(1,1) primary key,
    DatabaseID    int,
    Name        varchar(15),   
    Server      varchar(15)
)

-- insert a bunch rows into @databases

Kemudian lakukan ini -

declare @i int
select @i = min(RowID) from @databases
declare @max int
select @max = max(RowID) from @databases

while @i <= @max begin
    select DatabaseID, Name, Server from @database where RowID = @i --do some stuff
    set @i = @i + 1
end

16

Inilah cara saya akan melakukannya:

Select Identity(int, 1,1) AS PK, DatabaseID
Into   #T
From   @databases

Declare @maxPK int;Select @maxPK = MAX(PK) From #T
Declare @pk int;Set @pk = 1

While @pk <= @maxPK
Begin

    -- Get one record
    Select DatabaseID, Name, Server
    From @databases
    Where DatabaseID = (Select DatabaseID From #T Where PK = @pk)

    --Do some processing here
    -- 

    Select @pk = @pk + 1
End

[Sunting] Karena saya mungkin melewatkan kata "variabel" ketika pertama kali membaca pertanyaan, berikut ini adalah respons yang diperbarui ...


declare @databases table
(
    PK            int IDENTITY(1,1), 
    DatabaseID    int,
    Name        varchar(15),   
    Server      varchar(15)
)
-- insert a bunch rows into @databases
--/*
INSERT INTO @databases (DatabaseID, Name, Server) SELECT 1,'MainDB', 'MyServer'
INSERT INTO @databases (DatabaseID, Name, Server) SELECT 1,'MyDB',   'MyServer2'
--*/

Declare @maxPK int;Select @maxPK = MAX(PK) From @databases
Declare @pk int;Set @pk = 1

While @pk <= @maxPK
Begin

    /* Get one record (you can read the values into some variables) */
    Select DatabaseID, Name, Server
    From @databases
    Where PK = @pk

    /* Do some processing here */
    /* ... */ 

    Select @pk = @pk + 1
End

4
jadi pada dasarnya Anda melakukan kursor, tetapi tanpa semua manfaat kursor
Shawn

1
... tanpa mengunci tabel yang digunakan saat memproses ... karena ini adalah salah satu manfaat kursor :)
leoinfo

3
Meja? Ini tabel VARIABEL - tidak ada akses bersamaan mungkin.
DenNukem

DenNukem, Anda benar, saya pikir saya "melewatkan" kata "variabel" ketika saya membaca pertanyaan pada waktu itu ... Saya akan menambahkan beberapa catatan pada respons awal saya
leoinfo

Saya harus setuju dengan DenNukem dan Shawn. Mengapa, mengapa, mengapa Anda berusaha sejauh ini untuk menghindari menggunakan kursor? Sekali lagi: ia ingin beralih pada variabel tabel, bukan tabel tradisional !!!
peterh

10

Jika Anda tidak punya pilihan selain pergi baris demi baris membuat kursor FAST_FORWARD. Ini akan secepat membangun loop sementara dan jauh lebih mudah untuk mempertahankan dalam jangka panjang.

FAST_FORWARD Menentukan FORWARD_ONLY, READ_ONLY kursor dengan optimasi kinerja diaktifkan. FAST_FORWARD tidak dapat ditentukan jika SCROLL atau FOR_UPDATE juga ditentukan.


2
Ya! Seperti yang saya komentari di tempat lain saya belum melihat argumen mengapa TIDAK untuk menggunakan kursor ketika kasus ini untuk beralih pada variabel tabel . Sebuah FAST_FORWARDkursor adalah solusi yang baik. (
Suara positif

5

Pendekatan lain tanpa harus mengubah skema Anda atau menggunakan tabel temp:

DECLARE @rowCount int = 0
  ,@currentRow int = 1
  ,@databaseID int
  ,@name varchar(15)
  ,@server varchar(15);

SELECT @rowCount = COUNT(*)
FROM @databases;

WHILE (@currentRow <= @rowCount)
BEGIN
  SELECT TOP 1
     @databaseID = rt.[DatabaseID]
    ,@name = rt.[Name]
    ,@server = rt.[Server]
  FROM (
    SELECT ROW_NUMBER() OVER (
        ORDER BY t.[DatabaseID], t.[Name], t.[Server]
       ) AS [RowNumber]
      ,t.[DatabaseID]
      ,t.[Name]
      ,t.[Server]
    FROM @databases t
  ) rt
  WHERE rt.[RowNumber] = @currentRow;

  EXEC [your_stored_procedure] @databaseID, @name, @server;

  SET @currentRow = @currentRow + 1;
END

4

Anda dapat menggunakan loop sementara:

While (Select Count(*) From #TempTable) > 0
Begin
    Insert Into @Databases...

    Delete From #TempTable Where x = x
End

4

Ini akan bekerja dalam versi SQL SERVER 2012.

declare @Rowcount int 
select @Rowcount=count(*) from AddressTable;

while( @Rowcount>0)
  begin 
 select @Rowcount=@Rowcount-1;
 SELECT * FROM AddressTable order by AddressId desc OFFSET @Rowcount ROWS FETCH NEXT 1 ROWS ONLY;
end 

4

Ringan, tanpa harus membuat tabel tambahan, jika Anda memiliki bilangan bulat IDdi atas meja

Declare @id int = 0, @anything nvarchar(max)
WHILE(1=1) BEGIN
  Select Top 1 @anything=[Anything],@id=@id+1 FROM Table WHERE ID>@id
  if(@@ROWCOUNT=0) break;

  --Process @anything

END

3
-- [PO_RollBackOnReject]  'FININV10532'
alter procedure PO_RollBackOnReject
@CaseID nvarchar(100)

AS
Begin
SELECT  *
INTO    #tmpTable
FROM   PO_InvoiceItems where CaseID = @CaseID

Declare @Id int
Declare @PO_No int
Declare @Current_Balance Money


While (Select ROW_NUMBER() OVER(ORDER BY PO_LineNo DESC) From #tmpTable) > 0
Begin
        Select Top 1 @Id = PO_LineNo, @Current_Balance = Current_Balance,
        @PO_No = PO_No
        From #Temp
        update PO_Details
        Set  Current_Balance = Current_Balance + @Current_Balance,
            Previous_App_Amount= Previous_App_Amount + @Current_Balance,
            Is_Processed = 0
        Where PO_LineNumber = @Id
        AND PO_No = @PO_No
        update PO_InvoiceItems
        Set IsVisible = 0,
        Is_Processed= 0
        ,Is_InProgress = 0 , 
        Is_Active = 0
        Where PO_LineNo = @Id
        AND PO_No = @PO_No
End
End

2

Saya benar-benar tidak mengerti mengapa Anda harus menggunakan ketakutan cursor. Tapi di sini ada opsi lain jika Anda menggunakan SQL Server versi 2005/2008
Gunakan Rekursi

declare @databases table
(
    DatabaseID    int,
    Name        varchar(15),   
    Server      varchar(15)
)

--; Insert records into @databases...

--; Recurse through @databases
;with DBs as (
    select * from @databases where DatabaseID = 1
    union all
    select A.* from @databases A 
        inner join DBs B on A.DatabaseID = B.DatabaseID + 1
)
select * from DBs

2

Saya akan memberikan solusi berbasis set.

insert  @databases (DatabaseID, Name, Server)
select DatabaseID, Name, Server 
From ... (Use whatever query you would have used in the loop or cursor)

Ini jauh lebih cepat daripada teknik perulangan dan lebih mudah untuk menulis dan memelihara.


2

Saya lebih suka menggunakan Pengambilan Offset jika Anda memiliki ID unik, Anda dapat mengurutkan tabel berdasarkan:

DECLARE @TableVariable (ID int, Name varchar(50));
DECLARE @RecordCount int;
SELECT @RecordCount = COUNT(*) FROM @TableVariable;

WHILE @RecordCount > 0
BEGIN
SELECT ID, Name FROM @TableVariable ORDER BY ID OFFSET @RecordCount - 1 FETCH NEXT 1 ROW;
SET @RecordCount = @RecordCount - 1;
END

Dengan cara ini saya tidak perlu menambahkan bidang ke tabel atau menggunakan fungsi jendela.


2

Mungkin untuk menggunakan kursor untuk melakukan ini:

buat fungsi [dbo] .f_teste_loop mengembalikan tabel @tabela (cod int, nome varchar (10)) sebagai awal

insert into @tabela values (1, 'verde');
insert into @tabela values (2, 'amarelo');
insert into @tabela values (3, 'azul');
insert into @tabela values (4, 'branco');

return;

akhir

buat prosedur [dbo]. [sp_teste_loop] sebagai awal

DECLARE @cod int, @nome varchar(10);

DECLARE curLoop CURSOR STATIC LOCAL 
FOR
SELECT  
    cod
   ,nome
FROM 
    dbo.f_teste_loop();

OPEN curLoop;

FETCH NEXT FROM curLoop
           INTO @cod, @nome;

WHILE (@@FETCH_STATUS = 0)
BEGIN
    PRINT @nome;

    FETCH NEXT FROM curLoop
           INTO @cod, @nome;
END

CLOSE curLoop;
DEALLOCATE curLoop;

akhir


1
Bukankah pertanyaan awal "Tanpa menggunakan kursor"?
Fernando Gonzalez Sanchez

1

Saya setuju dengan posting sebelumnya bahwa operasi berbasis set biasanya akan berkinerja lebih baik, tetapi jika Anda perlu mengulangi baris berikut inilah pendekatan yang akan saya ambil:

  1. Tambahkan bidang baru ke variabel tabel Anda (Bit Jenis Data, default 0)
  2. Masukkan data Anda
  3. Pilih Baris 1 Atas di mana fUsed = 0 (Catatan: fUsed adalah nama bidang pada langkah 1)
  4. Lakukan pemrosesan apa pun yang perlu Anda lakukan
  5. Perbarui catatan dalam variabel tabel Anda dengan menetapkan fUsed = 1 untuk catatan
  6. Pilih catatan yang tidak digunakan berikutnya dari tabel dan ulangi prosesnya

    DECLARE @databases TABLE  
    (  
        DatabaseID  int,  
        Name        varchar(15),     
        Server      varchar(15),   
        fUsed       BIT DEFAULT 0  
    ) 
    
    -- insert a bunch rows into @databases
    
    DECLARE @DBID INT
    
    SELECT TOP 1 @DBID = DatabaseID from @databases where fUsed = 0 
    
    WHILE @@ROWCOUNT <> 0 and @DBID IS NOT NULL  
    BEGIN  
        -- Perform your processing here  
    
        --Update the record to "used" 
    
        UPDATE @databases SET fUsed = 1 WHERE DatabaseID = @DBID  
    
        --Get the next record  
        SELECT TOP 1 @DBID = DatabaseID from @databases where fUsed = 0   
    END

1

Langkah1: Pernyataan pilih di bawah ini membuat tabel temp dengan nomor baris unik untuk setiap catatan.

select eno,ename,eaddress,mobno int,row_number() over(order by eno desc) as rno into #tmp_sri from emp 

Langkah2: Deklarasikan variabel yang diperlukan

DECLARE @ROWNUMBER INT
DECLARE @ename varchar(100)

Langkah 3: Ambil jumlah baris total dari tabel temp

SELECT @ROWNUMBER = COUNT(*) FROM #tmp_sri
declare @rno int

Langkah4: Loop tabel temp berdasarkan nomor baris unik buat di temp

while @rownumber>0
begin
  set @rno=@rownumber
  select @ename=ename from #tmp_sri where rno=@rno  **// You can take columns data from here as many as you want**
  set @rownumber=@rownumber-1
  print @ename **// instead of printing, you can write insert, update, delete statements**
end

1

Pendekatan ini hanya memerlukan satu variabel dan tidak menghapus baris apa pun dari @databases. Saya tahu ada banyak jawaban di sini, tetapi saya tidak melihat jawaban yang menggunakan MIN untuk mendapatkan ID Anda berikutnya seperti ini.

DECLARE @databases TABLE
(
    DatabaseID    int,
    Name        varchar(15),   
    Server      varchar(15)
)

-- insert a bunch rows into @databases

DECLARE @CurrID INT

SELECT @CurrID = MIN(DatabaseID)
FROM @databases

WHILE @CurrID IS NOT NULL
BEGIN

    -- Do stuff for @CurrID

    SELECT @CurrID = MIN(DatabaseID)
    FROM @databases
    WHERE DatabaseID > @CurrID

END

1

Inilah solusi saya, yang menggunakan loop tak terbatas, BREAKpernyataan, dan @@ROWCOUNTfungsinya. Tidak ada kursor atau tabel sementara yang diperlukan, dan saya hanya perlu menulis satu kueri untuk mendapatkan baris berikutnya dalam @databasestabel:

declare @databases table
(
    DatabaseID    int,
    [Name]        varchar(15),   
    [Server]      varchar(15)
);


-- Populate the [@databases] table with test data.
insert into @databases (DatabaseID, [Name], [Server])
select X.DatabaseID, X.[Name], X.[Server]
from (values 
    (1, 'Roger', 'ServerA'),
    (5, 'Suzy', 'ServerB'),
    (8675309, 'Jenny', 'TommyTutone')
) X (DatabaseID, [Name], [Server])


-- Create an infinite loop & ensure that a break condition is reached in the loop code.
declare @databaseId int;

while (1=1)
begin
    -- Get the next database ID.
    select top(1) @databaseId = DatabaseId 
    from @databases 
    where DatabaseId > isnull(@databaseId, 0);

    -- If no rows were found by the preceding SQL query, you're done; exit the WHILE loop.
    if (@@ROWCOUNT = 0) break;

    -- Otherwise, do whatever you need to do with the current [@databases] table row here.
    print 'Processing @databaseId #' + cast(@databaseId as varchar(50));
end

Saya baru menyadari bahwa @ControlFreak merekomendasikan pendekatan ini sebelum saya; Saya hanya menambahkan komentar dan contoh yang lebih verbose.
Mass Dot Net

0

Ini adalah kode yang saya gunakan 2008 R2. Kode ini yang saya gunakan adalah untuk membuat indeks pada bidang kunci (SSNO & EMPR_NO) di semua kisah

if object_ID('tempdb..#a')is not NULL drop table #a

select 'IF EXISTS (SELECT name FROM sysindexes WHERE name ='+CHAR(39)+''+'IDX_'+COLUMN_NAME+'_'+SUBSTRING(table_name,5,len(table_name)-3)+char(39)+')' 
+' begin DROP INDEX [IDX_'+COLUMN_NAME+'_'+SUBSTRING(table_name,5,len(table_name)-3)+'] ON '+table_schema+'.'+table_name+' END Create index IDX_'+COLUMN_NAME+'_'+SUBSTRING(table_name,5,len(table_name)-3)+ ' on '+ table_schema+'.'+table_name+' ('+COLUMN_NAME+') '   'Field'
,ROW_NUMBER() over (order by table_NAMe) as  'ROWNMBR'
into #a
from INFORMATION_SCHEMA.COLUMNS
where (COLUMN_NAME like '%_SSNO_%' or COLUMN_NAME like'%_EMPR_NO_')
    and TABLE_SCHEMA='dbo'

declare @loopcntr int
declare @ROW int
declare @String nvarchar(1000)
set @loopcntr=(select count(*)  from #a)
set @ROW=1  

while (@ROW <= @loopcntr)
    begin
        select top 1 @String=a.Field 
        from #A a
        where a.ROWNMBR = @ROW
        execute sp_executesql @String
        set @ROW = @ROW + 1
    end 

0
SELECT @pk = @pk + 1

akan lebih baik:

SET @pk += @pk

Hindari menggunakan SELECT jika Anda tidak mereferensikan tabel hanya memberikan nilai.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.