カーソルを使用して、行の列がspwithoutへの入力パラメーターであるテーブルの各行に対してストアドプロシージャを呼び出すにはどうすればよいですか?
一般的に言えば、私は常にセットベースのアプローチを探します(時にはスキーマを変更することを犠牲にして)。
ただし、このスニペットには場所があります。
-- 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
あなたはこのようなことをすることができます:例えば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
を定義できる限り、これはどのテーブルでも機能するはずです。
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)
わかりましたので、私はそのようなコードを本番環境に入れることはありませんが、それはあなたの要件を満たします。
マークの答えは良いです(どうすれば解決できるかについてコメントします!)SELECT
が1回だけ存在するようにループを変更する方が良いと指摘したいと思いました(これを行う必要がある実際のケースでは、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
ストアドプロシージャをテーブルを返す関数に変換できる場合は、クロス適用を使用できます。
たとえば、顧客のテーブルがあり、注文の合計を計算する場合、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
)
明らかに、上記の例は、単一のクエリでユーザー定義関数なしで実行できます。
欠点は、関数が非常に限られていることです。ストアドプロシージャの機能の多くはユーザー定義関数では使用できず、ストアドプロシージャを関数に変換しても常に機能するとは限りません。
SQL Server 2005以降では、 CROSS APPLY とテーブル値関数を使用してこれを実行できます。
わかりやすくするために、ストアドプロシージャをテーブル値関数に変換できる場合について言及しています。
私は受け入れられた答えを使用しますが、別の可能性は、テーブル変数を使用して番号付きの値セット(この場合はテーブルの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
これは上記の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で使用します。
カーソルを使用しない場合は、外部で実行する必要があると思います(テーブルを取得し、各ステートメントで実行し、spを呼び出すたびに)カーソルを使用するのと同じですが、外部のみSQL。なぜカーソルを使用しないのですか?
区切り記号//
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;
かなりの数の行がある場合、私は通常このようにします:
(大きなデータセットでは、上記のソリューションのいずれかを使用します)。
一度に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
順序が重要な場合
--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
これはすでに提供されている回答のバリエーションですが、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
これに対するより良い解決策は
- ストアドプロシージャのコードのコピー/貼り付け
- そのコードを(各行に対して)再度実行するテーブルと結合します
これは、きれいなテーブル形式の出力を取得しました。一方、すべての行に対してSPを実行すると、反復ごとに個別のクエリ結果が得られますが、これは見苦しくなります。