カーソルを使用せずにTSQLのテーブル変数をループする方法はありますか?


243

次の単純なテーブル変数があるとします。

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

行を繰り返し処理したい場合、カーソルを宣言して使用することが唯一のオプションですか?別の方法はありますか?


3
上記のアプローチで見られる問題はわかりませんが、このことができます参照してください。.. databasejournal.com/features/mssql/article.php/3111031
Gishu

5
行を反復処理する理由を教えてください。反復を必要としない他のソリューションが存在する可能性があります(ほとんどの場合、これは大幅に高速です)
Pop Catalin

popに同意...状況によってはカーソルが不要な場合があります。ただし、必要に応じてカーソルを使用しても問題はありません
Shawn

3
カーソルを避けたい理由を述べていない。カーソルは反復する最も簡単な方法かもしれないことに注意してください。カーソルが「悪い」と聞いたことがあるかもしれませんが、セットベースの操作と比較して悪いのは、実際にはテーブルの反復です。イテレーションを回避できない場合は、カーソルが最善の方法である可能性があります。ロックはカーソルのもう1つの問題ですが、テーブル変数を使用する場合は関係ありません。
JacquesB 2014

1
カーソルの使用は唯一のオプションではありませんが、行ごとのアプローチを回避する方法がない場合は、それが最良のオプションになります。CURSORは、独自の愚かなWHILEループを実行するよりも効率的でエラーが発生しにくい組み込み構造です。ほとんどのSTATIC場合、デフォルトで存在するベーステーブルの定期的な再チェックとロックを削除するオプションを使用する必要があるだけで、ほとんどの人は誤ってカーソルが悪いと信じています。@JacquesBが非常に近い:結果行がまだ存在するかどうかを確認するための再チェック+ロックが問題です。そしてSTATIC通常それを修正します:-)。
ソロモンルツキー2016

回答:


376

まず最初に、各行を反復処理する必要があることを確実に確認する必要があります。考えられるすべてのケースで、セットベースの操作はより高速に実行され、通常はより単純なコードを使用します。

データによっては、SELECT以下に示すように、ステートメントだけを使用してループできる場合があります。

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

別の方法は、一時テーブルを使用することです:

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

選択するオプションは、実際にはデータの構造と量によって異なります。

注: SQL Serverを使用している場合は、次の方法でサービスを提供することをお勧めします。

WHILE EXISTS(SELECT * FROM #Temp)

を使用COUNTすると、テーブルのすべての行にEXISTS触れる必要があり、最初の行に触れるだけで済みます(以下のJosefの回答を参照)。


「Select Top 1 @Id = Id From ATable」は「Select Top 1 @Id = Id From ATable Where Where Processed = 0 "
Amzath

10
SQL Serverを使用している場合、上記の小さな調整については、以下のJosefの回答を参照してください。
Polshgiant

3
これがカーソルを使用するよりも優れている理由を説明できますか?
marco-fiset 2012年

5
これに反対票を与えました。なぜカーソルの使用を避けなければならないのですか?彼は、従来のテーブルではなく、テーブル変数の反復について話しています。カーソルの通常の欠点がここに当てはまるとは思いません。行ごとの処理が本当に必要な場合(そして指摘したように、最初にそれについて確実にすべきである場合)、ここで説明するものよりもカーソルを使用する方がはるかに優れたソリューションです。
peterh 2013年

@peterhあなたは正しいです。そして実際、STATIC結果セットを一時テーブルにコピーするオプションを使用することで、通常これらの「通常のマイナス面」を回避でき、したがってベーステーブルをロックしたり再チェックしたりしなくなります:-)。
ソロモンルツキー2016

132

SQL Server(2008以降)を使用している場合、簡単なメモを以下に示します。

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

添えたほうがいい

While EXISTS(SELECT * From #Temp)

カウントはテーブル内のすべての行にEXISTS触れる必要があり、最初のものだけに触れる必要があります。


9
これは回答ではなく、Martynw回答に関するコメント/拡張です。
Hammad Khan

7
このメモの内容は、コメントよりも優れたフォーマット機能を強制します。回答に追加することをお勧めします。
Custodio 2013

2
SQLの新しいバージョンでは、クエリオプティマイザーは、最初のものを書くとき、実際には2番目のものを意味し、それを最適化してテーブルスキャンを回避することを知るのに十分賢いです。
Dan Def

39

これが私のやり方です:

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

カーソル、一時テーブル、追加の列はありません。ほとんどの主キーと同様に、USERID列は一意の整数である必要があります。


26

このように一時テーブルを定義します-

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

次にこれを行います-

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

ここに私がそれをする方法があります:

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

[編集]質問を初めて読んだとき、おそらく「変数」という単語をスキップしたので、ここに更新された応答があります...


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
つまり、基本的にカーソルを実行しているが、カーソルのすべての利点がない
Shawn

1
...これはカーソルの利点の1つであるため、処理中に使用されるテーブルをロックしません...)
leoinfo

3
テーブル?これはVARIABLEテーブルです-同時アクセスはできません。
DenNukem 2010年

DenNukem、その通りです。そのときに質問を読んだとき、「変数」という単語を「スキップ」したと思います...私の最初の応答にいくつかのメモを追加します
leoinfo

DenNukemとShawnに同意する必要があります。なぜ、なぜ、なぜカーソルを使用しないようにこれらの長さにしますか?繰り返しますが、彼は従来のテーブルではなく、テーブル変数を反復処理したいと考えています。
peterh 2013年

10

行ごとに移動してFAST_FORWARDカーソルを作成する以外に選択肢がない場合。これは、whileループを構築するのと同じくらい高速で、長期にわたって維持するのがはるかに簡単になります。

FAST_FORWARDパフォーマンスの最適化を有効にして、FORWARD_ONLY、READ_ONLYカーソルを指定します。SCROLLまたはFOR_UPDATEも指定されている場合、FAST_FORWARDは指定できません。


2
うん!他の場所でコメントしたように、テーブル変数を反復処理する場合にカーソルを使用しない理由については、まだ引数がありません。カーソルは細かいソリューションです。(FAST_FORWARD
賛成

5

スキーマを変更したり、一時テーブルを使用したりする必要のない別のアプローチ:

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

whileループを使用できます。

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

    Delete From #TempTable Where x = x
End

4

これは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

軽量、あなたは整数がある場合、余分なテーブルを作成することなく、IDテーブルの上に

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

あなたが恐ろしいものを使うことに訴える必要がある理由は本当にわかりませんcursor。あなたは、SQL Serverのバージョン2005/2008使用している場合しかし、ここでもう一つの選択肢である
使用再帰を

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

セットベースのソリューションを提供します。

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

これは、どのループ手法よりもはるかに高速で、記述と保守が簡単です。


2

一意のIDがある場合は、オフセットフェッチを使用することをお勧めします。

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

この方法では、テーブルにフィールドを追加したり、ウィンドウ関数を使用したりする必要はありません。


2

これを行うためにカーソルを使用することが可能です:

create function [dbo] .f_teste_loopは、@ tabelaテーブル(cod int、nome varchar(10))を開始として返します

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;

終わり

最初にプロシージャ[dbo]。[sp_teste_loop]を作成します

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;

終わり


1
元の質問は「カーソルを使用せずに」ではなかったのですか?
フェルナンドゴンザレスサンチェス

1

セットベースの操作は通常、パフォーマンスが向上するという以前の投稿に同意しますが、行を反復処理する必要がある場合は、次の方法を使用します。

  1. テーブル変数に新しいフィールドを追加します(データ型ビット、デフォルトは0)
  2. データを挿入する
  3. fUsed = 0である上位1行を選択します (注:fUsedはステップ1のフィールドの名前です)
  4. 必要な処理をすべて実行します
  5. レコードにfUsed = 1を設定して、テーブル変数のレコードを更新します
  6. テーブルから次の未使用のレコードを選択し、プロセスを繰り返します

    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

ステップ1:以下のselectステートメントは、各レコードに一意の行番号を持つ一時テーブルを作成します。

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

ステップ2:必要な変数を宣言する

DECLARE @ROWNUMBER INT
DECLARE @ename varchar(100)

手順3:一時テーブルから合計行数を取得する

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

ステップ4:一時的に作成された一意の行番号に基づいて一時テーブルをループする

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

このアプローチでは1つの変数のみが必要で、@ databasesから行は削除されません。ここにはたくさんの答えがあることは知っていますが、MINを使用して次のIDを取得するような答えは見当たりません。

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

これが、無限ループ、BREAKステートメント、@@ROWCOUNT関数を使用する私の解決策です。カーソルや一時テーブルは必要ありません。テーブルの次の行を取得するために必要なクエリは1つだけです@databases

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

@ControlFreakが私より先にこのアプローチを推奨していることに気づきました。コメントとより詳細な例を追加しただけです。
マスドットネット

0

これは、2008 R2を使用しているコードです。私が使用しているこのコードは、キーフィールド(SSNO&EMPR_NO)にインデックスを構築するためのものですnすべての物語

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

方が良いだろう:

SET @pk += @pk

テーブルを参照しておらず、値を割り当てているだけの場合は、SELECTを使用しないでください。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.