在这种情况下,为什么使用表变量的速度是#temp表的两倍?


37

我在这里查看的文章“ 临时表与表变量及其对SQL Server性能和SQL Server 2008的影响”能够重现与2005年类似的结果。

当仅执行10行存储过程(以下定义)时,表变量version out会执行临时表版本两次以上。

我清除了过程缓存并运行了两个存储过程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表的存储过程)和其他9999个执行不提高任何这些事件。(表变量版本未获得任何这些事件)

跟踪

第一次运行存储过程的开销稍大一点也不能解决总体差异,但是由于清除过程缓存并同时运行两个过程只需要花费几毫秒,因此我不相信统计信息还是重新编译可能是原因。

创建所需的数据库对象

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次。
马丁·史密斯

Answers:


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 的表版本的事务日志条目,每次随后对存储过程的调用都会创建三个事务,而表变量只有两个。

+---------------------------------+----+---------------------------------+----+
|           #Temp Table                |         @Table Variable              |
+---------------------------------+----+---------------------------------+----+
| CREATE TABLE                    |  9 |                                 |    |
| INSERT                          | 12 | TVQuery                         | 12 |
| FCheckAndCleanupCachedTempTable | 11 | FCheckAndCleanupCachedTempTable |  5 |
+---------------------------------+----+---------------------------------+----+

INSERT/ TVQUERY交易除了名字相同。它包含插入临时表或表变量以及LOP_BEGIN_XACT/ LOP_COMMIT_XACT条目的10行中每行的日志记录。

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行,它们对应于相同的操作。首先LOP_MODIFY_ROW, LCX_CLUSTERED是对中modify_date列的更新sys.objects。其余五行都与对象重命名有关。因为name是两个受影响的NCI(nc1nc2)的键列,所以将其作为对它们的删除/插入来执行,然后返回聚簇索引并对其进行更新。

对于#temp表版本,似乎存储过程结束时,由FCheckAndCleanupCachedTempTable事务执行的部分清理工作是将temp表从类似的#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所提到的观察到的性能差异的一种潜在解释tempdb是,负责维护系统表的这项额外工作是负责的。


通过循环运行两个过程,Visual Studio Code Profiler显示以下内容

+-------------------------------+--------------------+-------+-----------+
|           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%的时间来执行insert语句和后续的select操作,而临时表则不到一半。这与OP中显示的时间是一致的,并且上面的结论是,性能差异取决于执行辅助工作所花费的时间,而不是由于查询执行本身所花费的时间。

导致临时表版本中“丢失” 75%的最重要功能是

+------------------------------------+-------------------+
|              Function              | Inclusive Samples |
+------------------------------------+-------------------+
| CXStmtCreateTableDDL::XretExecute  | 26.26%            |
| CXStmtDDL::FinishNormalImp         | 4.17%             |
| TmpObject::Release                 | 27.77%            |
+------------------------------------+-------------------+
| Total                              | 58.20%            |
+------------------------------------+-------------------+

在创建和发布功能下,该功能均以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添加内存系统表似乎是值得重新测试的机会。

我使用的测试工具稍有不同,因为我在处理其他问题时遇到了这个问题。

测试,测试

使用2013版本的Stack Overflow,我有此索引以及以下两个过程:

指数:

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现在有什么用呢?

坚果

看着上方的显示差异的样品中,增加,我们看到的sqlminsqlsqllang!TCacheStore<CacheClockAlgorithm>::GetNextUserDataInHashBucket是两个最大的罪犯。

坚果

从调用堆栈中的名称来看,清理和内部重命名临时表似乎是与临时表调用相比花费最长时间的时间。

即使表变量在内部由临时表支持,这似乎也不是问题。

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

表'#B98CE339'。扫描计数1

浏览表变量test的调用堆栈根本不会显示出两个主要违规者:

坚果

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毫秒

两种程序都不同。临时表调用快了几秒钟,表变量调用慢了约1.5秒。表变量变慢的部分原因可能是表变量延迟编译(2019年新的优化程序选择)。

查看Perfview中的差异,它有所更改-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差异不是很有趣。并排,有趣的是注意到时间跨得很近:

坚果

差异中有趣的一点是对的调用hkengine!,这似乎很明显,因为现在已经使用了具有hekaton功能的功能。

坚果

至于差异中的前两项,我不能做太多事情ntoskrnl!?

坚果

sqltses!CSqlSortManager_80::GetSortKey,但他们在这里供Smrtr Ppl™查看:

坚果

请注意,这里有一个未记录的文件,绝对不安全,因此请不要使用它来启动跟踪标记,因为它可以用于在内存功能中包含其他临时表系统对象(sysrowsets,sysallocunits和sysseobjvalues),但是它在这种情况下,执行时间没有明显差异。

围捕

即使在较新版本的SQL Server中,高频调用表变量也比高频调用临时表快得多。

尽管很容易将责任归咎于编译,重新编译,自动统计,闩锁,自旋锁,缓存或其他问题,但问题显然仍然在于管理临时表清理。

在启用了内存系统表的SQL Server 2019中,这是一个更紧密的调用,但是当调用频率很高时,表变量仍然会表现更好。

当然,作为一位虔诚的圣贤沉思着:“在计划选择不成问题时使用表变量”。


很好-对不起,我想念您添加了一个答案,直到您点击“调试”博客文章中的链接
Martin Smith
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.