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 的表版本的事务日志条目,每次随后对存储过程的调用都会创建三个事务,而表变量只有两个。
+---------------------------------+----+---------------------------------+----+
| #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(nc1
和nc2
)的键列,所以将其作为对它们的删除/插入来执行,然后返回聚簇索引并对其进行更新。
对于#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% |
+-----------------------------------+-------------------+
临时表配置文件
表变量配置文件
#temp
尽管清除了统计信息,但仅在该表上创建了一次统计信息,此后又重新填充了9,999次。