【问题标题】:SQL Call Stored Procedure for each Row without using a cursor不使用游标的每一行的 SQL 调用存储过程
【发布时间】:2010-12-12 00:35:08
【问题描述】:

如何为表中的每一行调用一个存储过程,其中一行的列是sp的输入参数没有使用游标? p>

【问题讨论】:

  • 那么,比如你有一个Customer表,其中有一个customerId列,你想为表中的每一行调用一次SP,传入对应的customerId作为参数?
  • 能否详细说明为什么不能使用光标?
  • @Gary:也许我只是想传递客户名称,而不一定是 ID。但你是对的。
  • @Andomar:纯科学:-)

标签: sql sql-server stored-procedures database-cursor


【解决方案1】:

一般来说,我总是寻找基于集合的方法(有时会以更改架构为代价)。

但是,这个 sn-p 确实有它的位置..

-- Declare & init (2008 syntax)
DECLARE @CustomerID INT = 0

-- Iterate over all customers
WHILE (1 = 1) 
BEGIN  

  -- Get next customerId
  SELECT TOP 1 @CustomerID = CustomerID
  FROM Sales.Customer
  WHERE CustomerID > @CustomerId 
  ORDER BY CustomerID

  -- Exit loop if no more customers
  IF @@ROWCOUNT = 0 BREAK;

  -- call your sproc
  EXEC dbo.YOURSPROC @CustomerId

END

【讨论】:

  • 与已接受的答案一样,使用 WITH CATION:根据您的表和索引结构,它的性能可能非常差( O(n^2) ),因为您每次都必须订购和搜索您的表枚举。
  • 这似乎不起作用(对我来说,break 永远不会退出循环 - 工作已完成,但查询在循环中旋转)。初始化 id 并在 while 条件中检查 null 退出循环。
  • @@ROWCOUNT 只能读取一次。甚至 IF/PRINT 语句也会将其设置为 0。@@ROWCOUNT 的测试必须在选择之后“立即”完成。我会重新检查您的代码/环境。 technet.microsoft.com/en-us/library/ms187316.aspx
  • 虽然循环并不比游标好,但要小心,它们可能更糟:techrepublic.com/blog/the-enterprise-cloud/…
  • @Brennan Pope 对 CURSOR 使用 LOCAL 选项,它会在失败时被销毁。使用 LOCAL FAST_FORWARD 并且几乎有零个理由不使用 CURSOR 进行此类循环。它肯定会胜过这个 WHILE 循环。
【解决方案2】:

您可以执行以下操作:通过例如订购您的桌子。 CustomerID(使用 AdventureWorks Sales.Customer 示例表),并使用 WHILE 循环遍历这些客户:

-- define the last customer ID handled
DECLARE @LastCustomerID INT
SET @LastCustomerID = 0

-- define the customer ID to be handled now
DECLARE @CustomerIDToHandle INT

-- select the next customer to handle    
SELECT TOP 1 @CustomerIDToHandle = CustomerID
FROM Sales.Customer
WHERE CustomerID > @LastCustomerID
ORDER BY CustomerID

-- as long as we have customers......    
WHILE @CustomerIDToHandle IS NOT NULL
BEGIN
    -- call your sproc

    -- set the last customer handled to the one we just handled
    SET @LastCustomerID = @CustomerIDToHandle
    SET @CustomerIDToHandle = NULL

    -- select the next customer to handle    
    SELECT TOP 1 @CustomerIDToHandle = CustomerID
    FROM Sales.Customer
    WHERE CustomerID > @LastCustomerID
    ORDER BY CustomerID
END

只要您可以在某些列上定义某种ORDER BY,这应该适用于任何表。

【讨论】:

  • @Mitch:是的,是的——开销少了一点。但仍然 - 它并不是真正基于 SQL 的集合心态
  • 基于集合的实现是否可能?
  • 我不知道有什么方法可以实现这一点,真的 - 这是一项非常程序化的任务......
  • @marc_s 为集合中的每个项目执行一个函数/存储过程,这听起来像是基于集合的操作的基础。问题可能来自没有从他们每个人那里得到结果。请参阅大多数函数式编程语言中的“地图”。
  • 回复:丹尼尔。一个函数是的,一个存储过程没有。根据定义,存储过程可能会产生副作用,并且查询中不允许出现副作用。同样,函数式语言中的正确“映射”会禁止副作用。
【解决方案3】:
DECLARE @SQL varchar(max)=''

-- MyTable has fields fld1 & fld2

Select @SQL = @SQL + 'exec myproc ' + convert(varchar(10),fld1) + ',' 
                   + convert(varchar(10),fld2) + ';'
From MyTable

EXEC (@SQL)

好的,所以我永远不会将这样的代码投入生产,但它确实满足您的要求。

【讨论】:

  • 当程序返回一个应该设置行值的值时,如何做同样的事情? (使用 PROCEDURE 而不是函数,因为function creation is not allowed
  • @WeihuiGuo 因为使用字符串动态构建的代码非常容易失败,而且调试起来非常痛苦。你绝对不应该在没有机会成为生产环境的常规部分的情况下做这样的事情
  • 虽然我不打算使用它,但我喜欢这种方法,主要是因为我必须编写最少的代码,这适用于我的数据验证,我的 sp 中有规则可以验证某些记录一些表。从数据库中读取每一行并处理它是很乏味的。
  • 我想补充一下,您可能应该使用 PRINT 语句,而不仅仅是 EXEC。至少你会在你做之前看到你正在执行什么。
【解决方案4】:

我会使用已接受的答案,但另一种可能性是使用表变量来保存一组编号的值(在这种情况下只是表的 ID 字段)并通过带有 JOIN 的行号遍历这些值表来检索循环中的操作所需的任何内容。

DECLARE @RowCnt int; SET @RowCnt = 0 -- Loop Counter

-- Use a table variable to hold numbered rows containg MyTable's ID values
DECLARE @tblLoop TABLE (RowNum int IDENTITY (1, 1) Primary key NOT NULL,
     ID INT )
INSERT INTO @tblLoop (ID)  SELECT ID FROM MyTable

  -- Vars to use within the loop
  DECLARE @Code NVarChar(10); DECLARE @Name NVarChar(100);

WHILE @RowCnt < (SELECT COUNT(RowNum) FROM @tblLoop)
BEGIN
    SET @RowCnt = @RowCnt + 1
    -- Do what you want here with the data stored in tblLoop for the given RowNum
    SELECT @Code=Code, @Name=LongName
      FROM MyTable INNER JOIN @tblLoop tL on MyTable.ID=tL.ID
      WHERE tl.RowNum=@RowCnt
    PRINT Convert(NVarChar(10),@RowCnt) +' '+ @Code +' '+ @Name
END

【讨论】:

  • 这更好,因为它不假定您所追求的值是整数或可以进行合理比较。
【解决方案5】:

Marc 的回答很好(如果我能弄清楚如何做,我会对此发表评论!)
只是想我会指出更改循环可能会更好,因此 SELECT 只存在一次(在我需要这样做的真实情况下,SELECT 非常复杂,并且写两次是有风险的维护问题)。

-- define the last customer ID handled
DECLARE @LastCustomerID INT
SET @LastCustomerID = 0
-- define the customer ID to be handled now
DECLARE @CustomerIDToHandle INT
SET @CustomerIDToHandle = 1

-- as long as we have customers......    
WHILE @LastCustomerID <> @CustomerIDToHandle
BEGIN  
  SET @LastCustomerId = @CustomerIDToHandle
  -- select the next customer to handle    
  SELECT TOP 1 @CustomerIDToHandle = CustomerID
  FROM Sales.Customer
  WHERE CustomerID > @LastCustomerId 
  ORDER BY CustomerID

  IF @CustomerIDToHandle <> @LastCustomerID
  BEGIN
      -- call your sproc
  END

END

【讨论】:

  • APPLY 只能与函数一起使用...因此,如果您不想与函数有关,这种方法要好得多。
【解决方案6】:

如果你可以将存储过程变成一个返回表的函数,那么你可以使用cross-apply。

例如,假设您有一张客户表,并且您想计算他们的订单总和,您将创建一个函数,该函数接受 CustomerID 并返回总和。

你可以这样做:

SELECT CustomerID, CustomerSum.Total

FROM Customers
CROSS APPLY ufn_ComputeCustomerTotal(Customers.CustomerID) AS CustomerSum

函数的样子:

CREATE FUNCTION ComputeCustomerTotal
(
    @CustomerID INT
)
RETURNS TABLE
AS
RETURN
(
    SELECT SUM(CustomerOrder.Amount) AS Total FROM CustomerOrder WHERE CustomerID = @CustomerID
)

显然,上面的示例可以在单个查询中没有用户定义的函数的情况下完成。

缺点是函数非常有限 - 存储过程的许多功能在用户定义的函数中不可用,并且将存储过程转换为函数并不总是有效。

【讨论】:

  • 如果没有创建函数的写权限?
【解决方案7】:

对于 SQL Server 2005 及更高版本,您可以使用 CROSS APPLY 和表值函数来执行此操作。

为了清楚起见,我指的是那些可以将存储过程转换为表值函数的情况。

【讨论】:

  • 好主意,但是函数不能调用存储过程
【解决方案8】:

这是已经提供的答案的变体,但性能应该更好,因为它不需要 ORDER BY、COUNT 或 MIN/MAX。这种方法的唯一缺点是您必须创建一个临时表来保存所有 Id(假设您的 CustomerID 列表中存在空白)。

也就是说,我同意@Mark Powell 的观点,但总的来说,基于集合的方法应该会更好。

DECLARE @tmp table (Id INT IDENTITY(1,1) PRIMARY KEY NOT NULL, CustomerID INT NOT NULL)
DECLARE @CustomerId INT 
DECLARE @Id INT = 0

INSERT INTO @tmp SELECT CustomerId FROM Sales.Customer

WHILE (1=1)
BEGIN
    SELECT @CustomerId = CustomerId, @Id = Id
    FROM @tmp
    WHERE Id = @Id + 1

    IF @@rowcount = 0 BREAK;

    -- call your sproc
    EXEC dbo.YOURSPROC @CustomerId;
END

【讨论】:

    【解决方案9】:

    这是上述 n3rds 解决方案的变体。不需要使用 ORDER BY 进行排序,因为使用了 MIN()。

    请记住,CustomerID(或您用于进度的任何其他数字列)必须具有唯一约束。此外,为了使其尽可能快,必须对 CustomerID 进行索引。

    -- Declare & init
    DECLARE @CustomerID INT = (SELECT MIN(CustomerID) FROM Sales.Customer); -- First ID
    DECLARE @Data1 VARCHAR(200);
    DECLARE @Data2 VARCHAR(200);
    
    -- Iterate over all customers
    WHILE @CustomerID IS NOT NULL
    BEGIN  
    
      -- Get data based on ID
      SELECT @Data1 = Data1, @Data2 = Data2
        FROM Sales.Customer
        WHERE [ID] = @CustomerID ;
    
      -- call your sproc
      EXEC dbo.YOURSPROC @Data1, @Data2
    
      -- Get next customerId
      SELECT @CustomerID = MIN(CustomerID)
        FROM Sales.Customer
        WHERE CustomerID > @CustomerId 
    
    END
    

    我在一些需要查看的 varchars 上使用这种方法,首先将它们放在一个临时表中,给它们一个 ID。

    【讨论】:

      【解决方案10】:

      如果您不使用游标,我认为您必须在外部进行(获取表,然后为每个语句运行并每次调用 sp) 它与使用游标相同,但仅限于 SQL 之外。 为什么不使用光标?

      【讨论】:

        【解决方案11】:

        当它有很多行时,我通常会这样做:

        1. 使用 SQL Management Studio 选择数据集中的所有存储过程参数
        2. 右键->复制
        3. 粘贴到 Excel 中
        4. 在新的 excel 列中使用公式(例如 '="EXEC schema.mysproc @param=" & A2')创建单行 sql 语句。 (其中 A2 是包含参数的 excel 列)
        5. 将 excel 语句列表复制到 SQL Management Studio 中的新查询中并执行。
        6. 完成。

        (在较大的数据集上,我会使用上述解决方案之一)。

        【讨论】:

        • 在编程情况下不是很有用,这是一次性的。
        【解决方案12】:

        分隔符 //

        CREATE PROCEDURE setFakeUsers (OUT output VARCHAR(100))
        BEGIN
        
            -- define the last customer ID handled
            DECLARE LastGameID INT;
            DECLARE CurrentGameID INT;
            DECLARE userID INT;
        
            SET @LastGameID = 0; 
        
            -- define the customer ID to be handled now
        
            SET @userID = 0;
        
            -- select the next game to handle    
            SELECT @CurrentGameID = id
            FROM online_games
            WHERE id > LastGameID
            ORDER BY id LIMIT 0,1;
        
            -- as long as we have customers......    
            WHILE (@CurrentGameID IS NOT NULL) 
            DO
                -- call your sproc
        
                -- set the last customer handled to the one we just handled
                SET @LastGameID = @CurrentGameID;
                SET @CurrentGameID = NULL;
        
                -- select the random bot
                SELECT @userID = userID
                FROM users
                WHERE FIND_IN_SET('bot',baseInfo)
                ORDER BY RAND() LIMIT 0,1;
        
                -- update the game
                UPDATE online_games SET userID = @userID WHERE id = @CurrentGameID;
        
                -- select the next game to handle    
                SELECT @CurrentGameID = id
                 FROM online_games
                 WHERE id > LastGameID
                 ORDER BY id LIMIT 0,1;
            END WHILE;
            SET output = "done";
        END;//
        
        CALL setFakeUsers(@status);
        SELECT @status;
        

        【讨论】:

          【解决方案13】:

          一个更好的解决方案是

          1. 复制/过去的存储过程代码
          2. 将该代码与您要再次运行的表连接起来(对于每一行)

          这是你得到一个干净的表格格式的输出。而如果对每一行都运行 SP,那么每次迭代都会得到一个单独的查询结果,这很丑陋。

          【讨论】:

            【解决方案14】:

            如果订单很重要

            --declare counter
            DECLARE     @CurrentRowNum BIGINT = 0;
            --Iterate over all rows in [DataTable]
            WHILE (1 = 1)
                BEGIN
                    --Get next row by number of row
                    SELECT TOP 1 @CurrentRowNum = extendedData.RowNum
                                --here also you can store another values
                                --for following usage
                                --@MyVariable = extendedData.Value
                    FROM    (
                                SELECT 
                                    data.*
                                    ,ROW_NUMBER() OVER(ORDER BY (SELECT 0)) RowNum
                                FROM [DataTable] data
                            ) extendedData
                    WHERE extendedData.RowNum > @CurrentRowNum
                    ORDER BY extendedData.RowNum
            
                    --Exit loop if no more rows
                    IF @@ROWCOUNT = 0 BREAK;
            
                    --call your sproc
                    --EXEC dbo.YOURSPROC @MyVariable
                END
            

            【讨论】:

              【解决方案15】:

              我有一些生产代码,一次只能处理 20 名员工,下面是代码的框架。我只是复制了生产代码并删除了下面的内容。

              ALTER procedure GetEmployees
                  @ClientId varchar(50)
              as
              begin
                  declare @EEList table (employeeId varchar(50));
                  declare @EE20 table (employeeId varchar(50));
              
                  insert into @EEList select employeeId from Employee where (ClientId = @ClientId);
              
                  -- Do 20 at a time
                  while (select count(*) from @EEList) > 0
                  BEGIN
                    insert into @EE20 select top 20 employeeId from @EEList;
              
                    -- Call sp here
              
                    delete @EEList where employeeId in (select employeeId from @EE20)
                    delete @EE20;
                  END;
              
                RETURN
              end
              

              【讨论】:

                【解决方案16】:

                我遇到过需要对结果集(表)执行一系列操作的情况。这些操作都是集合操作,所以这不是问题,但是...... 我需要在多个地方这样做。因此,将相关部分放在一个表类型中,然后使用每个结果集填充一个表变量,这样我就可以调用 sp 并在每次需要时重复这些操作。

                虽然这并没有解决他提出的确切问题,但它确实解决了如何在不使用游标的情况下对表的所有行执行操作。

                @Johannes 没有透露他的动机,所以这可能对他有帮助,也可能没有帮助。

                我的研究使我找到了这篇写得很好的文章,它是我解决方案的基础 https://codingsight.com/passing-data-table-as-parameter-to-stored-procedures/

                这里是设置

                    drop type if exists cpRootMapType 
                go 
                
                create type cpRootMapType as Table(
                    RootId1 int 
                    , RootId2 int
                )
                
                go 
                drop procedure if exists spMapRoot2toRoot1
                go 
                create procedure spMapRoot2toRoot1
                (
                @map cpRootMapType Readonly
                )
                as
                
                update linkTable set root = root1  
                from linktable  lt 
                join @map m on lt.root = root2
                
                update comments set root = root1 
                from comments c 
                join @map m on c.root = root2
                
                --  ever growing list of places this map would need to be applied....
                --  now consolidated into one place 
                

                这里是实现

                ... populate #matches
                
                declare @map cpRootMapType 
                insert @map select rootid1, rootid2 from #matches
                exec spMapRoot2toRoot1 @map 
                

                【讨论】:

                  【解决方案17】:

                  我喜欢做类似的事情(尽管它仍然非常类似于使用光标)

                  [代码]

                  -- Table variable to hold list of things that need looping
                  DECLARE @holdStuff TABLE ( 
                      id INT IDENTITY(1,1) , 
                      isIterated BIT DEFAULT 0 , 
                      someInt INT ,
                      someBool BIT ,
                      otherStuff VARCHAR(200)
                  )
                  
                  -- Populate your @holdStuff with... stuff
                  INSERT INTO @holdStuff ( 
                      someInt ,
                      someBool ,
                      otherStuff
                  )
                  SELECT  
                      1 , -- someInt - int
                      1 , -- someBool - bit
                      'I like turtles'  -- otherStuff - varchar(200)
                  UNION ALL
                  SELECT  
                      42 , -- someInt - int
                      0 , -- someBool - bit
                      'something profound'  -- otherStuff - varchar(200)
                  
                  -- Loop tracking variables
                  DECLARE @tableCount INT
                  SET     @tableCount = (SELECT COUNT(1) FROM [@holdStuff])
                  
                  DECLARE @loopCount INT
                  SET     @loopCount = 1
                  
                  -- While loop variables
                  DECLARE @id INT
                  DECLARE @someInt INT
                  DECLARE @someBool BIT
                  DECLARE @otherStuff VARCHAR(200)
                  
                  -- Loop through item in @holdStuff
                  WHILE (@loopCount <= @tableCount)
                      BEGIN
                  
                          -- Increment the loopCount variable
                          SET @loopCount = @loopCount + 1
                  
                          -- Grab the top unprocessed record
                          SELECT  TOP 1 
                              @id = id ,
                              @someInt = someInt ,
                              @someBool = someBool ,
                              @otherStuff = otherStuff
                          FROM    @holdStuff
                          WHERE   isIterated = 0
                  
                          -- Update the grabbed record to be iterated
                          UPDATE  @holdAccounts
                          SET     isIterated = 1
                          WHERE   id = @id
                  
                          -- Execute your stored procedure
                          EXEC someRandomSp @someInt, @someBool, @otherStuff
                  
                      END
                  

                  [/代码]

                  请注意,您不需要 临时/变量表上的标识或 isIterated 列,我只是更喜欢这样做,这样我就不必从中删除顶部记录我遍历循环时的集合。

                  【讨论】:

                    猜你喜欢
                    • 2017-07-17
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2018-12-08
                    • 1970-01-01
                    相关资源
                    最近更新 更多