有没有一种方法可以在不使用游标的情况下遍历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

同意弹出...视情况而定,可能不需要光标。但如果需要,使用游标也没有问题
Shawn

3
您没有说明为什么要避免使用游标。请注意,游标可能是最简单的迭代方法。您可能已经听说过游标是“错误的”,但是与基于集合的操作相比,实际上是对表的迭代是错误的。如果无法避免迭代,则游标可能是最好的方法。锁定是游标的另一个问题,但是在使用表变量时这并不重要。
JacquesB 2014年

1
使用游标不是唯一的选择,但是如果您无法避免逐行方法,那么它将是您的最佳选择。CURSOR是一个内置结构,比您自己进行的愚蠢的WHILE循环更有效且更不易出错。在大多数情况下,您只需要使用该STATIC选项即可删除对基表的不断重新检查以及默认情况下存在的锁定,并导致大多数人错误地认为CURSOR是邪恶的。@JacquesB非常接近:重新检查结果行是否仍然存在+锁定是问题所在。而STATIC通常修复了:-)。
所罗门·鲁茨基

Answers:


375

首先,您应该绝对确定需要遍历每一行-基于集的操作在我能想到的每种情况下都将更快地执行,并且通常将使用更简单的代码。

根据您的数据,可能仅使用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唯一需要触摸第一行(请参阅下面的约瑟夫的答案)。


“选择前1个@Id =来自一个表的ID”应为“选择前1个@Id =来自一个表的ID,其中已处理= 0”
Amzath 2011年

10
如果使用SQL Server,请参阅下面的Josef回答以对上述内容进行一些细微调整。
Polshgiant

3
您能解释为什么这比使用游标更好吗?
marco-fiset 2012年

5
给这个投下反对票。他为什么要避免使用光标?他在谈论的是遍历表变量,而不是传统表。我不认为游标的正常缺点在这里适用。如果确实需要逐行处理(并且您指出他应该首先确定这一点),那么使用游标比您在此描述的解决方案要好得多。
peterh 2013年

@peterh你是正确的。实际上,通常可以通过使用将STATIC结果集复制到临时表的选项来避免那些“正常的缺点” ,因此您不再需要锁定或重新检查基表:-)。
所罗门·鲁茨基

132

简要说明一下,如果您使用的是SQL Server(2008及更高版本),则示例包含:

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

最好搭配

While EXISTS(SELECT * From #Temp)

Count必须触摸表中的每一行,EXISTS唯一需要触摸的第一行。


9
这不是答案,而是对Martynw答案的评论/增强。
哈马德·汗

7
本注释的内容比注释提供了更好的格式化功能,我建议在“答案”后面附加。
Custodio

2
在更高版本的SQL中,查询优化器足够聪明,以至于您在编写第一件事时实际上是在指第二件事并对其进行优化以避免表扫描。
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
...而无需锁定在处理过程中使用的表...因为这是游标的好处之一:)
leoinfo

3
桌子?这是一个表变量-没有并发访问的可能。
DenNukem 2010年

DenNukem,您是对的,我认为我当时阅读问题时会“跳过”“变量”一词...我将在我的最初回复中添加一些笔记
leoinfo 2010年

我必须同意DenNukem和Shawn的观点。为什么,为什么,为什么要使用这些长度以避免使用光标?再次:他想遍历表变量,而不是传统表!
peterh 2013年

10

如果您别无选择,只能逐行创建FAST_FORWARD游标。它就像建立一个while循环一样快,并且从长远来看更容易维护。

FAST_FORWARD指定启用了性能优化的FORWARD_ONLY,READ_ONLY游标。如果还指定了SCROLL或FOR_UPDATE,则不能指定FAST_FORWARD。


2
是的 正如我在其他地方评论过的那样,我还没有看到任何有关为什么要在表变量上进行迭代时使用游标的争论。甲光标是一个很好的解决方案。(upvote)FAST_FORWARD
peterh 2013年

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. 选择顶部1行,其中fUsed = 0 (注意: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中删除任何行。我知道这里有很多答案,但是我看不到使用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函数。不需要游标或临时表,我只需要编写一个查询即可获取@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在我之前推荐了这种方法。我只是添加了注释和更详细的示例。
Mass Dot Net

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。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.