Хранимая процедура вызова SQL для каждой строки без использования курсора

163

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

Йоханнес Рудольф
источник
3
Так, например, у вас есть таблица Customer со столбцом customerId, и вы хотите вызвать SP один раз для каждой строки в таблице, передав соответствующий параметр customerId в качестве параметра?
Гэри МакГилл
2
Не могли бы вы рассказать, почему вы не можете использовать курсор?
Andomar
@ Гэри: Может быть, я просто хочу передать имя клиента, а не обязательно идентификатор. Но ты прав.
Йоханнес Рудольф
2
@ Andomar: чисто научный :-)
Йоханнес Рудольф
1
Эта проблема меня тоже сильно беспокоит.
Даниэль

Ответы:

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
как с принятым ответом ИСПОЛЬЗОВАТЬ С КАТИОНОМ: В зависимости от вашей таблицы и структуры индекса она может быть очень неэффективной (O (n ^ 2)), так как вам приходится упорядочивать и искать таблицу каждый раз при перечислении.
csauve
3
Кажется, это не работает (для меня break никогда не выходит из цикла - работа выполнена, но запрос вращается в цикле). Инициализация идентификатора и проверка на нулевое значение в условии while выходит из цикла.
dudeNumber4
8
@@ ROWCOUNT можно прочитать только один раз. Даже операторы IF / PRINT будут устанавливать его в 0. Тест для @@ ROWCOUNT должен быть выполнен «сразу» после выбора. Я бы перепроверил ваш код / ​​среду. technet.microsoft.com/en-us/library/ms187316.aspx
Марк Пауэлл
3
Хотя циклы не лучше курсоров, будьте осторожны, они могут быть еще хуже: techrepublic.com/blog/the-enterprise-cloud/…
Jaime
1
@Brennan Pope Используйте опцию LOCAL для CURSOR, и она будет уничтожена при неудаче. Используйте LOCAL FAST_FORWARD, и практически нет причин не использовать CURSOR для таких циклов. Это определенно превзойдет этот цикл WHILE.
Мартин
39

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

marc_s
источник
@ Митч: да, правда - немного меньше накладных расходов. Но все же - это не совсем в менталитете SQL на основе множеств
marc_s
6
Возможна ли реализация на основе множеств?
Йоханнес Рудольф
Я не знаю ни одного способа добиться этого, на самом деле - это очень процедурная задача для начала ....
marc_s
2
@marc_s выполняет функцию / storeprocedure для каждого элемента в коллекции, которая звучит как хлеб с маслом операций на основе множеств. Проблема возникает, вероятно, из-за отсутствия результатов от каждого из них. Смотрите «карту» в большинстве функциональных языков программирования.
Даниэль
4
Re: Даниил. Функция да, хранимая процедура нет. Хранимая процедура по определению может иметь побочные эффекты, и побочные эффекты не допускаются в запросах. Точно так же правильная «карта» на функциональном языке запрещает побочные эффекты.
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)

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

Томас Габриэль
источник
Как сделать то же самое, когда процедура возвращает значение, которое должно установить значение строки? (используя ПРОЦЕДУРУ вместо функции, потому что создание функции не разрешено )
user2284570
@WeihuiGuo, потому что код, созданный динамически с использованием строк, УЖАСНО склонен к сбоям и полной боли в отладке. Вы никогда не должны делать ничего подобного, кроме тех, которые не имеют шансов стать рутинной частью производственной среды
Мари
11

Хороший ответ Марка (я бы прокомментировал это, если бы смог разобраться, как это сделать!)
Просто подумал, что могу указать, что может быть лучше изменить цикл, чтобы 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
Maxxx
источник
APPLY может использоваться только с функциями ... так что этот подход гораздо лучше, если вы не хотите иметь дело с функциями.
Артур
Вам нужно 50 повторений, чтобы комментировать. Продолжайте отвечать на эти вопросы, и вы получите больше возможностей: D stackoverflow.com/help/privileges
SvendK
Я думаю, что этот ответ должен быть простым и понятным. Большое спасибо!
bomblike
7

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

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

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

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
7

Я бы использовал принятый ответ, но другая возможность состоит в том, чтобы использовать переменную таблицы для хранения пронумерованного набора значений (в данном случае это просто поле идентификатора таблицы) и перебирать их по номеру строки с помощью 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
AjV Jsy
источник
Это лучше, потому что он не предполагает, что значение, которое вы ищете, является целым числом или может быть разумно сравнено.
philw
Именно то, что я искал.
Рэйтлин
6

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

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

Митч Пшеничный
источник
12
Хорошая идея, но функция не может вызвать хранимую процедуру
Andomar
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.

beruic
источник
2

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

Дани
источник
2

Это вариант уже предоставленных ответов, но он должен быть более эффективным, поскольку не требует ORDER BY, COUNT или MIN / MAX. Единственный недостаток этого подхода заключается в том, что вам необходимо создать временную таблицу для хранения всех идентификаторов (предполагается, что в вашем списке идентификаторов клиентов есть пробелы).

Тем не менее, я согласен с @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. Выбрать все параметры sproc в наборе данных с помощью SQL Management Studio
  2. Щелкните правой кнопкой мыши -> Копировать
  3. Вставить, чтобы преуспеть
  4. Создайте однострочные операторы SQL с формулой, подобной '= "EXEC schema.mysproc @ param =" & A2', в новом столбце Excel. (Где A2 - ваш столбец Excel, содержащий параметр)
  5. Скопируйте список операторов Excel в новый запрос в SQL Management Studio и выполните.
  6. Готово.

(На больших наборах данных я бы использовал одно из упомянутых выше решений).

Джонас Стенсвед
источник
4
Не очень полезно в ситуациях программирования, это одноразовый взлом.
Уоррен П
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;
radixxko
источник
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
isxaker
источник
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 в таблице временных / переменных, я просто предпочитаю делать это таким образом, чтобы мне не нужно было удалять верхнюю запись из коллекции, когда я выполняю цикл.

Kritner
источник