カーソルを使用しない各行のSQL呼び出しストアドプロシージャ


163

カーソル使用せずに、行の列がspへの入力パラメーターであるテーブルの各行に対してストアドプロシージャを呼び出すにはどうすればよいですか?


3
したがって、たとえば、customerId列を持つCustomerテーブルがあり、対応するcustomerIdをパラメータとして渡して、テーブルの各行に対してSPを1回呼び出す必要があるとします。
ゲイリーマギル

2
カーソルを使用できない理由について詳しく教えてください。
Andomar 2009年

@ゲイリー:たぶん私は単にIDではなく顧客名を渡したいだけなのかもしれません。しかし、あなたは正しいです。
ヨハネスルドルフ

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
受け入れられた回答と同様にUSE WITH CATION:テーブルとインデックスの構造によっては、列挙するたびにテーブルを注文して検索する必要があるため、パフォーマンスが非常に低下する可能性があります(O(n ^ 2))。
csauve

3
これは機能していないようです(breakはループを終了しません-作業は完了しましたが、クエリはループ内でスピンします)。idを初期化し、while条件でnullを確認すると、ループが終了します。
dudeNumber4 2013年

8
@@ ROWCOUNTは一度だけ読み取ることができます。IF / PRINTステートメントでも0に設定されます。@@ ROWCOUNTのテストは、選択の直後に行う必要があります。コード/環境を再確認します。technet.microsoft.com/en-us/library/ms187316.aspx
Mark Powell

3
ループはカーソルよりも優れているわけではありませんが、注意してください。ループはさらに悪化する可能性があります。techrepublic.com
Jaime

1
@Brennan Pope CURSORのLOCALオプションを使用すると、失敗すると破棄されます。LOCAL FAST_FORWARDを使用します。これらの種類のループにCURSORを使用しない理由はほとんどありません。それは間違いなくこのWHILEループよりも優れています。
マーティン

39

次のようなことができます。たとえば、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列に何らかの種類のを定義できる限り、どのテーブルでも機能するはずです。


@ミッチ:はい、本当-オーバーヘッドが少し少なくなります。しかし、それでも、それは実際にはSQLのセットベースの考え方にはありません
marc_s 2009年

6
セットベースの実装は可能ですか?
ヨハネスルドルフ

私はそれを達成する方法を知りません、本当に-それはそもそも非常に手続き的なタスクです...
marc_s

2
@marc_sは、コレクション内の各アイテムに対して関数/ストアプロシージャを実行します。これは、セットベースの操作の基本のように聞こえます。問題はおそらくそれぞれの結果が得られないことから生じます。ほとんどの関数型プログラミング言語の「マップ」を参照してください。
ダニエル

4
re:Daniel。機能はい、ストアドプロシージャいいえ。ストアドプロシージャは当然のことながら副作用を持つ可能性があり、クエリでは副作用は許可されません。同様に、関数型言語の適切な「マップ」は副作用を禁止します。
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)

わかりましたので、そのようなコードを運用環境に配置することはありませんが、要件を満たしています。


プロシージャが行の値を設定する必要がある値を返したときに同じことをするにはどうすればよいですか?関数の作成は許可されていないため、関数ではなくPROCEDUREを使用)
user2284570 '26

@WeihuiGuoは、文字列を使用して動的に構築されたコードは、失敗する可能性が高く、デバッグするのに苦労するためです。生産環境の通常の部分になる可能性のない1回限りの外では、このようなことは絶対に行わないでください
マリー

11

マルクの答えは良いです(方法がわかればコメントします!)
ループを変更して、SELECT1つだけ存在するようにした方がよいと指摘しただけです(実際のケースでは、これを行うと、それSELECTは非常に複雑であり、それを2度書くことは危険なメンテナンスの問題でした)。

-- 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は関数でのみ使用できます...したがって、関数を使用する必要がない場合は、このアプローチの方がはるかに優れています。
Artur

コメントするには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
)

上記の例は、単一のクエリでユーザー定義関数なしで実行できることは明らかです。

欠点は、関数が非常に制限されていることです。ストアドプロシージャの機能の多くはユーザー定義関数では使用できず、ストアドプロシージャを関数に変換しても機能しない場合があります。


関数を作成するための書き込み権限がない場合?
user2284570

7

私は受け入れられた答えを使用しますが、別の可能性は、テーブル変数を使用して番号の付いた値のセット(この場合はテーブルのIDフィールドのみ)を保持し、テーブルへの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

これは、あなたが求めている値が整数であるとは想定していないか、または慎重に比較できるため、より優れています。
philw 2015

まさに私が探していたもの。
レイスリン2018

6

SQL Server 2005以降では、CROSS APPLYとテーブル値関数を使用してこれを行うことができます。

わかりやすくするために、ストアドプロシージャをテーブル値関数に変換できるケースについて説明します。


12
いいアイデアですが、関数はストアドプロシージャを呼び出せません
Andomar

3

これは、上記のn3rdsソリューションのバリエーションです。MIN()が使用されるため、ORDER BYを使用したソートは不要です。

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

私は、一時テーブルに最初に配置してIDを与えることで、調べる必要のある一部のvarcharでこのアプローチを使用しています。


2

カーソルを使用する方法がわからない場合は、外部で実行する必要があると思います(テーブルを取得し、各ステートメントに対して実行し、毎回spを呼び出す)カーソルを使用するのと同じですが、外側のみです。 SQL。なぜカーソルを使用しないのですか?


2

これは、すでに提供されている回答のバリエーションですが、ORDER BY、COUNT、またはMIN / MAXを必要としないため、パフォーマンスが向上するはずです。このアプローチの唯一の欠点は、すべてのIDを保持する一時テーブルを作成する必要があることです(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. SQL Management Studioでデータセット内のすべてのsprocパラメータを選択します
  2. 右クリック->コピー
  3. Excelに貼り付けてください
  4. 新しいExcel列に「= "EXEC schema.mysproc @ param ="&A2」のような式を使用して単一行のSQLステートメントを作成します。(A2はパラメーターを含むExcel列です)
  5. ExcelステートメントのリストをSQL Management Studioの新しいクエリにコピーして実行します。
  6. できました。

(ただし、より大きなデータセットでは、上記のソリューションの1つを使用します)。


4
プログラミングの状況ではあまり役に立ちませんが、これは1回限りのハックです。
ウォーレンP

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;

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

[/コード]

一時/変数テーブルのIDまたはisIterated列は必要ないことに注意してください。ループを反復処理するときにコレクションから最上位のレコードを削除する必要がないように、この方法で実行することをお勧めします。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.