Mar*_*ith 37 sql-server temporary-tables
我正在查看此处的文章 Temporary Tables vs. Table Variables and their Effect on SQL Server Performance and on SQL Server 2008 能够重现与 2005 中显示的结果类似的结果。
当执行只有 10 行的存储过程(定义如下)时,表变量 version out 执行临时表 version 的两倍以上。
我清除了过程缓存并运行了两个存储过程 10,000 次,然后再重复该过程 4 次。结果如下(每批时间以毫秒为单位)
T2_Time V2_Time
----------- -----------
8578 2718
6641 2781
6469 2813
6766 2797
6156 2719
Run Code Online (Sandbox Code Playgroud)
我的问题是:表变量版本性能更好的原因是什么?
我做了一些调查。例如查看性能计数器
SELECT cntr_value
from sys.dm_os_performance_counters
where counter_name = 'Temp Tables Creation Rate';
Run Code Online (Sandbox Code Playgroud)
确认在这两种情况下,临时对象都按预期在第一次运行后被缓存,而不是每次调用都从头开始创建。
类似地跟踪Profiler 中的Auto Stats, SP:Recompile,SQL:StmtRecompile事件(下面的屏幕截图)显示这些事件仅发生一次(在第一次调用#temp表存储过程时),其他 9,999 次执行不会引发任何这些事件。(表变量版本没有得到任何这些事件)

存储过程第一次运行的稍微大一点的开销并不能解释整体上的巨大差异,但是因为清除过程缓存并运行两个过程仍然只需要几毫秒,所以我不相信统计数据或重新编译可能是原因。
创建所需的数据库对象
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
Run Code Online (Sandbox Code Playgroud)
测试脚本
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
Run Code Online (Sandbox Code Playgroud)
Mar*_*ith 31
SET STATISTICS IO ON两者的输出看起来相似
SET STATISTICS IO ON;
PRINT 'V2'
EXEC dbo.V2 10
PRINT 'T2'
EXEC dbo.T2 10
Run Code Online (Sandbox Code Playgroud)
给
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
Run Code Online (Sandbox Code Playgroud)
正如 Aaron 在评论中指出的,表变量版本的计划实际上效率较低,因为两者都有一个由表版本上dbo.NUM的索引查找驱动的嵌套循环计划#temp,[#T].n = [dbo].[NUM].[n]使用剩余谓词执行对索引的查找,[#T].[n]<=[@total]而表变量version@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 |
+---------------------+------------+----------------+----------------+----------------+
Run Code Online (Sandbox Code Playgroud)
这些结果是执行 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 |
+---------------------+------------+----------------+----------------+----------------+
Run Code Online (Sandbox Code Playgroud)
所以很明显,表格案例中的PAGELATCH_SH等待次数要高得多#temp。我不知道有什么方法可以将等待资源添加到扩展事件跟踪中,因此为了进一步调查我跑了
WHILE 1=1
EXEC dbo.T2 10
Run Code Online (Sandbox Code Playgroud)
在另一个连接轮询中 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'
Run Code Online (Sandbox Code Playgroud)
在离开运行大约 15 秒后,它收集了以下结果
+-------+----------------------+
| Count | resource_description |
+-------+----------------------+
| 1098 | 2:1:150 |
| 1689 | 2:1:146 |
+-------+----------------------+
Run Code Online (Sandbox Code Playgroud)
被锁存的这两个页面都属于tempdb.sys.sysschobjs名为'nc1'and的基表上的(不同的)非聚集索引'nc2'。
tempdb.sys.fn_dblog运行期间的查询表明,每个存储过程的第一次执行添加的日志记录数量有些变化,但对于后续执行,每次迭代添加的数量非常一致且可预测。一旦程序计划被缓存,日志条目的数量大约是#temp版本所需数量的一半。
+-----------------+----------------+------------+
| | Table Variable | Temp Table |
+-----------------+----------------+------------+
| First Run | 126 | 72 or 136 |
| Subsequent Runs | 17 | 32 |
+-----------------+----------------+------------+
Run Code Online (Sandbox Code Playgroud)
更详细地查看#tempSP 表版本的事务日志条目,存储过程的每次后续调用都会创建三个事务,而表变量只有两个。
+---------------------------------+----+---------------------------------+----+
| #Temp Table | @Table Variable |
+---------------------------------+----+---------------------------------+----+
| CREATE TABLE | 9 | | |
| INSERT | 12 | TVQuery | 12 |
| FCheckAndCleanupCachedTempTable | 11 | FCheckAndCleanupCachedTempTable | 5 |
+---------------------------------+----+---------------------------------+----+
Run Code Online (Sandbox Code Playgroud)
该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 | |
+-----------------+-------------------+---------------------+
Run Code Online (Sandbox Code Playgroud)
该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 | |
+-----------------+-------------------+----------------------------------------------+
Run Code Online (Sandbox Code Playgroud)
查看两个事务中的这 6 行,它们对应于相同的操作。第一个LOP_MODIFY_ROW, LCX_CLUSTERED是更新 中的modify_date列sys.objects。其余五行都与对象重命名有关。因为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 '#%'
Run Code Online (Sandbox Code Playgroud)
示例结果

因此,对于 Alex 所暗示的观察到的性能差异的一个潜在解释是,维护系统表的这项额外工作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 |
+-------------------------------+--------------------+-------+-----------+
Run Code Online (Sandbox Code Playgroud)
表变量版本花费了大约 60% 的时间来执行插入语句和随后的选择,而临时表则不到一半。这与 OP 中显示的时间和上述结论一致,即性能差异归结为执行辅助工作所花费的时间,而不是查询执行本身所花费的时间。
导致临时表版本中“缺失”75% 的最重要的函数是
+------------------------------------+-------------------+
| Function | Inclusive Samples |
+------------------------------------+-------------------+
| CXStmtCreateTableDDL::XretExecute | 26.26% |
| CXStmtDDL::FinishNormalImp | 4.17% |
| TmpObject::Release | 27.77% |
+------------------------------------+-------------------+
| Total | 58.20% |
+------------------------------------+-------------------+
Run Code Online (Sandbox Code Playgroud)
在 create 和 release 函数下,函数CMEDProxyObject::SetName显示为包含样本值19.6%。从中我推断在临时表案例中 39.2% 的时间被前面描述的重命名占用了。
表变量版本中对其他 40% 贡献最大的是
+-----------------------------------+-------------------+
| Function | Inclusive Samples |
+-----------------------------------+-------------------+
| CTableCreate::LCreate | 7.41% |
| TmpObject::Release | 12.87% |
+-----------------------------------+-------------------+
| Total | 20.28% |
+-----------------------------------+-------------------+
Run Code Online (Sandbox Code Playgroud)
Eri*_*ing 10
由于这是一个较旧的问题,我决定在较新版本的 SQL Server 上重新审视这个问题,看看是否仍然存在相同的性能配置文件,或者特征是否发生了变化。
具体来说,为 SQL Server 2019添加内存系统表似乎是一个值得重新测试的机会。
我正在使用稍微不同的测试工具,因为我在处理其他事情时遇到了这个问题。
使用2013 版本的 Stack Overflow,我有这个索引和这两个程序:
指数:
CREATE INDEX ix_whatever
ON dbo.Posts(OwnerUserId) INCLUDE(Score);
GO
Run Code Online (Sandbox Code Playgroud)
温度表:
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
Run Code Online (Sandbox Code Playgroud)
表变量:
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
Run Code Online (Sandbox Code Playgroud)
为了防止任何潜在的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
Run Code Online (Sandbox Code Playgroud)
由于 2014 年和 2016 年在这一点上基本上是 RELICS,我从 2017 年开始我的测试。此外,为简洁起见,我直接跳到使用Perfview分析代码。在现实生活中,我查看了等待、闩锁、自旋锁、疯狂跟踪标志和其他东西。
分析代码是唯一可以揭示任何感兴趣的东西。
时间差异:
还是很明显的区别吧?但是现在 SQL Server 的目标是什么?
查看差异样本的前两个增长,我们看到sqlmin并且sqlsqllang!TCacheStore<CacheClockAlgorithm>::GetNextUserDataInHashBucket是两个最大的违规者。
从调用堆栈中的名称来看,清理和内部重命名临时表似乎是临时表调用与表变量调用中最大的时间。
尽管表变量在内部由临时表支持,但这似乎不是问题。
SET STATISTICS IO ON;
DECLARE @t TABLE(id INT);
SELECT * FROM @t AS t;
Run Code Online (Sandbox Code Playgroud)
表'#B98CE339'。扫描次数 1
查看表变量测试的调用堆栈根本没有显示任何一个主要违规者:
好的,所以这仍然是 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';
Run Code Online (Sandbox Code Playgroud)
时间差异:
两种程序都不同。临时表调用快了几秒钟,表变量调用慢了大约 1.5 秒。表变量变慢的部分原因可能是表变量延迟编译,这是 2019 年新的优化器选择。
查看 Perfview 中的差异,它发生了一些变化——sqlmin 不再存在——但是sqllang!TCacheStore<CacheClockAlgorithm>::GetNextUserDataInHashBucket存在。
这个新的内存系统表怎么样?嗯?吃那个?
让我们开启它!
EXEC sys.sp_configure @configname = 'advanced',
@configvalue = 1
RECONFIGURE;
EXEC sys.sp_configure @configname = 'tempdb metadata memory-optimized',
@configvalue = 1
RECONFIGURE;
Run Code Online (Sandbox Code Playgroud)
请注意,这需要重新启动 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;
Run Code Online (Sandbox Code Playgroud)
时间差异:
临时表的效果要好大约 4 秒!那是一些东西。
我喜欢某事。
这一次,Perfview 差异不是很有趣。并排,有趣的是注意到时间是多么接近:
差异中一个有趣的点是对 的调用hkengine!,这似乎很明显,因为现在正在使用类似 hekaton 的功能。
至于差异中的前两项,我不能做太多ntoskrnl!?:
或者sqltses!CSqlSortManager_80::GetSortKey,但他们在这里供 Smrtr Ppl™ 查看:
请注意,有一个未记录且绝对不安全的生产,因此请不要使用它启动跟踪标志,您可以使用它在内存功能中包含其他临时表系统对象(sysrowsets、sysallocunits 和 sysseobjvalues),但它在这种情况下,执行时间没有显着差异。
即使在较新版本的 SQL Server 中,对表变量的高频调用也比对临时表的高频调用快得多。
尽管人们很容易将责任归咎于编译、重新编译、自动统计、闩锁、自旋锁、缓存或其他问题,但问题显然仍然与管理临时表清理有关。
这是在启用内存系统表的 SQL Server 2019 中更接近的调用,但是当调用频率很高时,表变量仍然表现得更好。
当然,作为一个 vaping 的圣人曾经沉思:“当计划选择不是问题时使用表变量”。
| 归档时间: |
|
| 查看次数: |
14781 次 |
| 最近记录: |