【问题标题】:Is there a way to loop through a table variable in TSQL without using a cursor?有没有办法在不使用游标的情况下循环遍历 SQL 中的表变量?
【发布时间】:2010-09-08 21:14:00
【问题描述】:

假设我有以下简单的表变量:

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

如果我想遍历行,那么声明和使用游标是我唯一的选择吗?还有其他方法吗?

【问题讨论】:

  • 虽然我不确定您在上述方法中看到的问题;看看这是否有帮助.. databasejournal.com/features/mssql/article.php/3111031
  • 您能否向我们提供您想要迭代行的原因,可能存在其他不需要迭代的解决方案(并且在大多数情况下速度更快)
  • 同意pop...根据情况可能不需要光标。但是如果需要,使用游标没有问题
  • 您没有说明为什么要避免使用光标。请注意,游标可能是最简单的迭代方式。您可能听说游标是“坏的”,但与基于集合的操作相比,它确实是对表的迭代。如果你不能避免迭代,游标可能是最好的方法。锁定是游标的另一个问题,但这与使用表变量时无关。
  • 使用游标不是您的only选项,但如果您无法避免逐行方法,那么它将是您的最佳选择。 CURSOR 是一种内置结构,它比执行您自己的愚蠢 WHILE 循环更高效且不易出错。大多数情况下,您只需要使用STATIC 选项来消除对基表的不断重新检查和默认情况下存在的锁定,从而导致大多数人错误地认为 CURSOR 是邪恶的。 @JacquesB 非常接近:重新检查结果行是否仍然存在+锁定是问题所在。 STATIC 通常会解决这个问题:-)。

标签: sql-server tsql loops


【解决方案1】:

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

根据您的数据,可能只使用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's answer)。

【讨论】:

  • “Select Top 1 @Id = Id From ATable”应该是“Select Top 1 @Id = Id From ATable Where Processed = 0”
  • 如果使用 SQL Server,请参阅下面 Josef 的回答,对上述内容进行小幅调整。
  • 你能解释一下为什么这比使用光标更好吗?
  • 投了反对票。他为什么要避免使用游标?他说的是迭代表变量,而不是传统的表。我不相信游标的正常缺点适用于此。如果确实需要逐行处理(正如您指出的那样,他应该首先确定这一点),那么使用游标是比您在此处描述的解决方案更好的解决方案。
  • @peterh 你是对的。事实上,您通常可以通过使用将结果集复制到临时表的STATIC 选项来避免那些“正常的缺点”,因此您不再锁定或重新检查基表:-)。
【解决方案2】:

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

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

搭配

会更好
While EXISTS(SELECT * From #Temp)

Count 必须接触表格中的每一行,EXISTS 只需要接触第一行。

【讨论】:

  • 这不是答案,而是对 Martynw 答案的评论/增强。
  • 这篇笔记的内容比评论具有更好的格式化功能,我建议在答案处附加。
  • 在更高版本的 SQL 中,查询优化器足够聪明,知道当您编写第一件事时,您实际上是指第二件事,并对其进行优化以避免表扫描。
【解决方案3】:

这就是我的做法:

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 列必须是唯一的整数,就像大多数主键一样。

【讨论】:

  • 很好的解决方案,对我来说更好的方法是使用Min(Id),因为我的Id 列有一个ascending 聚集索引,所以它是查询的额外操作来反转排序。
【解决方案4】:

像这样定义你的临时表 -

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

【讨论】:

    【解决方案5】:

    我会这样做:

    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
    

    【讨论】:

    • 所以基本上你在做一个游标,但没有游标的所有好处
    • ...不锁定处理时使用的表...因为这是游标的好处之一:)
    • 表格?这是一个表 VARIABLE - 不可能有并发访问。
    • DenNukem,你说得对,我想我当时在阅读问题时“跳过”了“变量”这个词......我会在最初的回复中添加一些注释
    • 我必须同意 DenNukem 和 Shawn 的观点。为什么,为什么,你为什么要花这么多时间来避免使用游标?再说一遍:他想迭代一个表变量,而不是传统的表!!!
    【解决方案6】:

    如果您别无选择,只能逐行创建 FAST_FORWARD 游标。它将与建立一个while循环一样快,并且更容易长期维护。

    FAST_FORWARD 指定启用了性能优化的 FORWARD_ONLY、READ_ONLY 游标。如果同时指定了 SCROLL 或 FOR_UPDATE,则无法指定 FAST_FORWARD。

    【讨论】:

    • 是的!正如我在其他地方评论的那样,我还没有看到任何关于为什么 NOT 在迭代 table 变量 时使用游标的论据。 FAST_FORWARDcursor 是一个很好的解决方案。 (点赞)
    【解决方案7】:

    这适用于 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 
    

    【讨论】:

      【解决方案8】:

      你可以使用while循环:

      While (Select Count(*) From #TempTable) > 0
      Begin
          Insert Into @Databases...
      
          Delete From #TempTable Where x = x
      End
      

      【讨论】:

        【解决方案9】:

        另一种无需更改架构或使用临时表的方法:

        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
        

        【讨论】:

          【解决方案10】:

          轻量级,不用做额外的表,如果你的表上有一个整数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
          

          【讨论】:

            【解决方案11】:

            我真的不明白为什么你需要使用可怕的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
            

            【讨论】:

              【解决方案12】:
              -- [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
              

              【讨论】:

                【解决方案13】:

                可以使用光标来执行此操作:

                创建函数 [dbo].f_teste_loop 返回@tabela 表 ( 鳕鱼诠释, 名称 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;
                

                结束

                【讨论】:

                • 原来的问题不是“不使用光标”吗?
                【解决方案14】:

                我将提供基于集合的解决方案。

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

                这比任何循环技术都快得多,并且更容易编写和维护。

                【讨论】:

                  【解决方案15】:

                  如果您有唯一的 ID,我更喜欢使用 Offset Fetch,您可以按以下方式对表格进行排序:

                  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
                  

                  这样我就不需要在表格中添加字段或使用窗口函数了。

                  【讨论】:

                    【解决方案16】:

                    我同意上一篇文章,即基于集合的操作通常会执行得更好,但如果您确实需要遍历行,我会采取以下方法:

                    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
                      

                    【讨论】:

                      【解决方案17】:

                      Step1:下面的select语句为每条记录创建一个具有唯一行号的临时表。

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

                      Step2:声明需要的变量

                      DECLARE @ROWNUMBER INT
                      DECLARE @ename varchar(100)
                      

                      Step3:从临时表中获取总行数

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

                      Step4:根据在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
                      

                      【讨论】:

                        【解决方案18】:

                        这种方法只需要一个变量并且不会从@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
                        

                        【讨论】:

                          【解决方案19】:

                          这是我的解决方案,它利用了无限循环、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 在我之前推荐了这种方法;我只是添加了 cmets 和一个更详细的示例。
                          【解决方案20】:

                          这是我使用 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 
                          

                          【讨论】:

                            【解决方案21】:
                            SELECT @pk = @pk + 1
                            

                            会更好:

                            SET @pk += @pk
                            

                            如果您不引用表而只是分配值,请避免使用 SELECT。

                            【讨论】:

                              猜你喜欢
                              • 2016-05-12
                              • 2013-08-02
                              • 2013-11-22
                              • 1970-01-01
                              • 1970-01-01
                              • 1970-01-01
                              • 2013-02-27
                              • 2013-05-31
                              • 2020-12-04
                              相关资源
                              最近更新 更多