web-dev-qa-db-ja.com

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

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

10行のみでストアドプロシージャ(以下の定義)を実行すると、テーブル変数version outは一時テーブルバージョンを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:StmtRecompileeventsをトレースすると(以下のスクリーンショット)、これらのイベントは1回だけ発生します(#tempテーブルストアドプロシージャの最初の呼び出し時に)。他の9,999回の実行では、これらのイベントは発生しません。 (テーブル変数バージョンはこれらのイベントを取得しません)

Trace

ストアドプロシージャの最初の実行のわずかに大きなオーバーヘッドは、全体的な大きな違いを説明できません。ただし、プロシージャキャッシュをクリアして両方のプロシージャを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
37
Martin Smith

ディスコインフェルノ

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

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

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

テスト、テスト

2013バージョンのStack Overflow を使用して、このインデックスと次の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 ms
  • テーブル変数:5891 ms

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

NUTS

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

NUTS

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

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

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

テーブル '#B98CE339'。スキャンカウント1

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

NUTS

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';

NUTS

時差:

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

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

Perfviewのdiffを見ると、少し変わっています-sqlminはもうありません-sqllang!TCacheStore<CacheClockAlgorithm>::GetNextUserDataInHashBucketは違います。

NUTS

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;

NUTS

時差:

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

一時テーブルは約4秒速くなりました!それは何かです。

私は何かが好きです。

今回は、Perfviewの差分はそれほど興味深いものではありません。並んでいると、時間が全体でどれほど近いかを知るのは興味深いことです。

NUTS

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

NUTS

差分の上位2項目については、ntoskrnl!?をあまり活用できません。

NUTS

またはsqltses!CSqlSortManager_80::GetSortKeyですが、Smrtr Ppl™が参照するためにここにあります。

NUTS

文書化されていないため、本番環境では絶対に安全ではないため、使用しないでください スタートアップトレースフラグ 追加の一時テーブルシステムオブジェクト(sysrowsets、sysallocunits、sysseobjvalues)をインメモリ機能ですが、この場合、実行時間に大きな違いはありませんでした。

切り上げする

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

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

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

もちろん、かつて熟考されていたvaping sageとして、「プランの選択が問題ではないときにテーブル変数を使用する」です。

10
Erik Darling