この特定の場合に、テーブル変数を#tempテーブルの2倍以上高速で使用するのはなぜですか?


37

ここで、一時テーブルとテーブル変数、およびSQL ServerのパフォーマンスとSQL Server 2008に対する影響についての記事を見て、 2005年に示された結果と同様の結果を再現することができました。

10行のみでストアドプロシージャ(以下の定義)を実行すると、テーブル変数バージョンは一時テーブルバージョンを2回以上実行します。

プロシージャキャッシュをクリアし、両方のストアドプロシージャを10,000回実行してから、さらに4回実行するプロセスを繰り返しました。以下の結果(バッチあたりのミリ秒単位の時間)

T2_Time     V2_Time
----------- -----------
8578        2718      
6641        2781    
6469        2813   
6766        2797
6156        2719

私の質問は次のとおりです。テーブル変数バージョンのパフォーマンスが向上する理由は何ですか?

調査を行いました。例:パフォーマンスカウンターを見る

SELECT cntr_value
from sys.dm_os_performance_counters
where counter_name = 'Temp Tables Creation Rate';

どちらの場合も、一時オブジェクトは、最初の実行後にキャッシュされていることを確認し、予想通り呼び出しごとではなく、再びゼロから作成しました。

同様に、トレースAuto StatsSP:RecompileSQL:StmtRecompileこれらのイベントのみ(の最初の起動時に一度発生することを示しているプロファイラのイベント(下のスクリーンショット)を#tempテーブルストアドプロシージャ)および他の9,999の実行は、これらのイベントのいずれかを上げていません。(テーブル変数バージョンはこれらのイベントを取得しません)

トレース

ストアドプロシージャの最初の実行のわずかに大きいオーバーヘッドは、全体的な大きな違いを説明することはできませんが、プロシージャキャッシュをクリアして両方のプロシージャを1回実行するのに数ミリ秒しかかからないため、統計または再コンパイルが原因である可能性があります。

必要なデータベースオブジェクトを作成する

CREATE DATABASE TESTDB_18Feb2012;

GO

USE TESTDB_18Feb2012;

CREATE TABLE NUM 
  ( 
     n INT PRIMARY KEY, 
     s VARCHAR(128) 
  ); 

WITH NUMS(N) 
     AS (SELECT TOP 1000000 ROW_NUMBER() OVER (ORDER BY $/0) 
         FROM   master..spt_values v1, 
                master..spt_values v2) 
INSERT INTO NUM 
SELECT N, 
       'Value: ' + CONVERT(VARCHAR, N) 
FROM   NUMS 

GO

CREATE PROCEDURE [dbo].[T2] @total INT 
AS 
  CREATE TABLE #T 
    ( 
       n INT PRIMARY KEY, 
       s VARCHAR(128) 
    ) 

  INSERT INTO #T 
  SELECT n, 
         s 
  FROM   NUM 
  WHERE  n%100 > 0 
         AND n <= @total 

  DECLARE @res VARCHAR(128) 

  SELECT @res = MAX(s) 
  FROM   NUM 
  WHERE  n <= @total 
         AND NOT EXISTS(SELECT * 
                        FROM   #T 
                        WHERE  #T.n = NUM.n) 
GO

CREATE PROCEDURE [dbo].[V2] @total INT 
AS 
  DECLARE @V TABLE ( 
    n INT PRIMARY KEY, 
    s VARCHAR(128)) 

  INSERT INTO @V 
  SELECT n, 
         s 
  FROM   NUM 
  WHERE  n%100 > 0 
         AND n <= @total 

  DECLARE @res VARCHAR(128) 

  SELECT @res = MAX(s) 
  FROM   NUM 
  WHERE  n <= @total 
         AND NOT EXISTS(SELECT * 
                        FROM   @V V 
                        WHERE  V.n = NUM.n) 


GO

テストスクリプト

SET NOCOUNT ON;

DECLARE @T1 DATETIME2,
        @T2 DATETIME2,
        @T3 DATETIME2,  
        @Counter INT = 0

SET @T1 = SYSDATETIME()

WHILE ( @Counter < 10000)
BEGIN
EXEC dbo.T2 10
SET @Counter += 1
END

SET @T2 = SYSDATETIME()
SET @Counter = 0

WHILE ( @Counter < 10000)
BEGIN
EXEC dbo.V2 10
SET @Counter += 1
END

SET @T3 = SYSDATETIME()

SELECT DATEDIFF(MILLISECOND,@T1,@T2) AS T2_Time,
       DATEDIFF(MILLISECOND,@T2,@T3) AS V2_Time

プロファイラトレースは、#tempテーブルの統計がクリアされ、その後9,999回再作成されたにもかかわらず、テーブル上で1回だけ統計が作成されることを示しています。
マーティンスミス

回答:


31

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         |
+-----------------+----------------+------------+

#tempSPのテーブルバージョンのトランザクションログエントリをより詳細に見ると、以降のストアドプロシージャの呼び出しごとに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%            |
+-----------------------------------+-------------------+

一時表プロファイル

ここに画像の説明を入力してください

テーブル変数プロファイル

ここに画像の説明を入力してください


10

ディスコ・インフェルノ

これは古い質問であるため、同じパフォーマンスプロファイルがまだ存在するかどうか、または特性がまったく変わっていないかどうかを確認するために、SQL Serverの新しいバージョンで問題を再検討することにしました。

具体的には、SQL Server 2019のインメモリシステムテーブルの追加は再テストする価値のある機会のようです。

別の作業をしているときにこの問題に遭遇したため、私は少し異なるテストハーネスを使用しています。

テスト、テスト

Stack Overflowの2013バージョンを使用するこのインデックスと次の2つの手順があります。

インデックス:

CREATE INDEX ix_whatever 
    ON dbo.Posts(OwnerUserId) INCLUDE(Score);
GO

一時テーブル:

    CREATE OR ALTER PROCEDURE dbo.TempTableTest(@Id INT)
    AS
    BEGIN
    SET NOCOUNT ON;

        CREATE TABLE #t(i INT NOT NULL);
        DECLARE @i INT;

        INSERT #t ( i )
        SELECT p.Score
        FROM dbo.Posts AS p
        WHERE p.OwnerUserId = @Id;

        SELECT @i = AVG(t.i)
        FROM #t AS t;

    END;
    GO 

テーブル変数:

    CREATE OR ALTER PROCEDURE dbo.TableVariableTest(@Id INT)
    AS
    BEGIN
    SET NOCOUNT ON;

        DECLARE @t TABLE (i INT NOT NULL);
        DECLARE @i INT;

        INSERT @t ( i )
        SELECT p.Score
        FROM dbo.Posts AS p
        WHERE p.OwnerUserId = @Id;

        SELECT @i = AVG(t.i)
        FROM @t AS t;

    END;
    GO 

潜在的なASYNC_NETWORK_IO待機を防ぐために、ラッパープロシージャを使用しています。

CREATE PROCEDURE #TT AS
SET NOCOUNT ON;
    DECLARE @i INT = 1;
    DECLARE @StartDate DATETIME2(7) = SYSDATETIME();

    WHILE @i <= 50000
        BEGIN
            EXEC dbo.TempTableTest @Id = @i;
            SET @i += 1;
        END;
    SELECT DATEDIFF(MILLISECOND, @StartDate, SYSDATETIME()) AS [ElapsedTimeMilliseconds];
GO

CREATE PROCEDURE #TV AS
SET NOCOUNT ON;
    DECLARE @i INT = 1;
    DECLARE @StartDate DATETIME2(7) = SYSDATETIME();

    WHILE @i <= 50000
        BEGIN
            EXEC dbo.TableVariableTest @Id = @i;
            SET @i += 1;
        END;
    SELECT DATEDIFF(MILLISECOND, @StartDate, SYSDATETIME()) AS [ElapsedTimeMilliseconds];
GO

SQL Server 2017

2014年と2016年は基本的にこの時点ではRELICSであるため、2017年からテストを開始します。また、簡潔にするために、Perfviewを使用してコードのプロファイリングを開始します。実際には、待機、ラッチ、スピンロック、クレイジートレースフラグなどを調べました。

コードのプロファイリングは、興味のあるものを明らかにした唯一のことです。

時差:

  • 一時テーブル:17891ミリ秒
  • テーブル変数:5891 ms

まだ非常に明確な違いですか?しかし、SQL Serverは今何をしているのでしょうか?

ナッツ

差分サンプルの上位2つの増加を見るsqlminsqlsqllang!TCacheStore<CacheClockAlgorithm>::GetNextUserDataInHashBucket、2つの最大の違反者であることがわかります。

ナッツ

呼び出しスタックの名前から判断すると、一時テーブルのクリーンアップと内部での名前の変更は、一時テーブルの呼び出しとテーブル変数の呼び出しで最も時間がかかるようです。

テーブル変数は一時テーブルによって内部的にサポートされていますが、これは問題ではないようです。

SET STATISTICS IO ON;
DECLARE @t TABLE(id INT);
SELECT * FROM @t AS t;

テーブル「#B98CE339」。スキャン数1

テーブル変数テストの呼び出しスタックを調べても、主な違反者はまったく表示されません。

ナッツ

SQL Server 2019(バニラ)

さて、これはまだSQL Server 2017の問題ですが、2019ですぐに使用できるものはありますか?

まず、私の袖に何もないことを示すために:

SELECT c.name,
       c.value_in_use,
       c.description
FROM sys.configurations AS c
WHERE c.name = 'tempdb metadata memory-optimized';

ナッツ

時差:

  • 一時テーブル:15765ミリ秒
  • テーブル変数:7250 ms

両方の手順は異なっていました。一時テーブルの呼び出しは数秒速く、テーブル変数の呼び出しは約1.5秒遅くなりました。テーブル変数のスローダウンは、2019年の新しいオプティマイザーの選択であるテーブル変数遅延コンパイルによって部分的に説明される場合があります。

Perfviewのdiffを見ると、少し変更されています-sqlminはもう存在していませんが、変更されていますsqllang!TCacheStore<CacheClockAlgorithm>::GetNextUserDataInHashBucket

ナッツ

SQL Server 2019(インメモリTempdbシステムテーブル)

メモリシステムテーブルのこの新しいものはどうですか?ん?それでサップ?

オンにしましょう!

EXEC sys.sp_configure @configname = 'advanced', 
                      @configvalue = 1  
RECONFIGURE;

EXEC sys.sp_configure @configname = 'tempdb metadata memory-optimized', 
                      @configvalue = 1 
RECONFIGURE;

これを開始するにはSQL Serverを再起動する必要があるため、この素敵な金曜日の午後にSQLを再起動するまでご容赦ください。

今、物事は異なって見えます:

SELECT c.name,
       c.value_in_use,
       c.description
FROM sys.configurations AS c
WHERE c.name = 'tempdb metadata memory-optimized';

SELECT *, 
       OBJECT_NAME(object_id) AS object_name, 
       @@VERSION AS sql_server_version
FROM tempdb.sys.memory_optimized_tables_internal_attributes;

ナッツ

時差:

  • 一時テーブル:11638ミリ秒
  • テーブル変数:7403 ms

一時テーブルのパフォーマンスは約4秒向上しました!それは何かです。

私は何かが好きです。

今回は、Perfview diffはあまり興味深いものではありません。並んで、時間は全体的にどれくらい近いかに注目するのは興味深いです:

ナッツ

diffの興味深い点の1つは、への呼び出しhkengine!です。これは、ヘカトンのような機能が現在使用されているため、明白に思えるかもしれません。

ナッツ

差分の上位2つの項目に関しては、次のことはできませんntoskrnl!?

ナッツ

またはsqltses!CSqlSortManager_80::GetSortKey、しかし、彼らはSmrtr Ppl™が見るためにここにいます:

ナッツ

文書化されていないため、実稼働環境では安全ではないことに注意してください。メモリ内機能に追加の一時テーブルシステムオブジェクト(sysrowsets、sysallocunits、sysseobjvalues)を含めるために使用できる起動トレースフラグは使用しないでください。この場合、実行時間に顕著な違いはありませんでした。

切り上げする

SQL Serverの新しいバージョンでも、テーブル変数の高頻度呼び出しは、一時テーブルの高頻度呼び出しよりもはるかに高速です。

コンパイル、再コンパイル、自動統計、ラッチ、スピンロック、キャッシュ、またはその他の問題を非難するのは魅力的ですが、問題は明らかに一時テーブルのクリーンアップの管理に関するものです。

インメモリシステムテーブルが有効になっているSQL Server 2019では、より近い呼び出しですが、呼び出し頻度が高い場合でもテーブル変数のパフォーマンスは向上します。

もちろん、気が狂ったように、「計画の選択が問題にならない場合はテーブル変数を使用してください」。


ニース-申し訳ありませんが、「デバッグ」ブログ投稿のリンクをたどるまで、これに対する回答を追加できなかったことを残念に思います
Martin Smith
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.