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

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

Как бы я написал запрос для этого?

7 ответов


используйте курсор

добавление: [пример курсора MS SQL]

declare @field1 int
declare @field2 int
declare cur CURSOR LOCAL for
    select field1, field2 from sometable where someotherfield is null

open cur

fetch next from cur into @field1, @field2

while @@FETCH_STATUS = 0 BEGIN

    --execute your sproc on each row
    exec uspYourSproc @field1, @field2

    fetch next from cur into @field1, @field2
END

close cur
deallocate cur

в MS SQL,вот пример статьи

обратите внимание, что курсоры медленнее, чем операции на основе набора, но быстрее, чем ручные циклы while; подробнее в этом так вопрос

добавление 2: Если вы будете обрабатывать больше, чем просто несколько записей, сначала вытащите их в временную таблицу и запустите курсор над временной таблицей; это предотвратит SQL от эскалация в таблицы-замки и ускорить работу

добавление 3: и, конечно, если вы можете встроить все, что ваша хранимая процедура делает с каждым идентификатором пользователя, и запустить все это как один оператор обновления SQL, это было бы оптимально


попробуйте изменить свой метод, если вам нужно цикл!

в родительской хранимой процедуре создайте таблицу #temp, содержащую данные, которые необходимо обработать. Вызовите дочернюю хранимую процедуру, таблица #temp будет видна, и вы можете обработать ее, надеюсь, работая со всем набором данных и без курсора или цикла.

это действительно зависит от того, что делает эта дочерняя хранимая процедура. Если вы обновляете, вы можете "обновить от" присоединения в таблице #temp и выполните всю работу в одном операторе без цикла. То же самое можно сделать для INSERT и Delets. Если вам нужно сделать несколько обновлений с IFs, вы можете преобразовать их в несколько UPDATE FROM с таблицей # temp и инструкциями Use CASE или условиями WHERE.

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

опубликовать содержимое этой процедуры, которую вы хотите вызвать в цикле, и я ставлю 9 из 10 раз, вы можете написать ее для работы над набором строк.


что-то вроде этой замены потребуется для ваших таблиц и имен полей.

Declare @TableUsers Table (User_ID, MyRowCount Int Identity(1,1)
Declare @i Int, @MaxI Int, @UserID nVarchar(50)

Insert into @TableUser
Select User_ID
From Users 
Where (My Criteria)
Select @MaxI = @@RowCount, @i = 1

While @i <= @MaxI
Begin
Select @UserID = UserID from @TableUsers Where MyRowCount = @i
Exec prMyStoredProc @UserID
Select

 @i = @i + 1, @UserID = null
End

нельзя ли это сделать с помощью пользовательской функции для репликации того, что делает хранимая процедура?

SELECT udfMyFunction(user_id), someOtherField, etc FROM MyTable WHERE WhateverCondition

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

посмотреть http://www.sqlteam.com/article/user-defined-functions немного больше фона

Я согласен, что курсоры действительно следует избегать, где это возможно. И это обычно возможно!

(of конечно, мой ответ предполагает, что вы заинтересованы только в получении выходных данных от SP и что вы не изменяете фактические данные. Я нахожу, что "изменяет пользовательские данные определенным образом" немного неоднозначно от исходного вопроса, поэтому я подумал, что предложу это как возможное решение. Все зависит от того, что вы делаете!)


вы можете сделать это с помощью динамического запроса.

declare @cadena varchar(max) = ''
select @cadena = @cadena + 'exec spAPI ' + ltrim(id) + ';'
from sysobjects;
exec(@cadena);

Мне нравится динамический способ запроса Дэйва Ринкона, поскольку он не использует курсоры и мал и прост. Спасибо, Дэйв, что поделился.

но для моих нужд в Azure SQL и с "отличным" в запросе мне пришлось изменить код следующим образом:

Declare @SQL nvarchar(max);
-- Set SQL Variable
-- Prepare exec command for each distinctive tenantid found in Machines 
SELECT @SQL = (Select distinct 'exec dbo.sp_S2_Laser_to_cache ' + 
              convert(varchar(8),tenantid) + ';' 
              from Dim_Machine
              where iscurrent = 1
              FOR XML PATH(''))

--for debugging print the sql 
print @SQL;

--execute the generated sql script
exec sp_executesql @SQL;

надеюсь, это кому-то поможет...


использовать табличную переменную или временную таблицу.

Как уже упоминалось ранее, Курсор-это крайняя мера. В основном потому, что он использует много ресурсов, блокирует проблемы и может быть признаком того, что вы просто не понимаете, как правильно использовать SQL.

(Примечание: я однажды наткнулся на решение, которое использовало курсоры для обновления строк в таблице. После некоторого изучения оказалось, что все это можно заменить одной командой обновления.)

однако, в в этом случае, когда хранимая процедура должна быть выполнена, одна SQL-команда не будет работать.

создать табличную переменную, как это (если вы работаете с большим количеством данных или не хватает памяти, использовать временная таблица вместо):

DECLARE @menus AS TABLE (
    id INT IDENTITY(1,1),
    parent NVARCHAR(128),
    child NVARCHAR(128));

на id важно.

заменить parent и child С некоторыми хорошими данными, например, соответствующими идентификаторами или всем набором данных для работы.

вставить данные в таблицу, например:

INSERT INTO @menus (parent,child) 
  VALUES ('Some name',  'Child name');
...
INSERT INTO @menus (parent,child) 
  VALUES ('Some other name', 'Some other child name');

объявить несколько переменных:

DECLARE @id INT = 1;
DECLARE @parentName NVARCHAR(128);
DECLARE @childName NVARCHAR(128);

и, наконец, создайте цикл while над данными в таблице:

WHILE @id IS NOT NULL
BEGIN
    SELECT @parentName = parent,
           @childName = child 
        FROM @menus WHERE id = @id;

    EXEC myProcedure @parent=@parentName, @child=@childName;

    SELECT @id = MIN(id) FROM @menus WHERE id > @id;
END

первый select извлекает данные из временной таблицы. Второй выбор обновляет @id. MIN возвращает null, если строки не были выбраны.

альтернативным подходом является цикл, в то время как таблица имеет строки, SELECT TOP 1 и удалите выбранную строку из temp таблица.

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

WHILE EXISTS(SELECT 1 FROM @menuIDs) 
BEGIN
    SELECT TOP 1 @menuID = menuID FROM @menuIDs;

    EXEC myProcedure @menuID=@menuID;

    DELETE FROM @menuIDs WHERE menuID = @menuID;
END;