Хранимая процедура вызова SQL для каждой строки без использования курсора - PullRequest
142 голосов
/ 01 ноября 2009

Как можно вызвать хранимую процедуру для каждой строки в таблице, где столбцы строки являются входными параметрами для sp без с использованием курсора?

Ответы [ 16 ]

181 голосов
/ 29 мая 2011

Вообще говоря, я всегда ищу подход, основанный на множестве (иногда за счет изменения схемы).

Однако этот фрагмент имеет свое место ..

-- 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
38 голосов
/ 01 ноября 2009

Вы можете сделать что-то вроде этого: заказать столик, например, 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 для некоторого столбца.

26 голосов
/ 15 декабря 2012
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)

Хорошо, я бы никогда не внедрил такой код в производство, но он удовлетворяет вашим требованиям.

10 голосов
/ 28 сентября 2010

Хороший ответ Марка (я бы прокомментировал его, если бы смог разобраться, как это сделать!)
Просто подумал, что могу указать, что может быть лучше изменить цикл так, чтобы 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
7 голосов
/ 17 сентября 2010

Если вы можете превратить хранимую процедуру в функцию, которая возвращает таблицу, вы можете использовать перекрестное применение.

Например, если у вас есть таблица клиентов, и вы хотите вычислить сумму их заказов, вы бы создали функцию, которая бы принимала идентификатор клиента и возвращала сумму.

И вы могли бы сделать это:

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
)

Очевидно, что приведенный выше пример может быть выполнен без определяемой пользователем функции в одном запросе.

Недостатком является то, что функции очень ограничены - многие функции хранимой процедуры недоступны в пользовательской функции, и преобразование хранимой процедуры в функцию не всегда работает.

6 голосов
/ 01 ноября 2009

Для SQL Server 2005 и более поздних версий это можно сделать с помощью CROSS APPLY и табличной функции.

Просто для ясности, я имею в виду те случаи, когда хранимая процедура может быть преобразована в табличную функцию.

4 голосов
/ 24 сентября 2014

Я бы использовал принятый ответ, но другая возможность состоит в том, чтобы использовать переменную таблицы для хранения пронумерованного набора значений (в данном случае это просто поле идентификатора таблицы) и перебирать их по номеру строки с 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
3 голосов
/ 15 октября 2012

Это вариант решения 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, которые мне нужно просмотреть, поместив их во временную таблицу, чтобы дать им идентификатор.

2 голосов
/ 01 ноября 2009

Если вам не нужно использовать курсор, я думаю, вам придется делать это извне (получить таблицу, а затем выполнить для каждого оператора и каждый раз вызывать sp) Это то же самое, что использование курсора, но только вне SQL. Почему вы не используете курсор?

1 голос
/ 30 мая 2012

РАЗДЕЛИТЕЛЬ //

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