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

如何调用一个表中的每一行的存储过程,其中一行的列是input参数的SP 没有使用光标?

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

但是,这个片段确实有它的地方..

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

你可以这样做:按CustomerID(使用AdventureWorks Sales.Customer示例表)对表格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

 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) 

好吧,我绝不会把这些代码投入生产,但它确实满足你的要求。

马克的回答是好的(如果我能解决问题的话,我会评论一下)
只是想我会指出,改变循环可能会更好,所以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 

如果可以将存储过程转换为返回表的函数,那么可以使用交叉应用。

例如,假设你有一个客户表,并且你想要计算他们的订单总数,你可以创build一个接受CustomerID并返回总和的函数。

你可以这样做:

 SELECT CustomerID, CustomerSum.Total FROM Customers CROSS APPLY ufn_ComputeCustomerTotal(Customers.CustomerID) AS CustomerSum 

function如下所示:

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

显然,上面的例子可以在一个查询中没有用户定义的function。

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

对于SQL Server 2005,你可以用CROSS APPLY和一个表值函数来做到这一点。

  • 在SQL Server 2005中使用CROSS APPLY

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

这是上面n3rds解决scheme的一个变种。 不需要使用ORDER BY进行sorting,因为使用了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 

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

如果你不使用游标,我想你必须在外部执行(获取表格,然后运行每个语句,每次调用sp),它与使用游标相同,但只在外部SQL。 你为什么不使用游标?

我经常这样做,当它是相当多的行:

  1. 使用SQL Management Studioselect数据集中的所有sproc参数
  2. 右键单击 – >复制
  3. 粘贴到Excel中
  4. 在一个新的excel列中创build一个公式如“=”EXEC schema.mysproc @ param =“&A2”的单行sql语句。 (其中A2是包含参数的excel列)
  5. 将excel语句的列表复制到SQL Management Studio中的新查询中并执行。
  6. 完成。

(在较大的数据集上,我会使用上面提到的解决scheme之一)。

DELIMITER //

 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; 

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

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

也就是说,我同意马克·鲍威尔的观点,但总的来说,基于集合的方法应该还是比较好的。

 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 

如果订单很重要

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

更好的解决scheme是

  1. 复制/过去的存储过程代码
  2. join你想要再次运行它的表的代码(对于每一行)

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

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

我喜欢做类似这样的事情(尽pipe它和使用游标非常相似)

[码]

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

[/码]

请注意,您不需要身份或临时/variables表上的isIterated列,我只是喜欢这样做,所以我不必删除集合中的顶部logging,因为我遍历循环。