オプティマイザは常に1つの行を持っていると見なすため、行数が多いテーブル変数は問題になる可能性があることが長い間確立されてきました。テーブル変数が入力された後(それ以前は空であるため)の再コンパイルがないと、テーブルのカーディナリティはなく、テーブル変数は再コンパイルのしきい値の対象にならないため、自動再コンパイルは行われません。したがって、プランは1ではなく0のテーブルカーディナリティに基づいていますが、Paul White(@SQL_Kiwi)がこのdba.stackexchangeの回答で説明しているように、最小値は1に増加します。
この問題を通常回避する方法は、OPTION (RECOMPILE)
を追加することです。 テーブル変数を参照するクエリに対して、データが入力された後、オプティマイザにテーブル変数のカーディナリティを検査するように強制します。明示的な再コンパイルヒントを追加するためにすべてのクエリを手動で変更する必要をなくすために、SQL Server 2012 ServicePack2およびSQLServer2014累積更新#3に新しいトレースフラグ(2453)が導入されました:
- KB#2952444:修正:SQLServer2012またはSQLServer2014でテーブル変数を使用するとパフォーマンスが低下する
トレースフラグ2453がアクティブな場合、オプティマイザは、テーブル変数が作成された後、テーブルカーディナリティの正確な画像を取得できます。これは多くのクエリにとってAGoodThing™になる可能性がありますが、おそらくすべてではありません。OPTION (RECOMPILE)
とは異なる動作をすることに注意する必要があります。 。最も注目すべきは、この投稿でPaul Whiteが説明しているパラメータ埋め込みの最適化は、OPTION (RECOMPILE)
の下で発生することです。 、ただし、この新しいトレースフラグの下にはありません。
簡単なテスト
私の最初のテストは、テーブル変数にデータを入力してそこから選択することで構成されていました。これにより、非常に馴染みのある推定行数が1になりました。これが実行したテストです(比較するために再コンパイルのヒントを追加しました):
DBCC TRACEON(2453); DECLARE @t TABLE(id INT PRIMARY KEY, name SYSNAME NOT NULL UNIQUE); INSERT @t SELECT TOP (1000) [object_id], name FROM sys.all_objects; SELECT t.id, t.name FROM @t AS t; SELECT t.id, t.name FROM @t AS t OPTION (RECOMPILE); DBCC TRACEOFF(2453);
SQL Sentry Plan Explorerを使用すると、この場合の両方のクエリのグラフィカルプランが同一であることがわかります。おそらく、これは文字通り些細なプランであるためです。
@tに対する簡単なインデックススキャンのグラフィカルプラン>
ただし、見積もりは同じではありません。トレースフラグが有効になっている場合でも、再コンパイルのヒントを使用しない場合は、インデックススキャンから1の推定値が得られます。
トレースフラグ(左)と再コンパイル(右)の推定値の比較
直接私の周りにいたことがあれば、おそらくこの時点で私が作った顔を想像することができます。 KBの記事に間違ったトレースフラグ番号が記載されているか、それを実際にアクティブにするには他の設定を有効にする必要があると確信しました。
Benjamin Nevarez(@BenjaminNevarez)は、「SQL Server 2012 ServicePack2で修正されたバグ」KB記事を詳しく調べる必要があることをすぐに指摘しました。 [ハイライト]>[リレーショナルエンジン]の下にある非表示の箇条書きの背後にあるテキストを覆い隠していますが、修正リストの記事は、元の記事(強調私のもの)よりもトレースフラグの動作を説明するのに少し優れています:
テーブル変数したがって、この説明から、トレースフラグは、テーブル変数が結合に参加している場合にのみ問題に対処することを目的としているように見えます。 (元の記事でその区別がなされていない理由はわかりません。)しかし、クエリにもう少し作業を行わせても機能します。上記のクエリはオプティマイザによって簡単であると見なされ、トレースフラグは機能しません。その場合は何でもしようとさえします。ただし、結合がなくても、コストベースの最適化を実行すると開始されます。トレースフラグは、些細な計画には影響しません。参加を伴わない重要な計画の例を次に示します。
DBCC TRACEON(2453); DECLARE @t TABLE(id INT PRIMARY KEY, name SYSNAME NOT NULL UNIQUE); INSERT @t SELECT TOP (1000) [object_id], name FROM sys.all_objects; SELECT TOP (100) t.id, t.name FROM @t AS t ORDER BY NEWID(); SELECT TOP (100) t.id, t.name FROM @t AS t ORDER BY NEWID() OPTION (RECOMPILE); DBCC TRACEOFF(2453);
この計画はもはや些細なことではありません。最適化はフルとしてマークされます。コストの大部分は並べ替え演算子に移動されます:
そして、両方のクエリの見積もりが並んでいます(今回はツールチップを保存しますが、同じであることを保証できます):
したがって、KBの記事は正確ではないようです。結合を導入せずに、トレースフラグに期待される動作を強制することができました。しかし、私も参加してテストしたいと思います。
より良いテスト
トレースフラグがある場合とない場合のこの簡単な例を見てみましょう。
--DBCC TRACEON(2453); DECLARE @t TABLE(id INT PRIMARY KEY, name SYSNAME NOT NULL UNIQUE); INSERT @t SELECT TOP (1000) [object_id], name FROM sys.all_objects; SELECT t.name, c.name FROM @t AS t LEFT OUTER JOIN sys.all_columns AS c ON t.id = c.[object_id]; --DBCC TRACEOFF(2453);
トレースフラグがない場合、オプティマイザは、テーブル変数に対するインデックススキャンから1行が取得されると推定します。ただし、トレースフラグを有効にすると、1,000行が強打されます:
インデックススキャン推定値の比較(左側にトレースフラグなし、右側のトレースフラグ)
違いはそれだけではありません。よく見ると、オプティマイザーが行ったさまざまな決定がわかります。これらはすべて、これらのより適切な見積もりに基づいています。
計画の比較(左側にトレースフラグなし、トレースフラグ右側)
違いの簡単な要約:
- トレースフラグのないクエリは4,140の読み取り操作を実行しましたが、見積もりが改善されたクエリは424しか実行しませんでした(約90%の削減)。
- オプティマイザは、クエリ全体がトレースフラグなしで10行を返し、トレースフラグを使用するとはるかに正確な2,318行を返すと推定しました。
- トレースフラグがない場合、オプティマイザはネストされたループ結合を実行することを選択しました(これは、入力の1つが非常に小さいと推定される場合に意味があります)。これにより、連結演算子と両方のスキャンが1回だけ実行されるトレースフラグで選択されたハッシュ一致とは対照的に、連結演算子と両方のインデックスシークが1,000回実行されます。
- [テーブルI/O]タブには、1,000回のスキャン(インデックスシークを装った範囲スキャン)と、
syscolpars
に対するはるかに高い論理読み取りカウントも表示されます。 (sys.all_columns
の背後にあるシステムテーブル 。 - 期間はそれほど影響を受けませんでしたが(24ミリ秒対18ミリ秒)、これらの他の違いがより深刻なクエリに与える可能性のある影響の種類を想像できるでしょう。
- 図を推定コストに切り替えると、テーブル変数がトレースフラグなしでオプティマイザーをだますことができるかどうかがわかります。
推定行数の比較(左側にトレースフラグがない、トレース右側の旗)
オプティマイザが、関連するカーディナリティを正確に把握している場合に、適切なプランを選択する際により良い仕事をすることは明らかであり、ショックではありません。しかし、どのくらいの費用がかかりますか?
再コンパイルとオーバーヘッド
OPTION (RECOMPILE)
を使用する場合 上記のバッチでは、トレースフラグを有効にしないと、次のプランが得られます。これは、トレースフラグを使用したプランとほぼ同じです(推定行が2,318ではなく2,316であるという唯一の顕著な違い):
OPTION(RECOMPILE)と同じクエリ
したがって、これにより、トレースフラグが毎回再コンパイルをトリガーすることで、同様の結果が得られると思われる可能性があります。非常に単純な拡張イベントセッションを使用してこれを調査できます:
CREATE EVENT SESSION [CaptureRecompiles] ON SERVER ADD EVENT sqlserver.sql_statement_recompile ( ACTION(sqlserver.sql_text) ) ADD TARGET package0.asynchronous_file_target ( SET FILENAME = N'C:\temp\CaptureRecompiles.xel' ); GO ALTER EVENT SESSION [CaptureRecompiles] ON SERVER STATE = START;
次の一連のバッチを実行し、(a)再コンパイルオプションまたはトレースフラグなし、(b)再コンパイルオプション、および(c)セッションレベルのトレースフラグを使用して20個のクエリを実行しました。
/* default - no trace flag, no recompile */ DECLARE @t TABLE(id INT PRIMARY KEY, name SYSNAME NOT NULL UNIQUE); INSERT @t SELECT TOP (1000) [object_id], name FROM sys.all_objects; SELECT t.name, c.name FROM @t AS t LEFT OUTER JOIN sys.all_columns AS c ON t.id = c.[object_id]; GO 20 /* recompile */ DECLARE @t TABLE(id INT PRIMARY KEY, name SYSNAME NOT NULL UNIQUE); INSERT @t SELECT TOP (1000) [object_id], name FROM sys.all_objects; SELECT t.name, c.name FROM @t AS t LEFT OUTER JOIN sys.all_columns AS c ON t.id = c.[object_id] OPTION (RECOMPILE); GO 20 /* trace flag */ DBCC TRACEON(2453); DECLARE @t TABLE(id INT PRIMARY KEY, name SYSNAME NOT NULL UNIQUE); INSERT @t SELECT TOP (1000) [object_id], name FROM sys.all_objects; SELECT t.name, c.name FROM @t AS t LEFT OUTER JOIN sys.all_columns AS c ON t.id = c.[object_id]; DBCC TRACEOFF(2453); GO 20
次に、イベントデータを確認しました:
SELECT sql_text = LEFT(sql_text, 255), recompile_count = COUNT(*) FROM ( SELECT x.x.value(N'(event/action[@name="sql_text"]/value)[1]',N'nvarchar(max)') FROM sys.fn_xe_file_target_read_file(N'C:\temp\CaptureRecompiles*.xel',NULL,NULL,NULL) AS f CROSS APPLY (SELECT CONVERT(XML, f.event_data)) AS x(x) ) AS x(sql_text) GROUP BY LEFT(sql_text, 255);
結果は、標準クエリでは再コンパイルが行われなかったことを示しています。テーブル変数を参照するステートメントは一度再コンパイルされました。 トレースフラグの下で、ご想像のとおり、毎回 RECOMPILE
を使用 オプション:
sql_text | recompile_count |
---|---|
/*再コンパイル*/DECLARE @t TABLE(iINT… | 20 |
/*トレースフラグ*/DBCC TRACEON(2453);宣言@t… | 1 |
XEventsデータに対するクエリの結果
次に、拡張イベントセッションをオフにしてから、大規模に測定するようにバッチを変更しました。基本的に、コードはテーブル変数の作成と入力の1,000回の反復を測定し、3つの方法のそれぞれを使用して、その結果を#tempテーブルに選択します(その多くの使い捨て結果セットの出力を抑制する1つの方法)。
SET NOCOUNT ON; /* default - no trace flag, no recompile */ SELECT SYSDATETIME(); GO DECLARE @t TABLE(id INT PRIMARY KEY, name SYSNAME NOT NULL UNIQUE); INSERT @t SELECT TOP (1000) [object_id], name FROM sys.all_objects; SELECT t.id, c.name INTO #x FROM @t AS t LEFT OUTER JOIN sys.all_columns AS c ON t.id = c.[object_id]; DROP TABLE #x; GO 1000 SELECT SYSDATETIME(); GO /* recompile */ DECLARE @t TABLE(id INT PRIMARY KEY, name SYSNAME NOT NULL UNIQUE); INSERT @t SELECT TOP (1000) [object_id], name FROM sys.all_objects; SELECT t.id, c.name INTO #x FROM @t AS t LEFT OUTER JOIN sys.all_columns AS c ON t.id = c.[object_id] OPTION (RECOMPILE); DROP TABLE #x; GO 1000 SELECT SYSDATETIME(); GO /* trace flag */ DBCC TRACEON(2453); DECLARE @t TABLE(id INT PRIMARY KEY, name SYSNAME NOT NULL UNIQUE); INSERT @t SELECT TOP (1000) [object_id], name FROM sys.all_objects; SELECT t.id, c.name INTO #x FROM @t AS t LEFT OUTER JOIN sys.all_columns AS c ON t.id = c.[object_id]; DROP TABLE #x; DBCC TRACEOFF(2453); GO 1000 SELECT SYSDATETIME(); GO
このバッチを10回実行し、平均を取りました。彼らは:
メソッド | 平均継続時間 (ミリ秒) |
---|---|
デフォルト | 23,148.4 |
再コンパイル | 29,959.3 |
トレースフラグ | 22,100.7 |
1,000回の反復の平均期間
この場合、再コンパイルヒントを使用して毎回正しい推定値を取得するのはデフォルトの動作よりもはるかに遅くなりましたが、トレースフラグを使用する方がわずかに高速でした。これは理にかなっています。どちらの方法も、偽の見積もりを使用する(そして結果として悪い計画を取得する)デフォルトの動作を修正しますが、再コンパイルにはリソースが必要であり、より効率的な計画が得られない、または得られない場合は、全体的なバッチ期間に貢献します。
簡単そうに見えますが、待ってください…
上記のテストには、わずかに(そして意図的に)欠陥があります。同じ数の行(1,000)をテーブル変数に毎回挿入しています 。テーブル変数の初期母集団がバッチごとに異なる場合はどうなりますか?確かに、トレースフラグの下でも、再コンパイルが表示されますよね?別のテストの時間です。ターゲットファイル名を変えて(他のセッションのデータを混同しないように)、少し異なる拡張イベントセッションを設定しましょう:
CREATE EVENT SESSION [CaptureRecompiles_v2] ON SERVER ADD EVENT sqlserver.sql_statement_recompile ( ACTION(sqlserver.sql_text) ) ADD TARGET package0.asynchronous_file_target ( SET FILENAME = N'C:\temp\CaptureRecompiles_v2.xel' ); GO ALTER EVENT SESSION [CaptureRecompiles_v2] ON SERVER STATE = START;
次に、このバッチを調べて、大幅に異なる反復ごとの行数を設定しましょう。これを3回実行し、適切なコメントを削除して、トレースフラグまたは明示的な再コンパイルのないバッチ、トレースフラグのあるバッチ、およびOPTION (RECOMPILE)
のあるバッチを作成します。 (最初に正確なコメントがあると、拡張イベント出力などの場所でこれらのバッチを簡単に識別できます):
/* default, no trace flag or recompile */ /* recompile */ /* trace flag */ DECLARE @i INT = 1; WHILE @i <= 6 BEGIN --DBCC TRACEON(2453); -- uncomment this for trace flag DECLARE @t TABLE(id INT PRIMARY KEY); INSERT @t SELECT TOP (CASE @i WHEN 1 THEN 24 WHEN 2 THEN 1782 WHEN 3 THEN 1701 WHEN 4 THEN 12 WHEN 5 THEN 15 WHEN 6 THEN 1560 END) [object_id] FROM sys.all_objects; SELECT t.id, c.name FROM @t AS t INNER JOIN sys.all_objects AS c ON t.id = c.[object_id] --OPTION (RECOMPILE); -- uncomment this for recompile --DBCC TRACEOFF(2453); -- uncomment this for trace flag DELETE @t; SET @i += 1; END
これらのバッチをManagementStudioで実行し、プランエクスプローラーで個別に開き、SELECT
だけでステートメントツリーをフィルター処理しました。 クエリ。推定行と実際の行を見ると、3つのバッチで異なる動作を確認できます。
3つのバッチの比較、推定行と実際の行の比較
右端のグリッドでは、トレースフラグの下で再コンパイルが行われなかった場所を明確に確認できます。
XEventsデータをチェックして、再コンパイルで実際に何が起こったかを確認できます。
SELECT sql_text = LEFT(sql_text, 255), recompile_count = COUNT(*) FROM ( SELECT x.x.value(N'(event/action[@name="sql_text"]/value)[1]',N'nvarchar(max)') FROM sys.fn_xe_file_target_read_file(N'C:\temp\CaptureRecompiles_v2*.xel',NULL,NULL,NULL) AS f CROSS APPLY (SELECT CONVERT(XML, f.event_data)) AS x(x) ) AS x(sql_text) GROUP BY LEFT(sql_text, 255);
結果:
sql_text | recompile_count |
---|---|
/*再コンパイル*/DECLARE @i INT =1; WHILE… | 6 |
/*トレースフラグ*/DECLARE @i INT =1; WHILE… | 4 |
XEventsデータに対するクエリの結果
とても興味深い!トレースフラグの下では、再コンパイルが*表示されます*が、ランタイムパラメータ値がキャッシュされた値と大幅に異なる場合に限ります。実行時の値が異なるが、それほど大きくない場合、再コンパイルは行われず、同じ見積もりが使用されます。したがって、トレースフラグがテーブル変数に再コンパイルのしきい値を導入することは明らかであり、これがこの「古代」の#tempテーブルで説明されているものと同じアルゴリズムを使用していることを(別のテストを通じて)確認しました。これはフォローアップの投稿で証明します。
ここでも、パフォーマンスをテストし、バッチを1,000回実行し(拡張イベントセッションをオフにして)、期間を測定します。
メソッド | 平均継続時間 (ミリ秒) |
---|---|
デフォルト | 101,285.4 |
再コンパイル | 111,423.3 |
トレースフラグ | 110,318.2 |
1,000回の反復の平均期間
この特定のシナリオでは、毎回再コンパイルを強制するか、トレースフラグを使用することにより、パフォーマンスの約10%が失われます。デルタがどのように分布していたか正確にはわかりません:計画は有意にではなく、より適切な見積もりに基づいていました より良い?再コンパイルは、パフォーマンスの向上をその分相殺しましたか ?私はこれにあまり時間をかけたくありません、そしてそれは些細な例でした、しかしそれはオプティマイザーが働く方法で遊ぶことが予測できない事柄であるかもしれないことをあなたに示します。場合によっては、カーディナリティ=1のデフォルトの動作を使用したほうがよい場合があります。これは、過度の再コンパイルが発生しないことを知っているためです。トレースフラグが非常に意味をなすのは、同じデータセット(たとえば、郵便番号ルックアップテーブル)をテーブル変数に繰り返し入力するクエリがある場合、または常に50または1,000行を使用している(たとえば、入力する)場合です。ページネーションで使用するテーブル変数)。いずれの場合も、トレースフラグまたは明示的な再コンパイルを導入する予定のワークロードに対して、これが与える影響を確実にテストする必要があります。
TVPとテーブルタイプ
また、これがテーブルタイプにどのように影響するか、およびこの同じ症状が存在するTVPのカーディナリティに改善が見られるかどうかにも興味がありました。そこで、これまでに使用されていたテーブル変数を模倣した単純なテーブルタイプを作成しました。
USE MyTestDB; GO CREATE TYPE dbo.t AS TABLE ( id INT PRIMARY KEY );
次に、上記のバッチを取得して、DECLARE @t TABLE(id INT PRIMARY KEY);
を単純に置き換えました。 DECLARE @t dbo.t;
を使用 –他のすべてはまったく同じままでした。同じ3つのバッチを実行しましたが、これが私が見たものです:
デフォルトの動作、オプションの再コンパイル、トレースフラグの見積もりと実績の比較2453
そうです、トレースフラグはTVPとまったく同じように機能するようです。行数が再コンパイルのしきい値を超えると、再コンパイルによってオプティマイザーの新しい推定値が生成され、行数が「十分に近い」場合はスキップされます。
長所、短所、警告
トレースフラグの利点の1つは、一部を回避できることです。 再コンパイルしても、テーブルのカーディナリティが表示されます。テーブル変数の行数が安定していると予想される場合、またはカーディナリティの変化による大幅な計画の逸脱が見られない場合に限ります。もう1つは、グローバルまたはセッションレベルで有効にでき、すべてのクエリに再コンパイルのヒントを導入する必要がないことです。そして最後に、少なくともテーブル変数のカーディナリティが安定している場合、適切な見積もりにより、デフォルトよりもパフォーマンスが向上し、再コンパイルオプションを使用するよりもパフォーマンスが向上します。これらのコンパイルはすべて確実に加算されます。
もちろん、いくつかの欠点もあります。私が上で述べたものの1つは、OPTION (RECOMPILE)
と比較したものです。 パラメータの埋め込みなど、特定の最適化を見逃します。もう1つは、トレースフラグが些細な計画に期待する影響を与えないことです。そして、その過程で私が発見したのは、QUERYTRACEON
を使用していることです。 クエリレベルでトレースフラグを適用するためのヒントは機能しません。私が知る限り、オプティマイザが上記のカーディナリティを確認するには、テーブル変数またはTVPが作成および/または入力されるときに、トレースフラグを設定する必要があります。 1。
トレースフラグをグローバルに実行すると、テーブル変数を含むクエリにクエリプランが回帰する可能性があることに注意してください(これが、この機能が最初にトレースフラグの下で導入された理由です)。したがって、ワークロード全体をテストしてください。トレースフラグをどのように使用しても。また、この動作をテストするときは、ユーザーデータベースでテストしてください。コンテキストがtempdbに設定されている場合、通常発生すると予想される最適化と簡略化の一部は発生しないため、コードと設定をユーザーデータベースに移動すると、そこで観察される動作の一貫性が維持されない場合があります。
>結論
行数が多いが比較的一貫しているテーブル変数またはTVPを使用する場合、個々のクエリで手動で再コンパイルを強制せずに正確なテーブルカーディナリティを取得するために、特定のバッチまたはプロシージャに対してこのトレースフラグを有効にすると便利な場合があります。インスタンスレベルでトレースフラグを使用することもできます。これは、すべてのクエリに影響します。ただし、他の変更と同様に、いずれの場合も、ワークロード全体のパフォーマンスをテストし、リグレッションを明示的に監視し、テーブル変数の安定性を信頼できるため、トレースフラグの動作が必要であることを確認する必要があります。行数。
SQL Server 2014にトレースフラグが追加されているのを見てうれしいですが、それがデフォルトの動作になったほうがよいでしょう。大きな#tempテーブルよりも大きなテーブル変数を使用することに大きな利点があるわけではありませんが、より高いレベルで指定できるこれら2つの一時構造タイプ間のパリティが増えると便利です。パリティが多ければ多いほど、どちらを使用するかを検討する必要のある人は少なくなります(または、少なくとも選択する際に考慮すべき基準が少なくなります)。 Martin Smithは、dba.stackexchangeですばらしいQ&Aを行っていますが、これはおそらく更新の予定です。SQLServerの一時テーブルとテーブル変数の違いは何ですか?
重要な注意事項
SQL Server 2012 Service Pack 2をインストールする場合(このトレースフラグを使用するかどうかに関係なく)、まれなシナリオで導入される可能性のあるSQLServer2012および2014のリグレッションに関する私の投稿も参照してください。オンラインインデックスの再構築中に発生する可能性のあるデータの損失または破損。 SQL Server 2012 SP1とSP2、およびSQLServer2014で利用可能な累積的な更新プログラムがあります。2012RTMブランチの修正はありません。
さらなるテスト
私のリストには他にもテストするものがあります。 1つは、このトレースフラグがSQL Server 2014のインメモリテーブルタイプに影響を与えるかどうかを確認したいことです。また、トレースフラグ2453がテーブルに同じ再コンパイルしきい値を使用していることを疑いの余地なく証明します。 #tempテーブルの場合と同様に変数とTVP。