Процедура зберігання виклику SQL для кожного рядка без використання курсору


163

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


3
Так, наприклад, у вас є таблиця Customer із стовпцем customerId, і ви хочете викликати SP один раз для кожного рядка таблиці, передаючи відповідний customerId як параметр?
Гері Макгілл

2
Не могли б ви детальніше пояснити, чому ви не можете використовувати курсор?
Андомар

@Gary: Можливо, я просто хочу ввести Ім'я клієнта, не обов'язково ідентифікатор. Але ви праві.
Йоханнес Рудольф

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
Це, здається, не працює (перерва ніколи не виходить з циклу для мене - робота виконана, але запит крутиться в циклі). Ініціалізація ідентифікатора та перевірка наявності null в той час, коли стан виходить з циклу.
dudeNumber4

8
@@ ROWCOUNT можна прочитати лише один раз. Навіть висловлювання IF / PRINT встановлять його на 0. Тест на @@ ROWCOUNT потрібно зробити "негайно" після вибору. Я б перевiрив ваш код / ​​середовище. technet.microsoft.com/en-us/library/ms187316.aspx
Марк Пауелл

3
Хоча петлі не кращі за курсори, будьте обережні, вони можуть бути ще гіршими: techrepublic.com/blog/the-enterprise-cloud/…
Хайме

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якомусь стовпці.


@Mitch: так, правда - трохи менше накладних витрат. Але все-таки - насправді справа не в наборі менталітетів 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)

Гаразд, я б ніколи не вводив такого коду у виробництво, але він відповідає вашим вимогам.


Як зробити те саме, коли процедура повертає значення, яке повинно встановлювати значення рядка? (використання ПРОЦЕДУРИ замість функції, оскільки створення функції заборонено )
користувач2284570

@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

APPLY можна використовувати лише з функціями ... тому такий підхід набагато кращий, якщо ви не хочете робити з функціями.
Артур

Щоб коментувати, потрібно 50 повторень. Продовжуйте відповідати на ці питання , і ви отримаєте більше енергії: D stackoverflow.com/help/privileges
SvendK

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

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
)

Очевидно, що приклад вище можна зробити без визначеної користувачем функції в одному запиті.

Недолік полягає в тому, що функції дуже обмежені - багато функцій збереженої процедури недоступні у визначеній користувачем функції, а перетворення збереженої процедури у функцію не завжди працює.


У випадку, якщо немає дозволу на запис для створення функції?
користувач2284570

7

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

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

Це краще, оскільки воно не припускає, що значення, яке ви шукаєте, є цілим числом або його можна порівняно розумно порівняти.
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

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


2

Якщо ви не маєте для чого використовувати курсор, я думаю, що вам доведеться це робити зовні (дістаньте таблицю, а потім запустіть для кожного оператора і кожен раз зателефонуйте sp) це те саме, що використовувати курсор, але тільки зовні SQL. Чому ви не використовуєте курсор?


2

Це варіант у відповідях, що вже надаються, але він повинен бути більш ефективним, оскільки він не вимагає ЗАМОВЛЕННЯ ПО, КОНТАКТУ або МІН / МАКС. Єдиним недоліком такого підходу є те, що вам потрібно створити темп-таблицю для зберігання всіх ідентифікаторів (припущення полягає в тому, що у вашому списку 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

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
Не дуже корисно в програмуванні, це одноразовий злом.
Warren P

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;

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

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 у таблиці temp / змінної, я просто вважаю за краще це робити так, щоб мені не довелося видаляти верхню запис із колекції, як я повторюю цикл.

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