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

时间:2009-11-01 10:15:21

标签: sql sql-server stored-procedures cursor

如何为表中的每一行调用存储过程,其中一行的列是sp 的输入参数,而不使用 使用Cursor?

17 个答案:

答案 0 :(得分:179)

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

但是,这段代码确实有它的位置..

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

答案 1 :(得分:37)

您可以执行以下操作:按以下顺序订购表格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,就可以使用任何表格。

答案 2 :(得分:25)

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)

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

答案 3 :(得分:10)

马克的答案很好(如果我能弄明白的话,我会评论它!)
我想我会指出改变循环可能会更好,因此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

答案 4 :(得分: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
)

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

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

答案 5 :(得分:6)

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

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

答案 6 :(得分:4)

我使用接受的答案,但另一种可能性是使用表变量来保存一组编号的值(在这种情况下只是表的ID字段)并通过行号循环使用加入表格以检索循环中动作所需的任何内容。

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

答案 7 :(得分:3)

这是上面的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。

答案 8 :(得分:2)

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

答案 9 :(得分:1)

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;

答案 10 :(得分:1)

我通常会这样做,因为它有很多行:

  1. 使用SQL Management Studio
  2. 选择数据集中的所有sproc参数
  3. 右键单击 - &gt;复制
  4. 粘贴到excel
  5. 使用类似'='的公式创建单行sql语句EXEC schema.mysproc @ param =“&amp; A2'在一个新的excel专栏中。 (其中A2是包含参数的excel列)
  6. 将excel语句列表复制到SQL Management Studio中的新查询中并执行。
  7. 完成。
  8. (在较大的数据集上,我会使用上面提到的解决方案之一)。

答案 11 :(得分:0)

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

答案 12 :(得分: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

答案 13 :(得分:0)

更好的解决方案是

  
      
  1. 存储过程的复制/过去代码
  2.   
  3. 将该代码加入要再次运行它的表(对于每一行)
  4.   

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

答案 14 :(得分: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

答案 15 :(得分:0)

我遇到了需要对结果集(表)执行一系列操作的情况。这些操作都是已设置的操作,因此这不是问题,但是... 我需要在多个地方这样做。因此,将相关片段放入表类型中,然后使用每个结果集填充表变量,使我可以调用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 

答案 16 :(得分:-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

[/代码]

请注意,您不需要需要 temp / variable表上的标识或isIterated列,我更喜欢这样做,所以我不必删除顶部记录我遍历循环时的集合。