Як виконати збережену процедуру один раз для кожного рядка, повернутого запитом?


206

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

Як би я написав запит на це?


5
Вам потрібно вказати, що таке RDBMS - відповідь буде різною для SQL Server, Oracle, MySql тощо.
Gary.Ray

5
Ймовірно, що вам взагалі не потрібна збережена процедура. Чи можете ви точно окреслити "що" зберігає процедуру? Можливо, увесь процес можна виразити як єдину заяву оновлення. Загалом слід уникати шаблону "зробити один раз для кожного запису", якщо це можливо.
Томалак

Яку базу даних ви використовуєте?
SO Користувач

1
Ви повинні прочитати цю статтю ... У пункті 2 сказано, що НЕ використовуйте курсори codeproject.com/KB/database/sqldodont.aspx ..., я також проти передчасної оптимізації.
Майкл Превецький

7
@MichaelPrewecki: Якщо ви прочитаєте далі в цій погано написаній статті, ви побачите, що пункт 10 - "НЕ використовуйте бічні курсори сервера, якщо ви не знаєте, що ви робите". Я думаю, що це випадок "я знаю, що роблю".
Гейб

Відповіді:


246

використовувати курсор

ДОДАТОК: [приклад курсора 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, ось приклад статті

зауважте, що курсори повільніші, ніж операції на основі встановлених даних, але швидші, ніж ручні петлі; Більш детально у цьому питанні

ДОПОЛНЕННЯ 2: якщо ви будете обробляти більше ніж кілька записів, спочатку перетягніть їх у таблицю темпів та запустіть курсор над таблицею темпів; це запобіжить ескалації SQL у блоки столів та прискорить роботу

ДОПОМОГА 3: і, звичайно, якщо ви можете вкласти вбудовану будь-яку збережену процедуру до кожного ідентифікатора користувача та запустити все як єдиний оператор оновлення SQL, це було б оптимальним


21
Ви пропустили "відкритий кур" після декларації - це дало мені помилки "курсор не відкритий". У мене немає представника для редагування.
Fiona - myaccessible.website

5
Ви можете подякувати людям, голосуючи за їх коментар. Хто знає, можливо, таким чином у наступного разу вони матимуть представника для редагування! :-)
Робіно,

Переконайтеся, що ви перевіряєте ваші індекси на пунктах JOINS і WHERE у полях, які використовуються у вашій збереженій процедурі. Я різко прискорив виклик свого SP в циклі після додавання відповідних індексів.
Матвій

1
Дякуємо за нагадування про використання таблиці темп, щоб уникнути можливих проблем із блокуванням, викликаних тривалим виконанням.
Тоні

Іноді збережена процедура є занадто великою або складною для вбудовування, не ризикуючи вводити помилки. Якщо продуктивність не є головним пріоритетом, виконання SP в курсовому циклі часто є найбільш практичним вибором.
Suncat2000

55

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

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

це насправді залежить від того, що робить ця зберігається дітьми процедура. Якщо ви ОНОВЛЮЄТЬСЯ, ви можете "оновити з" приєднання до таблиці #temp і виконати всю роботу в одному операторі без циклу. Те саме можна зробити для INSERT та DELETE. Якщо вам потрібно зробити кілька оновлень за допомогою ІФ, ви можете перетворити їх у кількаUPDATE FROM за допомогою таблиці #temp і використовувати оператори CASE або умови WHERE.

Під час роботи в базі даних намагайтеся втратити розумовий цикл, це справжній злив продуктивності, спричинить блокування / блокування та уповільнить обробку. Якщо ви циркулюєте скрізь, ваша система не буде масштабуватися дуже добре, і буде дуже важко прискорити, коли користувачі почнуть скаржитися на повільне оновлення.

Опублікуйте вміст цієї процедури, яку ви хочете зателефонувати, в циклі, і я ставлю 9 з 10 разів, ви можете написати це для роботи на наборі рядків.


3
+1 за дуже вдале рішення, якщо припустити, що ви керуєте паросткою дитини
Стівен А. Лоу

трохи подумавши, це забруднення набагато перевершує!
encc

7
Встановлені операції завжди є кращими. Однак майте на увазі, що зміни SP не завжди є варіантом - подумайте, що постачальник постачав рішення. Деякі користувачі можуть навіть не мати видимості, залишаючи лише курсор або параметр циклу. У моєму магазині наші дияволи можуть побачити все, але є багато перешкод, щоб зрозуміти, чи рішення будується поза додатком постачальника через тригери, вкладені програми, кількість записів, що маніпулюються і т. Д. Багато разів найкращий варіант, завдяки Складність програми полягає в тому, щоб просто перемістити курсор через записи.
Стів Манґямелі

11

Щось подібне заміни знадобиться для ваших таблиць та імен полів.

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

2
а петлі повільніші за курсори
Стівен А. Лоу

Конструкція або заява SQL курсора не підтримується (??)
MetaGuru

9

Ви можете зробити це за допомогою динамічного запиту.

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

6

Чи не можна цього зробити за допомогою визначеної користувачем функції для копіювання того, що робить збережена процедура?

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

де udfMyFunction - це функція, яку ви створюєте, яка містить ідентифікатор користувача і робить все, що вам потрібно з цим робити.

Див. Http://www.sqlteam.com/article/user-defined-functions отримання додаткової інформації

Я погоджуюсь, що дійсно слід уникати курсорів, де це можливо. І зазвичай це можливо!

(звичайно, моя відповідь передбачає, що ви зацікавлені лише в отриманні результатів з ІП ​​і що ви не змінюєте фактичні дані. Я вважаю, що "певними способами змінюються дані користувачів", ніж у первісному питанні, тому я подумав, що я запропоную це як можливе рішення. Повністю залежить від того, що ти робиш!)


1
OP: "збережена процедура, яка певним чином змінює дані користувача" MSDN : Функції, визначені користувачем, не можна використовувати для виконання дій, що змінюють стан бази даних. Однак у SQLSVR 2014, схоже, немає проблем з цим
johnny 5

6

Використовуйте змінну таблиці або тимчасову таблицю.

Як вже було сказано раніше, курсор є крайнім засобом. Переважно тому, що він використовує багато ресурсів, видає блокування і може бути ознакою того, що ви просто не розумієте, як правильно використовувати SQL.

Побічна примітка: Я одного разу натрапив на рішення, яке використовувало курсори для оновлення рядків у таблиці. Після деякої перевірки виявилося, що вся справа може бути замінена однією командою UPDATE. Однак у цьому випадку, коли має бути виконана збережена процедура, одна SQL-команда не працюватиме.

Створіть подібну змінну таблиці (якщо ви працюєте з великою кількістю даних або не вистачає пам’яті, замість цього використовуйте тимчасову таблицю ):

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

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

Перший вибір витягує дані з тимчасової таблиці. Другий вибір оновлює @id.MINповертає null, якщо не було вибрано рядків.

Альтернативний підхід полягає в циклі, поки таблиця має рядки, SELECT TOP 1і видалення вибраного рядка з таблиці темп:

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;

3

Мені подобається динамічний спосіб запиту Дейва Рінкона, оскільки він не використовує курсори, і він малий і простий. Дякую, Дейв, що поділився.

Але для моїх потреб у 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;

Я сподіваюся, що це комусь допоможе ...

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.