一部の人々にとって、それは間違った質問です。 SQLカーソルIS 間違い。悪魔は細部に宿る! SQL CURSORの名前で、SQLブロゴスフィア全体であらゆる種類の冒涜を読むことができます。
同じように感じた場合、この結論に至った理由は何ですか?
信頼できる友人や同僚からのものであれば、私はあなたを責めることはできません。それは起こります。時々たくさん。しかし、誰かがあなたに証拠を納得させた場合、それは別の話です。
今まで会ったことがありません。あなたは私を友達として知らない。しかし、例を挙げて説明し、SQLCURSORがその役割を果たしていることを納得させたいと思います。それほど多くはありませんが、コード内のその小さな場所にはルールがあります。
しかし、最初に、私の話をしましょう。
xBaseを使用してデータベースでプログラミングを開始しました。それは私の最初の2年間のプロのプログラミングまで大学に戻っていました。これは、昔はSQLのようなセットのバッチではなく、データを順番に処理していたためです。 SQLを学んだとき、それはパラダイムシフトのようなものでした。データベースエンジンは、私が発行したセットベースのコマンドを使用して決定します。 SQL CURSORについて知ったとき、私は古いが快適な方法に戻ったように感じました。
しかし、一部の先輩は、「SQLカーソルは絶対に避けてください」と警告しました。口頭での説明がいくつかありましたが、それだけでした。
SQL CURSORを間違ったジョブに使用すると、問題が発生する可能性があります。ハンマーを使って木を切るように、ばかげています。もちろん、間違いが起こる可能性があり、それが私たちの焦点です。
1。セットベースのコマンドでSQLCURSORを使用すると
これを十分に強調することはできませんが、これが問題の核心です。 SQL CURSORとは何かを最初に知ったとき、電球が点灯しました。 「おっと!そんなこと知ってる!"しかし、それが私に頭痛を与え、私の先輩が私を叱るまではありませんでした。
ご覧のとおり、SQLのアプローチはセットベースです。テーブル値からINSERTコマンドを発行すると、コードをループすることなくジョブが実行されます。前に言ったように、それはデータベースエンジンの仕事です。したがって、ループを強制してレコードをテーブルに追加すると、その権限をバイパスすることになります。醜くなるでしょう。
ばかげた例を試す前に、データを準備しましょう:
SELECT TOP (500)
val = ROW_NUMBER() OVER (ORDER BY sod.SalesOrderDetailID)
, modified = GETDATE()
, status = 'inserted'
INTO dbo.TestTable
FROM AdventureWorks.Sales.SalesOrderDetail sod
CROSS JOIN AdventureWorks.Sales.SalesOrderDetail sod2
SELECT
tt.val
,GETDATE() AS modified
,'inserted' AS status
INTO dbo.TestTable2
FROM dbo.TestTable tt
WHERE CAST(val AS VARCHAR) LIKE '%2%'
最初のステートメントは、500レコードのデータを生成します。 2番目のものはそのサブセットを取得します。これで準備が整いました。 TestTableから欠落しているデータを挿入します TestTable2に SQLカーソルを使用します。以下を参照してください:
DECLARE @val INT
DECLARE test_inserts CURSOR FOR
SELECT val FROM TestTable tt
WHERE NOT EXISTS(SELECT val FROM TestTable2 tt1
WHERE tt1.val = tt.val)
OPEN test_inserts
FETCH NEXT FROM test_inserts INTO @val
WHILE @@fetch_status = 0
BEGIN
INSERT INTO TestTable2
(val, modified, status)
VALUES
(@val, GETDATE(),'inserted')
FETCH NEXT FROM test_inserts INTO @val
END
CLOSE test_inserts
DEALLOCATE test_inserts
これが、SQL CURSORを使用してループし、欠落しているレコードを1つずつ挿入する方法です。かなり長いですよね?
それでは、より良い方法、つまりセットベースの代替方法を試してみましょう。
INSERT INTO TestTable2
(val, modified, status)
SELECT val, GETDATE(), status
FROM TestTable tt
WHERE NOT EXISTS(SELECT val FROM TestTable2 tt1
WHERE tt1.val = tt.val)
それは短く、きちんとしていて、速いです。どのくらい速いのか?以下の図1を参照してください:
SQL Server ManagementStudioでxEventProfilerを使用して、CPU時間の数値、期間、および論理読み取りを比較しました。図1に示すように、set-basedコマンドを使用してレコードを挿入すると、パフォーマンステストに勝ちます。数字はそれ自体を物語っています。 SQL CURSORを使用すると、より多くのリソースと処理時間が消費されます。
したがって、SQL CURSORを使用する前に、まずセットベースのコマンドを記述してみてください。長期的に見れば、より効果的になるでしょう。
しかし、仕事を遂行するためにSQLカーソルが必要な場合はどうでしょうか?
2。適切なSQLCURSORオプションを使用していない
私が過去に犯したもう1つの間違いは、DECLARECURSORで適切なオプションを使用していなかったことです。スコープ、モデル、同時実行性、およびスクロール可能かどうかのオプションがあります。これらの引数はオプションであり、無視するのは簡単です。ただし、SQL CURSORがタスクを実行する唯一の方法である場合は、意図を明示する必要があります。
ですから、自問してみてください:
- ループをトラバースするとき、行を前方にのみナビゲートしますか、それとも最初、最後、前、または次の行に移動しますか? CURSORが順方向のみであるか、スクロール可能であるかを指定する必要があります。それはDECLARE
CURSOR FORWARD_ONLY またはDECLARECURSOR SCROLL 。 - CURSORの列を更新しますか?更新できない場合は、READ_ONLYを使用してください。
- ループをトラバースするときに最新の値が必要ですか?最新かどうかに関係なく値が重要でない場合は、STATICを使用します。他のトランザクションがCURSORで使用する列を更新したり行を削除したりして、最新の値が必要な場合は、DYNAMICを使用します。 注 :DYNAMICは高価になります。
- CURSORは接続に対してグローバルですか、それともバッチまたはストアドプロシージャに対してローカルですか?ローカルかグローバルかを指定します。
これらの引数の詳細については、MicrosoftDocsのリファレンスを参照してください。
例
xEvents Profilerを使用して、CPU時間、論理読み取り、および期間について3つのカーソルを比較する例を試してみましょう。 DECLARE CURSORの後、最初のオプションには適切なオプションがありません。 2つ目は、LOCAL STATICFORWARD_ONLYREAD_ONLYです。最後はLOtyuiCALFAST_FORWARDです。
これが最初です:
-- NOTE: Don't just COPY and PASTE this code then run in your machine. Read and assess.
-- DECLARE CURSOR with no options
SET NOCOUNT ON
DECLARE @command NVARCHAR(2000) = N'SET NOCOUNT ON;'
CREATE TABLE #commands (
ID INT IDENTITY (1, 1) PRIMARY KEY CLUSTERED
,Command NVARCHAR(2000)
);
INSERT INTO #commands (Command)
VALUES (@command)
INSERT INTO #commands (Command)
SELECT
'SELECT ' + CHAR(39) + a.TABLE_SCHEMA + '.' + a.TABLE_NAME
+ ' - ' + CHAR(39)
+ ' + cast(count(*) as varchar) from '
+ a.TABLE_SCHEMA + '.' + a.TABLE_NAME
FROM INFORMATION_SCHEMA.tables a
WHERE a.TABLE_TYPE = 'BASE TABLE';
DECLARE command_builder CURSOR FOR
SELECT
Command
FROM #commands
OPEN command_builder
FETCH NEXT FROM command_builder INTO @command
WHILE @@fetch_status = 0
BEGIN
PRINT @command
FETCH NEXT FROM command_builder INTO @command
END
CLOSE command_builder
DEALLOCATE command_builder
DROP TABLE #commands
GO
もちろん、上記のコードよりも優れたオプションがあります。目的が既存のユーザーテーブルからスクリプトを生成することだけである場合、SELECTはそれを行います。次に、出力を別のクエリウィンドウに貼り付けます。
ただし、スクリプトを生成して一度に実行する必要がある場合、それは別の話です。サーバーに負担がかかるかどうかは、出力スクリプトを評価する必要があります。後で間違い#4を参照してください。
オプションが異なる3つのカーソルの比較を表示するには、これで十分です。
次に、同様のコードを作成しますが、LOCAL STATICFORWARD_ONLYREAD_ONLYを使用します。
--- STATIC LOCAL FORWARD_ONLY READ_ONLY
SET NOCOUNT ON
DECLARE @command NVARCHAR(2000) = N'SET NOCOUNT ON;'
CREATE TABLE #commands (
ID INT IDENTITY (1, 1) PRIMARY KEY CLUSTERED
,Command NVARCHAR(2000)
);
INSERT INTO #commands (Command)
VALUES (@command)
INSERT INTO #commands (Command)
SELECT
'SELECT ' + CHAR(39) + a.TABLE_SCHEMA + '.' + a.TABLE_NAME
+ ' - ' + CHAR(39)
+ ' + cast(count(*) as varchar) from '
+ a.TABLE_SCHEMA + '.' + a.TABLE_NAME
FROM INFORMATION_SCHEMA.tables a
WHERE a.TABLE_TYPE = 'BASE TABLE';
DECLARE command_builder CURSOR LOCAL STATIC FORWARD_ONLY READ_ONLY FOR SELECT
Command
FROM #commands
OPEN command_builder
FETCH NEXT FROM command_builder INTO @command
WHILE @@fetch_status = 0
BEGIN
PRINT @command
FETCH NEXT FROM command_builder INTO @command
END
CLOSE command_builder
DEALLOCATE command_builder
DROP TABLE #commands
GO
上記のように、前のコードとの唯一の違いは、 LOCAL STATIC FORWARD_ONLY READ_ONLYです。 引数。
3番目にはLOCALFAST_FORWARDがあります。現在、Microsoftによると、FAST_FORWARDは、最適化が有効になっているFORWARD_ONLY、READ_ONLYCURSORです。これが最初の2つでどのようにうまくいくかを見ていきます。
彼らはどのように比較しますか?図2を参照してください:
CPU時間と期間が少ないのは、LOCAL STATIC FORWARD_ONLYREAD_ONLYCURSORです。 STATICやREAD_ONLYなどの引数を指定しない場合、SQLServerにはデフォルトがあることにも注意してください。次のセクションで説明するように、これにはひどい結果があります。
sp_describe_cursorが明らかにしたもの
sp_describe_cursor マスターからのストアドプロシージャです 開いているCURSORから情報を取得するために使用できるデータベース。そして、これがCURSORオプションのないクエリの最初のバッチから明らかになったものです。 sp_describe_cursor の結果については、図3を参照してください。 :
やり過ぎ?あなたは賭けます。クエリの最初のバッチからのカーソルは次のとおりです。
- 既存の接続に対してグローバル。
- 動的。これは、更新、削除、および挿入のために#commandsテーブルの変更を追跡することを意味します。
- 楽観的。これは、SQLServerがCWTと呼ばれる一時テーブルに追加の列を追加したことを意味します。これは、#commandsテーブルの値の変更を追跡するためのチェックサム列です。
- スクロール可能。つまり、カーソルの前、次、上、または下の行に移動できます。
ばかげている?私は強く同意します。なぜグローバル接続が必要なのですか? #commands一時テーブルへの変更を追跡する必要があるのはなぜですか? CURSORの次のレコード以外の場所にスクロールしましたか?
SQL Serverがこれを判断すると、CURSORループはひどい間違いになります。
これで、SQLCURSORオプションを明示的に指定することが非常に重要である理由がわかりました。したがって、今後、CURSORを使用する必要がある場合は、常にこれらのCURSOR引数を指定してください。
実行計画により詳細が明らかになる
実際の実行プランには、FETCH NEXT FROM command_builderINTO@commandが実行されるたびに何が起こるかについてさらに説明があります。図4では、クラスター化インデックス CWT_PrimaryKeyに行が挿入されています。 tempdbで テーブルCWT :
書き込みはtempdbに発生します すべてのFETCHNEXTで。その上、もっとあります。図3では、カーソルが最適であることを覚えていますか?計画の右端にあるクラスター化インデックススキャンのプロパティにより、 Chk1002と呼ばれる余分な未知の列が明らかになります。 :
これはチェックサム列でしょうか? Plan XMLは、これが実際に当てはまることを確認しています。
ここで、CURSORがLOCAL STATIC FORWARD_ONLYREAD_ONLYの場合のFETCHNEXTの実際の実行プランを比較します。
tempdbを使用します でも、それははるかに簡単です。一方、図8は、LOCALFAST_FORWARDを使用した場合の実行プランを示しています。
要点
SQL CURSORの適切な使用法の1つは、データベースオブジェクトのグループに対してスクリプトを生成するか、いくつかの管理コマンドを実行することです。マイナーな使用法がある場合でも、最初のオプションはLOCAL STATIC FORWARD_ONLYREAD_ONLYCURSORまたはLOCALFAST_FORWARDを使用することです。より良い計画と論理的な読み取りを備えたものが勝ちます。
次に、必要に応じて、これらのいずれかを適切なものに置き換えます。しかし、あなたは何を知っていますか?私の個人的な経験では、フォワードのみのトラバーサルでローカルの読み取り専用カーソルのみを使用しました。 CURSORをグローバルで更新可能にする必要はありませんでした。
これらの引数を使用する以外に、実行のタイミングが重要です。
3。毎日のトランザクションでのSQLCURSORの使用
私は管理者ではありません。しかし、DBAのツールから(またはユーザーが叫ぶデシベルの数から)、ビジー状態のサーバーがどのように見えるかについてはわかります。このような状況で、さらに負担をかけたいですか?
日常のトランザクション用にCURSORを使用してコードを作成しようとしている場合は、もう一度考えてみてください。 CURSORは、データセットが小さい、ビジー状態の少ないサーバーでの1回限りの実行に適しています。ただし、通常の忙しい日には、カーソルは次のことができます。
- 特にSCROLL_LOCKS同時実行引数が明示的に指定されている場合は、行をロックします。
- CPU使用率が高くなります。
- tempdbを使用します 広範囲に。
これらのいくつかが通常の日に同時に実行されていると想像してください。
もうすぐ終わりますが、もう1つ話し合う必要のある間違いがあります。
4。 SQLカーソルがもたらす影響を評価しない
あなたはCURSORオプションが良いことを知っています。それらを指定するだけで十分だと思いますか?上記の結果はすでにご覧になっています。ツールがなければ、正しい結論を出すことはできません。
さらに、カーソル内のコード 。それが何をするかに応じて、それは消費されるリソースにさらに追加します。これらは他のプロセスで利用できた可能性があります。インフラストラクチャ全体、ハードウェア、およびSQL Serverの構成により、ストーリーがさらに追加されます。
データ量はどうですか ? SQLCURSORは数百のレコードでのみ使用しました。それはあなたにとって異なるかもしれません。最初の例では、500レコードしか取得しませんでした。これは、それが私が待つことに同意する数だったためです。 10,000または1000でさえそれをカットしませんでした。パフォーマンスが悪かった。
最終的には、いくら少なくても多くても、たとえば論理読み取りをチェックすることで違いが生じる可能性があります。
実行プラン、論理読み取り、または経過時間をチェックしない場合はどうなりますか? SQL Serverがフリーズする以外に、どのようなひどいことが起こる可能性がありますか?私たちはあらゆる種類の終末のシナリオを想像することしかできません。ポイントがわかります。
結論
SQL CURSORは、データを行ごとに処理することによって機能します。その場所はありますが、注意しないと悪い場合があります。ツールボックスから出てくることはめったにないツールのようなものです。
したがって、まず、セットベースのコマンドを使用して問題を解決してみてください。これは、SQLのニーズのほとんどに対応します。また、SQL CURSORを使用する場合は、適切なオプションを指定して使用してください。実行プラン、STATISTICS IO、およびxEventProfilerを使用して影響を見積もります。次に、実行する適切なタイミングを選択します。
これにより、SQLCURSORの使用が少し改善されます。