커서를 사용하지 않고 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
커서를 피하려는 이유는 밝히지 않았습니다. 커서가 반복하는 가장 간단한 방법 일 수 있습니다. 커서가 '나쁘다'는 말을 들었을 수도 있지만 실제로는 세트 기반 조작에 비해 나쁜 테이블에 대한 반복입니다. 반복을 피할 수 없다면 커서가 가장 좋습니다. 잠금은 커서의 또 다른 문제점이지만 테이블 변수를 사용할 때는 관련이 없습니다.
JacquesB

1
커서를 사용하는 것이 유일한 옵션은 아니지만, 행 단위 접근 방식을 피할 방법이 없다면 최선의 선택이 될 것입니다. CURSOR는 자신의 바보 같은 WHILE 루프를 수행하는 것보다 더 효율적이고 오류가 적은 내장 구조입니다. 대부분의 경우 STATIC기본 테이블의 지속적인 재확인 및 기본적으로 잠금을 제거하고 CURSOR가 악하다고 잘못 인식하는 옵션을 사용해야합니다. @JacquesB 매우 가까이 : 결과 행이 여전히 존재하는지 확인하고 다시 잠그는 것이 문제입니다. 그리고 STATIC보통 그 수정 :-).
Solomon Rutzky

답변:


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 답변 참조).


"선택 상위 1 @Id = ID는 ATable에서 선택"은 "선택 상위 1 @Id = ID는 ATable에서 처리 된 = 0"
Amzath

10
SQL Server를 사용하는 경우 위의 작은 조정에 대해서는 아래 Josef의 답변을 참조하십시오.
Polshgiant

3
커서를 사용하는 것보다 이것이 왜 더 좋은지 설명 할 수 있습니까?
marco-fiset

5
이것 하나를 공감하십시오. 왜 커서 사용을 피해야합니까? 그는 전통적인 테이블이 아닌 테이블 변수 를 반복하는 것에 대해 이야기하고 있습니다. 나는 커서의 정상적인 단점이 여기에 적용되지 않는다고 생각합니다. 행 단위 처리가 실제로 필요한 경우 (그리고 먼저이를 확인해야 함) 커서를 사용하는 것이 여기에 설명 된 것보다 훨씬 나은 솔루션입니다.
peterh 2016 년

@ peterh 당신이 맞습니다. 실제로 STATIC결과 세트를 임시 테이블에 복사하는 옵션을 사용하여 이러한 "정상적인 단점"을 피할 수 있으므로 더 이상 기본 테이블을 잠 그거나 다시 확인하지 않습니다.
Solomon Rutzky

132

SQL Server (2008 이상)를 사용하는 경우 간단한 참고 사항은 다음과 같습니다.

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

더 나은 제공

While EXISTS(SELECT * From #Temp)

개수는 테이블의 모든 단일 행 EXISTS을 터치해야하며 첫 번째 행만 터치하면됩니다.


9
이것은 답변이 아니라 Martynw 답변에 대한 의견 / 향상입니다.
Hammad Khan

7
이 메모의 내용은 주석보다 더 나은 서식 기능을 강요하므로 답변에 추가하는 것이 좋습니다.
Custodio

2
이후 버전의 SQL에서 쿼리 최적화 프로그램은 첫 번째 내용을 작성할 때 실제로 두 번째 내용을 의미하고 테이블 스캔을 피하도록 최적화한다는 것을 알기에 충분히 영리합니다.
댄 데프

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

[편집] 처음 질문을 읽을 때 "variable"이라는 단어를 건너 뛰었으므로 업데이트 된 응답은 다음과 같습니다.


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
... 처리하는 동안 사용되는 테이블을 잠그지 않고 ... 이것은 커서 의 장점 중 하나이기 때문에 :)
leoinfo

3
테이블? VARIABLE 테이블입니다. 동시 액세스가 불가능합니다.
DenNukem

DenNukem, 당신 말이 맞습니다. 저는 당시 질문을 읽을 때 "가변"이라는 단어를 "건너"습니다 "라고 생각합니다. 저는 초기 답변에 메모를 추가 할 것입니다.
leoinfo

DenNukem과 Shawn에 동의해야합니다. 왜, 왜, 왜 커서 사용을 피하기 위해이 길이로 가나 요? 다시 : 그는 전통적인 테이블이 아닌 테이블 변수를 반복하려고합니다 !!!
peterh

10

FAST_FORWARD 커서를 작성하여 행 단위로 이동하는 것 외에는 선택의 여지가 없습니다. while 루프를 구축하는 것만 큼 빠르며 장기적으로 유지 관리가 훨씬 쉽습니다.

FAST_FORWARD 성능 최적화가 활성화 된 FORWARD_ONLY, READ_ONLY 커서를 지정합니다. SCROLL 또는 FOR_UPDATE도 지정된 경우 FAST_FORWARD를 지정할 수 없습니다.


2
네! 다른 곳에서 언급했듯이 테이블 변수 를 반복 할 때 커서를 사용 하지 않는 이유에 대한 인수는 아직 보지 못했습니다 . 커서 훌륭한 솔루션입니다. (upvote)FAST_FORWARD
peterh 2016 년

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

커서를 사용하여이를 수행 할 수 있습니다.

함수 작성 [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

이 방법은 하나의 변수 만 필요하며 @databases에서 행을 삭제하지 않습니다. 여기에 많은 답변이 있다는 것을 알고 있지만 다음과 같은 ID를 얻는 데 MIN을 사용하는 답변이 없습니다.

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함수를 사용하는 솔루션 입니다. 커서 나 임시 테이블이 필요하지 않으며 테이블의 다음 행을 얻기 위해 하나의 쿼리 만 작성하면됩니다 @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)에 대한 색인을 작성하는 것입니다.

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.