SET STATISTICS IO ON
両方の出力は似ています
SET STATISTICS IO ON;
PRINT 'V2'
EXEC dbo.V2 10
PRINT 'T2'
EXEC dbo.T2 10
与える
V2
Table '#58B62A60'. Scan count 0, logical reads 20
Table 'NUM'. Scan count 1, logical reads 3
Table '#58B62A60'. Scan count 10, logical reads 20
Table 'NUM'. Scan count 1, logical reads 3
T2
Table '#T__ ... __00000000E2FE'. Scan count 0, logical reads 20
Table 'NUM'. Scan count 1, logical reads 3
Table '#T__ ... __00000000E2FE'. Scan count 0, logical reads 20
Table 'NUM'. Scan count 1, logical reads 3
そして、アーロンがコメントで指摘しているように、テーブル変数バージョンのプランは実際には効率的ではありませんがdbo.NUM
、#temp
テーブルバージョンのインデックスシークによって駆動されるネストされたループプランは、[#T].n = [dbo].[NUM].[n]
残りの述語でインデックスシークを実行[#T].[n]<=[@total]
しますが、テーブル変数はバージョンは、@V.n <= [@total]
残りの述部でインデックスシークを実行し、@V.[n]=[dbo].[NUM].[n]
そのためより多くの行を処理します(これは、このプランがより多くの行に対して非常にパフォーマンスが悪い理由です)
拡張イベントを使用して特定のspidの待機タイプを調べると、10,000回の実行に対してこれらの結果が得られます。EXEC dbo.T2 10
+---------------------+------------+----------------+----------------+----------------+
| | | Total | Total Resource | Total Signal |
| Wait Type | Wait Count | Wait Time (ms) | Wait Time (ms) | Wait Time (ms) |
+---------------------+------------+----------------+----------------+----------------+
| SOS_SCHEDULER_YIELD | 16 | 19 | 19 | 0 |
| PAGELATCH_SH | 39998 | 14 | 0 | 14 |
| PAGELATCH_EX | 1 | 0 | 0 | 0 |
+---------------------+------------+----------------+----------------+----------------+
これらの結果は、10,000回の実行に対して EXEC dbo.V2 10
+---------------------+------------+----------------+----------------+----------------+
| | | Total | Total Resource | Total Signal |
| Wait Type | Wait Count | Wait Time (ms) | Wait Time (ms) | Wait Time (ms) |
+---------------------+------------+----------------+----------------+----------------+
| PAGELATCH_EX | 2 | 0 | 0 | 0 |
| PAGELATCH_SH | 1 | 0 | 0 | 0 |
| SOS_SCHEDULER_YIELD | 676 | 0 | 0 | 0 |
+---------------------+------------+----------------+----------------+----------------+
したがってPAGELATCH_SH
、#temp
テーブルの場合、待機数がはるかに多いことは明らかです。拡張イベントトレースに待機リソースを追加する方法を知らないので、これをさらに調査するために実行しました
WHILE 1=1
EXEC dbo.T2 10
別の接続ポーリング中 sys.dm_os_waiting_tasks
CREATE TABLE #T(resource_description NVARCHAR(2048))
WHILE 1=1
INSERT INTO #T
SELECT resource_description
FROM sys.dm_os_waiting_tasks
WHERE session_id=<spid_of_other_session> and wait_type='PAGELATCH_SH'
約15秒間実行した後、次の結果が収集されました。
+-------+----------------------+
| Count | resource_description |
+-------+----------------------+
| 1098 | 2:1:150 |
| 1689 | 2:1:146 |
+-------+----------------------+
ラッチされるこれらのページは両方とも、およびtempdb.sys.sysschobjs
という名前のベーステーブルの(異なる)非クラスター化インデックスに属します。'nc1'
'nc2'
tempdb.sys.fn_dblog
実行中にクエリを実行すると、各ストアドプロシージャの最初の実行で追加されるログレコードの数は多少変動しますが、その後の実行では各反復で追加される数は非常に一貫性があり予測可能です。プロシージャプランがキャッシュされると、ログエントリの数は#temp
バージョンに必要な数の約半分になります。
+-----------------+----------------+------------+
| | Table Variable | Temp Table |
+-----------------+----------------+------------+
| First Run | 126 | 72 or 136 |
| Subsequent Runs | 17 | 32 |
+-----------------+----------------+------------+
#temp
SPのテーブルバージョンのトランザクションログエントリをより詳細に見ると、以降のストアドプロシージャの呼び出しごとに3つのトランザクションとテーブル変数が1つだけ作成されます。
+---------------------------------+----+---------------------------------+----+
| #Temp Table | @Table Variable |
+---------------------------------+----+---------------------------------+----+
| CREATE TABLE | 9 | | |
| INSERT | 12 | TVQuery | 12 |
| FCheckAndCleanupCachedTempTable | 11 | FCheckAndCleanupCachedTempTable | 5 |
+---------------------------------+----+---------------------------------+----+
INSERT
/ TVQUERY
トランザクションは、名前を除いて同一です。これには、一時テーブルまたはテーブル変数に挿入された10行ごとのログレコードとLOP_BEGIN_XACT
/ LOP_COMMIT_XACT
エントリが含まれます。
CREATE TABLE
取引のみに表示され#Temp
、次のようにバージョンとルックス。
+-----------------+-------------------+---------------------+
| Operation | Context | AllocUnitName |
+-----------------+-------------------+---------------------+
| LOP_BEGIN_XACT | LCX_NULL | |
| LOP_SHRINK_NOOP | LCX_NULL | |
| LOP_MODIFY_ROW | LCX_CLUSTERED | sys.sysschobjs.clst |
| LOP_DELETE_ROWS | LCX_MARK_AS_GHOST | sys.sysschobjs.nc1 |
| LOP_INSERT_ROWS | LCX_INDEX_LEAF | sys.sysschobjs.nc1 |
| LOP_DELETE_ROWS | LCX_MARK_AS_GHOST | sys.sysschobjs.nc2 |
| LOP_INSERT_ROWS | LCX_INDEX_LEAF | sys.sysschobjs.nc2 |
| LOP_MODIFY_ROW | LCX_CLUSTERED | sys.sysschobjs.clst |
| LOP_COMMIT_XACT | LCX_NULL | |
+-----------------+-------------------+---------------------+
FCheckAndCleanupCachedTempTable
トランザクションは、両方に表示されますが、中に6つの追加のエントリを持ってい#temp
たバージョン。これらは6行を参照してsys.sysschobjs
おり、上記とまったく同じパターンを持っています。
+-----------------+-------------------+----------------------------------------------+
| Operation | Context | AllocUnitName |
+-----------------+-------------------+----------------------------------------------+
| LOP_BEGIN_XACT | LCX_NULL | |
| LOP_DELETE_ROWS | LCX_NONSYS_SPLIT | dbo.#7240F239.PK__#T________3BD0199374293AAB |
| LOP_HOBT_DELTA | LCX_NULL | |
| LOP_HOBT_DELTA | LCX_NULL | |
| LOP_MODIFY_ROW | LCX_CLUSTERED | sys.sysschobjs.clst |
| LOP_DELETE_ROWS | LCX_MARK_AS_GHOST | sys.sysschobjs.nc1 |
| LOP_INSERT_ROWS | LCX_INDEX_LEAF | sys.sysschobjs.nc1 |
| LOP_DELETE_ROWS | LCX_MARK_AS_GHOST | sys.sysschobjs.nc2 |
| LOP_INSERT_ROWS | LCX_INDEX_LEAF | sys.sysschobjs.nc2 |
| LOP_MODIFY_ROW | LCX_CLUSTERED | sys.sysschobjs.clst |
| LOP_COMMIT_XACT | LCX_NULL | |
+-----------------+-------------------+----------------------------------------------+
両方のトランザクションでこれらの6行を見ると、同じ操作に対応しています。1つ目LOP_MODIFY_ROW, LCX_CLUSTERED
は、のmodify_date
列の更新sys.objects
です。残りの5行はすべて、オブジェクトの名前変更に関係しています。name
影響を受ける両方のNCI(nc1
およびnc2
)のキー列であるため、これはそれらの削除/挿入として実行され、クラスター化インデックスに戻って更新されます。
のためと思われる#temp
クリーンアップのストアドプロシージャが終了する部分がで行われ、テーブルのバージョンFCheckAndCleanupCachedTempTable
トランザクションのようなものから、一時テーブルの名前を変更することがある#T__________________________________________________________________________________________________________________00000000E316
ような別の内部名に#2F4A0079
、入力されたCREATE TABLE
トランザクションが戻ってそれを変更します。このフリップフロップ名は、ある接続dbo.T2
でループで実行され、別の接続で別の接続で実行されることで確認できます。
WHILE 1=1
SELECT name, object_id, create_date, modify_date
FROM tempdb.sys.objects
WHERE name LIKE '#%'
結果の例
したがって、Alexがほのめかしたように、観察されたパフォーマンスの違いの1つの潜在的な説明は、システムテーブルを維持するのはこの追加作業であるtempdb
ということです。
ループで両方の手順を実行すると、Visual Studio Codeプロファイラーは以下を明らかにします
+-------------------------------+--------------------+-------+-----------+
| Function | Explanation | Temp | Table Var |
+-------------------------------+--------------------+-------+-----------+
| CXStmtDML::XretExecute | Insert ... Select | 16.93 | 37.31 |
| CXStmtQuery::ErsqExecuteQuery | Select Max | 8.77 | 23.19 |
+-------------------------------+--------------------+-------+-----------+
| Total | | 25.7 | 60.5 |
+-------------------------------+--------------------+-------+-----------+
テーブル変数バージョンは、挿入テーブルとそれに続く選択を実行する時間の約60%を費やしますが、一時テーブルはその半分未満です。これは、OPに示されているタイミングと、パフォーマンスの違いは、クエリの実行自体に費やされた時間ではなく、補助的な作業に費やされた時間までであるという結論と一致しています。
一時テーブルバージョンの75%の「欠落」に寄与する最も重要な関数は次のとおりです。
+------------------------------------+-------------------+
| Function | Inclusive Samples |
+------------------------------------+-------------------+
| CXStmtCreateTableDDL::XretExecute | 26.26% |
| CXStmtDDL::FinishNormalImp | 4.17% |
| TmpObject::Release | 27.77% |
+------------------------------------+-------------------+
| Total | 58.20% |
+------------------------------------+-------------------+
create関数とrelease関数の両方の下で、関数CMEDProxyObject::SetName
はの包括的なサンプル値で表示され19.6%
ます。これから、一時テーブルの場合の39.2%の時間が、前述の名前変更に使用されると推測します。
そして、他の40%に寄与するテーブル変数バージョンの最大のものは
+-----------------------------------+-------------------+
| Function | Inclusive Samples |
+-----------------------------------+-------------------+
| CTableCreate::LCreate | 7.41% |
| TmpObject::Release | 12.87% |
+-----------------------------------+-------------------+
| Total | 20.28% |
+-----------------------------------+-------------------+
一時表プロファイル
テーブル変数プロファイル
#temp
テーブルの統計がクリアされ、その後9,999回再作成されたにもかかわらず、テーブル上で1回だけ統計が作成されることを示しています。