不使用游标的每行的SQL调用存储过程


163

一个人如何能为一个表中的每一行调用一个存储过程,其中一行的列是sp的输入参数,而无需使用游标?


3
因此,例如,您有一个带有customerId列的Customer表,并且您想为表中的每一行调用一次SP,并将相应的customerId作为参数传递?
加里·麦吉尔

2
您能否详细说明为什么不能使用光标?
2009年

@加里:也许我只想传递客户名称,不一定是ID。但是你是对的。
约翰内斯·鲁道夫

2
@Andomar:纯粹是科学的:-)
Johannes Rudolph

1
这个问题也极大地困扰了我。
丹尼尔(Daniel)2010年

Answers:


200

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

但是,此片段确实有它的位置。

-- 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

21
就像接受答案一样:USE WITH CATION:根据表和索引结构的不同,它的性能可能非常差(O(n ^ 2)),因为每次枚举时都必须对表进行排序和搜索。
csauve 2012年

3
这似乎不起作用(break对我而言永远不会退出循环-工作已完成,但查询在循环中旋转)。初始化id并在while条件中检查null会退出循环。
dudeNumber4 2013年

8
@@ ROWCOUNT只能读取一次。甚至IF / PRINT语句也将其设置为0。@@ ROWCOUNT的测试必须在选择之后“立即”完成。我会重新检查您的代码/环境。technet.microsoft.com/zh-CN/library/ms187316.aspx
Mark Powell,

3
尽管循环并不比游标好,但要小心,它们甚至会更糟:techrepublic.com/blog/the-enterprise-cloud/…–
Jaime

1
@Brennan Pope对CURSOR使用LOCAL选项,如果失败,它将被销毁。使用LOCAL FAST_FORWARD,几乎没有零个理由不对此类循环使用CURSOR。它肯定会胜过这个WHILE循环。
马丁

39

您可以执行以下操作:通过例如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

6
基于集合的实现甚至可能吗?
约翰内斯·鲁道夫

我不知道有什么方法可以实现这一目标,实际上
从头

2
@marc_s为集合中的每个项目执行一个函数/存储过程,这听起来像基于集合的操作的基础。问题可能是由于每个人都没有得到结果。请参见大多数功能性编程语言中的“地图”。
丹尼尔(Daniel)2010年

4
回复:丹尼尔。函数是,存储过程是。根据定义,存储过程可能会产生副作用,并且查询中不允许出现副作用。类似地,使用功能语言编写的适当“地图”禁止出现副作用。
csauve

28
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而不是函数)
user2284570 2015年

@WeihuiGuo,因为使用字符串动态构建的代码很容易出错,并且调试起来非常痛苦。您绝对不应在没有机会成为生产环境的常规部分的情况下做任何这样的事情
玛丽

11

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只能与函数一起使用...因此,如果您不想与函数有关,则此方法要好得多。
Artur 2014年

您需要50次评论才能发表评论。继续回答这些问题,您将获得更多的力量:D stackoverflow.com/help/privileges
SvendK

我认为这应该是简单明了的答案。非常感谢你!
炸弹般的效果,

7

如果您可以将存储过程变成返回表的函数,则可以使用交叉应用。

例如,假设您有一个客户表,并且要计算他们的订单总和,您将创建一个使用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
)

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

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


如果没有创建功能的写权限?
user2284570 2015年

7

我会使用可接受的答案,但是另一种可能性是使用表变量来保存一组编号的值(在这种情况下,仅是表的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

这样做会更好,因为它不会假定您所追求的值是整数或可以合理地进行比较。
菲利普2015年

正是我想要的。
Raithlin


3

这是上述n3rds解决方案的一种变体。由于使用了MIN(),因此无需使用ORDER BY进行排序。

请记住,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

我在需要检查的一些varchar上使用了这种方法,方法是先将它们放在临时表中,以为其指定ID。


2

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


2

这是已经提供的答案的一种变体,但是应该更好地执行,因为它不需要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

1

当行数很多时,我通常以这种方式进行操作:

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

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


4
在编程情况下不是很有用,这是一次性的。
沃伦·P

1

分隔符//

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;

1

一个更好的解决方案是

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

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


0

如果订单很重要

--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

0

我有一些生产代码,一次只能处理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

-1

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

[码]

-- 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,我只是喜欢做这种方式,所以我没有穿过绳套以删除集合上面记录着我迭代。

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.