这似乎是一个有很多神话和相互矛盾的观点的领域。
那么 SQL Server 中的表变量和本地临时表有什么区别呢?
内容
警告
这个答案讨论了 SQL Server 2000 中引入的“经典”表变量。内存 OLTP 中的 SQL Server 2014 引入了内存优化表类型。这些表变量实例在许多方面与下面讨论的不同!
存储位置
没有不同。两者都存储在tempdb中。
我已经看到它表明对于表变量,情况并非总是如此,但这可以从下面验证
DECLARE @T TABLE(X INT) INSERT INTO @T VALUES(1),(2) SELECT sys.fn_PhysLocFormatter(%%physloc%%) AS [File:Page:Slot] FROM @T
示例结果(在tempdb中显示存储 2 行的位置)
File:Page:Slot ---------------- (1:148:0) (1:148:1)
逻辑位置
与表相比,表变量的行为更像是当前数据库的一部分#temp。对于表变量(自 2005 年以来),如果未明确指定列排序规则,则将是当前数据库的列排序规则,而对于#temp表,它将使用tempdb的默认排序规则(更多详细信息)。
#temp
用户定义的数据类型和 XML 集合必须在tempdb中才能用于#temp表和表变量 ( Source )。表的tempdb中必须存在用户定义的别名类型,但表变量使用上下文数据库。#temp
SQL Server 2012 引入了包含的数据库。这些临时表的行为不同(h/t Aaron)
在包含的数据库中,临时表数据在包含的数据库的排序规则中进行排序。 与临时表关联的所有元数据(例如,表和列名、索引等)都将在目录排序规则中。 命名约束不能在临时表中使用。 临时表可能不引用用户定义的类型、XML 模式集合或用户定义的函数。
在包含的数据库中,临时表数据在包含的数据库的排序规则中进行排序。
不同范围的可见性
表变量只能在声明它们的批处理和范围内访问。#temp可以在子批次(嵌套触发器、过程、exec调用)中访问表。#temp在外部范围 ( @@NESTLEVEL=0) 创建的表也可以跨越批次,因为它们会一直持续到会话结束。两种类型的对象都不能在子批处理中创建并在调用范围内访问,但是如下所述(尽管可以##temp是全局表)。
exec
@@NESTLEVEL=0
##temp
寿命
DECLARE @.. TABLE执行包含语句的批处理时(在该批处理中的任何用户代码运行之前)隐式创建表变量,并在最后隐式删除。
DECLARE @.. TABLE
尽管解析器不允许您在DECLARE语句之前尝试使用表变量,但可以在下面看到隐式创建。
DECLARE
IF (1 = 0) BEGIN DECLARE @T TABLE(X INT) END --Works fine SELECT * FROM @T
#temp表在CREATE TABLE遇到 TSQL 语句时显式创建,并且可以在批处理结束时显式删除DROP TABLE或将在批处理结束时隐式删除(如果在子批处理中创建@@NESTLEVEL > 0)或会话结束时以其他方式结束。
CREATE TABLE
DROP TABLE
@@NESTLEVEL > 0
注意:在存储例程中,两种类型的对象都可以被缓存,而不是重复创建和删除新表。对于何时可以发生这种缓存有一些限制,但是对于#temp表可能会违反这些限制,但对表变量的限制无论如何都会阻止这些限制。#temp缓存表的维护开销略大于表变量
对象元数据
这对于两种类型的对象基本上是相同的。它存储在tempdb的系统基表中。#temp但是,对于表 来说,查看起来更直接,因为OBJECT_ID('tempdb..#T')它可以用于键入系统表,并且内部生成的名称与CREATE TABLE语句中定义的名称更密切相关。对于表变量,该object_id函数不起作用,内部名称完全是系统生成的,与变量名称无关(名称是对象 id 的十六进制形式)。
OBJECT_ID('tempdb..#T')
object_id
下面演示了元数据仍然存在,但是通过键入(希望是唯一的)列名。DBCC PAGE对于没有唯一列名的表,只要它们不为空,就可以使用 object_id 来确定它们。
DBCC PAGE
/*Declare a table variable with some unusual options.*/ DECLARE @T TABLE ( [dba.se] INT IDENTITY PRIMARY KEY NONCLUSTERED, A INT CHECK (A > 0), B INT DEFAULT 1, InRowFiller char(1000) DEFAULT REPLICATE('A',1000), OffRowFiller varchar(8000) DEFAULT REPLICATE('B',8000), LOBFiller varchar(max) DEFAULT REPLICATE(cast('C' as varchar(max)),10000), UNIQUE CLUSTERED (A,B) WITH (FILLFACTOR = 80, IGNORE_DUP_KEY = ON, DATA_COMPRESSION = PAGE, ALLOW_ROW_LOCKS=ON, ALLOW_PAGE_LOCKS=ON) ) INSERT INTO @T (A) VALUES (1),(1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),(12),(13) SELECT t.object_id, t.name, p.rows, a.type_desc, a.total_pages, a.used_pages, a.data_pages, p.data_compression_desc FROM tempdb.sys.partitions AS p INNER JOIN tempdb.sys.system_internals_allocation_units AS a ON p.hobt_id = a.container_id INNER JOIN tempdb.sys.tables AS t ON t.object_id = p.object_id INNER JOIN tempdb.sys.columns AS c ON c.object_id = p.object_id WHERE c.name = 'dba.se'
输出
Duplicate key was ignored.
在 SQL Server 2012 之前,临时表和表变量的对象 ID 为正数。从 SQL Server 2012 开始,临时表和表变量的对象 ID 始终为负数(设置高位)。
交易
对表变量的操作作为系统事务执行,独立于任何外部用户事务,而等效的#temp表操作将作为用户事务本身的一部分执行。由于这个原因,一个ROLLBACK命令会影响一个#temp表,但不会改变一个表变量。
ROLLBACK
DECLARE @T TABLE(X INT) CREATE TABLE #T(X INT) BEGIN TRAN INSERT #T OUTPUT INSERTED.X INTO @T VALUES(1),(2),(3) /*Both have 3 rows*/ SELECT * FROM #T SELECT * FROM @T ROLLBACK /*Only table variable now has rows*/ SELECT * FROM #T SELECT * FROM @T DROP TABLE #T
日志记录
tempdb两者都为事务日志生成日志记录。一个常见的误解是表变量并非如此,因此下面的脚本演示了这一点,它声明了一个表变量,添加了几行,然后更新它们并删除它们。
tempdb
因为表变量是在批处理开始和结束时隐式创建和删除的,所以需要使用多个批处理才能查看完整的日志记录。
USE tempdb; /* Don't run this on a busy server. Ideally should be no concurrent activity at all */ CHECKPOINT; GO /* The 2nd column is binary to allow easier correlation with log output shown later*/ DECLARE @T TABLE ([C71ACF0B-47E9-4CAD-9A1E-0C687A8F9CF3] INT, B BINARY(10)) INSERT INTO @T VALUES (1, 0x41414141414141414141), (2, 0x41414141414141414141) UPDATE @T SET B = 0x42424242424242424242 DELETE FROM @T /*Put allocation_unit_id into CONTEXT_INFO to access in next batch*/ DECLARE @allocId BIGINT, @Context_Info VARBINARY(128) SELECT @Context_Info = allocation_unit_id, @allocId = a.allocation_unit_id FROM sys.system_internals_allocation_units a INNER JOIN sys.partitions p ON p.hobt_id = a.container_id INNER JOIN sys.columns c ON c.object_id = p.object_id WHERE ( c.name = 'C71ACF0B-47E9-4CAD-9A1E-0C687A8F9CF3' ) SET CONTEXT_INFO @Context_Info /*Check log for records related to modifications of table variable itself*/ SELECT Operation, Context, AllocUnitName, [RowLog Contents 0], [Log Record Length] FROM fn_dblog(NULL, NULL) WHERE AllocUnitId = @allocId GO /*Check total log usage including updates against system tables*/ DECLARE @allocId BIGINT = CAST(CONTEXT_INFO() AS BINARY(8)); WITH T AS (SELECT Operation, Context, CASE WHEN AllocUnitId = @allocId THEN 'Table Variable' WHEN AllocUnitName LIKE 'sys.%' THEN 'System Base Table' ELSE AllocUnitName END AS AllocUnitName, [Log Record Length] FROM fn_dblog(NULL, NULL) AS D) SELECT Operation = CASE WHEN GROUPING(Operation) = 1 THEN 'Total' ELSE Operation END, Context, AllocUnitName, [Size in Bytes] = COALESCE(SUM([Log Record Length]), 0), Cnt = COUNT(*) FROM T GROUP BY GROUPING SETS( ( Operation, Context, AllocUnitName ), ( ) ) ORDER BY GROUPING(Operation), AllocUnitName
退货
详细视图
摘要视图(包括隐式删除和系统基表的日志记录)
据我所知,两者上的操作会产生大致相等的日志记录。
虽然日志记录的数量非常相似,但一个重要的区别是与#temp表相关的日志记录在任何包含用户事务完成之前不能被清除,因此在某些时候写入#temp表的长时间运行的事务将阻止日志截断,tempdb而自治事务不会为表变量生成。
表变量不支持TRUNCATE,因此当要求从表中删除所有行时,可能会处于日志记录的劣势(尽管对于非常小的表DELETE 无论如何都可以更好地工作
TRUNCATE
DELETE
基数
许多涉及表变量的执行计划将显示单行估计为它们的输出。检查表变量属性表明 SQL Server 认为表变量有零行(Paul White 解释了为什么它估计会从零行表中发出一行)。
但是,上一节中显示的结果确实显示了准确的rows计数sys.partitions。问题是在大多数情况下,引用表变量的语句是在表为空时编译的。如果在填充表变量后(重新)编译语句,则当前行数将用于表基数(这可能是由于显式recompile或可能因为语句还引用了另一个导致延迟编译的对象或重新编译。)
rows
sys.partitions
recompile
DECLARE @T TABLE(I INT); INSERT INTO @T VALUES(1),(2),(3),(4),(5) CREATE TABLE #T(I INT) /*Reference to #T means this statement is subject to deferred compile*/ SELECT * FROM @T WHERE NOT EXISTS(SELECT * FROM #T) DROP TABLE #T
计划显示延迟编译后的准确估计行数。
在 SQL Server 2012 SP2 中,引入了跟踪标志 2453。更多详细信息在此处的“关系引擎”下。
当启用此跟踪标志时,它可能会导致自动重新编译以考虑更改的基数,正如稍后将进一步讨论的那样。
注意:在兼容级别 150 的 Azure 上,该语句的编译现在推迟到第一次执行。这意味着它将不再受到零行估计问题的影响。
无列统计
然而,拥有更准确的表基数并不意味着估计的行数会更准确(除非对表中的所有行进行操作)。SQL Server 根本不维护表变量的列统计信息,因此将依赖于基于比较谓词的猜测(例如,=针对非唯一列将返回 10% 的表,或针对>比较返回 30% 的表)。相反,为表维护列统计信息#temp。
=
>
SQL Server 维护对每列所做的修改次数的计数。如果自编译计划以来的修改次数超过重新编译阈值 (RT),则将重新编译计划并更新统计信息。RT 取决于表类型和大小。
来自SQL Server 2008 中的计划缓存
RT 计算如下。(n 指编译查询计划时表的基数。) 常设桌 如果 n <= 500,则 RT = 500。 如果 n > 500,则 RT = 500 + 0.20 * n。 临时表 如果 n < 6,则 RT = 6。 如果 6 <= n <= 500,则 RT = 500。 如果 n > 500,则 RT = 500 + 0.20 * n。 表变量 RT 不存在。因此,不会因为表变量的基数变化而发生重新编译。 (但请参阅下面关于 TF 2453 的说明)
RT 计算如下。(n 指编译查询计划时表的基数。)
常设桌
临时表
该KEEP PLAN提示可用于将表的 RT 设置为#temp与永久表相同。
KEEP PLAN
所有这一切的最终结果是,#temp当涉及许多行时,为表生成的执行计划通常比为表变量生成的执行计划好几个数量级,因为 SQL Server 有更好的信息可以使用。
NB1:表变量没有统计信息,但仍会在跟踪标志 2453 下引发“统计信息已更改”重新编译事件(不适用于“琐碎”计划)这似乎发生在与上述临时表相同的重新编译阈值下额外的一个 if N=0 -> RT = 1。即当表变量为空时编译的所有语句最终将在非空时第一次执行时重新编译并更正TableCardinality 。编译时间表基数存储在计划中,如果语句以相同的基数再次执行(由于控制语句的流或缓存计划的重用),则不会发生重新编译。
N=0 -> RT = 1
NB2:对于存储过程中的缓存临时表,重新编译的故事比上面描述的要复杂得多。有关所有详细信息,请参阅存储过程中的临时表。
重新编译
除了上面描述的基于修改的重新编译之外,#temp表还可以与其他编译相关联,因为它们允许对触发编译的表变量进行禁止的操作(例如 DDL 更改CREATE INDEX,ALTER TABLE)
CREATE INDEX
ALTER TABLE
锁定
已经声明表变量不参与锁定。不是这种情况。将以下输出运行到 SSMS 消息选项卡,以获取为插入语句获取和释放的锁的详细信息。
DECLARE @tv_target TABLE (c11 int, c22 char(100)) DBCC TRACEON(1200,-1,3604) INSERT INTO @tv_target (c11, c22) VALUES (1, REPLICATE('A',100)), (2, REPLICATE('A',100)) DBCC TRACEOFF(1200,-1,3604)
对于SELECT来自表变量的查询,Paul White 在注释中指出这些自动带有隐式NOLOCK提示。如下所示
SELECT
NOLOCK
DECLARE @T TABLE(X INT); SELECT X FROM @T OPTION (RECOMPILE, QUERYTRACEON 3604, QUERYTRACEON 8607)
*** Output Tree: (trivial plan) *** PhyOp_TableScan TBL: @T Bmk ( Bmk1000) IsRow: COL: IsBaseRow1002 Hints( NOLOCK )
然而,这对锁定的影响可能很小。
SET NOCOUNT ON; CREATE TABLE #T( [ID] [int] IDENTITY NOT NULL, [Filler] [char](8000) NULL, PRIMARY KEY CLUSTERED ([ID] DESC)) DECLARE @T TABLE ( [ID] [int] IDENTITY NOT NULL, [Filler] [char](8000) NULL, PRIMARY KEY CLUSTERED ([ID] DESC)) DECLARE @I INT = 0 WHILE (@I < 10000) BEGIN INSERT INTO #T DEFAULT VALUES INSERT INTO @T DEFAULT VALUES SET @I += 1 END /*Run once so compilation output doesn't appear in lock output*/ EXEC('SELECT *, sys.fn_PhysLocFormatter(%%physloc%%) FROM #T') DBCC TRACEON(1200,3604,-1) SELECT *, sys.fn_PhysLocFormatter(%%physloc%%) FROM @T PRINT '--*--' EXEC('SELECT *, sys.fn_PhysLocFormatter(%%physloc%%) FROM #T') DBCC TRACEOFF(1200,3604,-1) DROP TABLE #T
这些返回结果都不是索引键顺序,表明 SQL Server 对两者都使用了分配顺序扫描。
我运行了上述脚本两次,第二次运行的结果如下
Process 58 acquiring Sch-S lock on OBJECT: 2:-1325894110:0 (class bit0 ref1) result: OK --*-- Process 58 acquiring IS lock on OBJECT: 2:-1293893996:0 (class bit0 ref1) result: OK Process 58 acquiring S lock on OBJECT: 2:-1293893996:0 (class bit0 ref1) result: OK Process 58 releasing lock on OBJECT: 2:-1293893996:0
表变量的锁定输出确实非常少,因为 SQL Server 只是在对象上获取了架构稳定性锁。但是对于一个#temp表来说,它几乎和它一样轻,因为它取出了一个对象级S锁。当然,在使用表时也可以显式指定NOLOCK提示或隔离级别。READ UNCOMMITTED``#temp
S
READ UNCOMMITTED``#temp
与记录周围用户事务的问题类似,这可能意味着表的锁定时间更长#temp。使用下面的脚本
--BEGIN TRAN; CREATE TABLE #T (X INT,Y CHAR(4000) NULL); INSERT INTO #T (X) VALUES(1) SELECT CASE resource_type WHEN 'OBJECT' THEN OBJECT_NAME(resource_associated_entity_id, 2) WHEN 'ALLOCATION_UNIT' THEN (SELECT OBJECT_NAME(object_id, 2) FROM tempdb.sys.allocation_units a JOIN tempdb.sys.partitions p ON a.container_id = p.hobt_id WHERE a.allocation_unit_id = resource_associated_entity_id) WHEN 'DATABASE' THEN DB_NAME(resource_database_id) ELSE (SELECT OBJECT_NAME(object_id, 2) FROM tempdb.sys.partitions WHERE partition_id = resource_associated_entity_id) END AS object_name, * FROM sys.dm_tran_locks WHERE request_session_id = @@SPID DROP TABLE #T -- ROLLBACK
对于这两种情况,当在显式用户事务之外运行时,检查时返回的唯一锁sys.dm_tran_locks是DATABASE.
sys.dm_tran_locks
DATABASE
取消注释BEGIN TRAN ... ROLLBACK时返回 26 行,表明对象本身和系统表行都持有锁,以允许回滚并防止其他事务读取未提交的数据。等效的表变量操作不受用户事务回滚的影响,并且不需要为我们在下一条语句中检查而持有这些锁,但是跟踪在 Profiler 中获取和释放的锁或使用跟踪标志 1200 显示大量锁定事件仍然存在发生。
BEGIN TRAN ... ROLLBACK
索引
对于 SQL Server 2014 之前的版本,索引只能在表变量上隐式创建,这是添加唯一约束或主键的副作用。这当然意味着只支持唯一索引。UNIQUE NONCLUSTERED可以通过简单地声明它并将 CI 键添加到所需 NCI 键的末尾来模拟具有唯一聚集索引的表上的非唯一非聚集索引(SQL Server无论如何都会在幕后执行此操作,即使非唯一NCI 可以指定)
UNIQUE NONCLUSTERED
如前所述index_option,可以在约束声明中指定各种 s ,包括DATA_COMPRESSION, IGNORE_DUP_KEY, and FILLFACTOR(尽管设置它没有意义,因为它只会对索引重建产生任何影响,并且您不能重建表变量上的索引!)
index_option
DATA_COMPRESSION
IGNORE_DUP_KEY
FILLFACTOR
此外,表变量不支持INCLUDEd 列、过滤索引(直到 2016 年)或分区,#temp表支持(分区方案必须在 中创建tempdb)。
INCLUDE
SQL Server 2014 中的索引
可以在 SQL Server 2014 的表变量定义中内联声明非唯一索引。示例语法如下。
DECLARE @T TABLE ( C1 INT INDEX IX1 CLUSTERED, /*Single column indexes can be declared next to the column*/ C2 INT INDEX IX2 NONCLUSTERED, INDEX IX3 NONCLUSTERED(C1,C2) /*Example composite index*/ );
SQL Server 2016 中的索引
从 CTP 3.1 开始,现在可以为表变量声明过滤索引。通过 RTM,可能会允许包含的列,尽管由于资源限制它们可能不会进入 SQL16
DECLARE @T TABLE ( c1 INT NULL INDEX ix UNIQUE WHERE c1 IS NOT NULL /*Unique ignoring nulls*/ )
并行性
插入(或以其他方式修改)表变量的查询不能有并行计划,#temp表不受这种方式的限制。
有一个明显的解决方法,如下重写确实允许SELECT部分并行发生,但最终使用隐藏的临时表(在幕后)
INSERT INTO @DATA ( ... ) EXEC('SELECT .. FROM ...')
其他功能差异
#temp_tables
INTO
ALTER
DBCC
DBCC CHECKIDENT``SET IDENTITY INSERT``WITH (FORCESCAN)
CHECK
PAGELATCH_EX
仅记忆?
如开头所述,两者都存储在tempdb的页面上。但是,在将这些页面写入持久存储时,我没有说明在行为上是否存在任何差异。
我现在已经对此进行了少量测试,到目前为止还没有看到这种差异。在我对 SQL Server 250 页面实例进行的特定测试中,似乎是数据文件被写入之前的截止点。
注意:在 SQL Server 2014 或SQL Server 2012 SP1/CU10 或 SP2/CU1中不再出现以下行为,急切的编写器不再急切地刷新页面。有关SQL Server 2014更改的更多详细信息: tempdb 隐藏的性能宝石。
运行以下脚本
CREATE TABLE #T(X INT, Filler char(8000) NULL) INSERT INTO #T(X) SELECT TOP 250 ROW_NUMBER() OVER (ORDER BY @@SPID) FROM master..spt_values DROP TABLE #T
使用 Process Monitor 监控对tempdb数据文件的写入,我没有看到任何内容(除了偶尔在偏移 73,728 处的数据库引导页面)。将 250 更改为 251 后,我开始看到如下写入。
上面的屏幕截图显示了 5 * 32 页写入和一个单页写入,表示已写入 161 页。在使用表变量进行测试时,我也得到了 250 页的相同截止点。下面的脚本通过查看以不同的方式显示它sys.dm_os_buffer_descriptors
sys.dm_os_buffer_descriptors
DECLARE @T TABLE ( X INT, [dba.se] CHAR(8000) NULL) INSERT INTO @T (X) SELECT TOP 251 Row_number() OVER (ORDER BY (SELECT 0)) FROM master..spt_values SELECT is_modified, Count(*) AS page_count FROM sys.dm_os_buffer_descriptors WHERE database_id = 2 AND allocation_unit_id = (SELECT a.allocation_unit_id FROM tempdb.sys.partitions AS p INNER JOIN tempdb.sys.system_internals_allocation_units AS a ON p.hobt_id = a.container_id INNER JOIN tempdb.sys.columns AS c ON c.object_id = p.object_id WHERE c.name = 'dba.se') GROUP BY is_modified
显示已写入 192 页并清除了脏标志。它还表明,被写入并不意味着页面将立即从缓冲池中逐出。仍然可以完全从内存中满足针对此表变量的查询。
在max server memory设置为 2000 MB 并DBCC MEMORYSTATUS报告 Buffer Pool Pages Allocated 为大约 1,843,000 KB(c. 23,000 页)的空闲服务器上,我以 1,000 行/页的批次插入到上面的表中,并记录了每次迭代。
max server memory
DBCC MEMORYSTATUS
SELECT Count(*) FROM sys.dm_os_buffer_descriptors WHERE database_id = 2 AND allocation_unit_id = @allocId AND page_type = 'DATA_PAGE'
表变量和#temp表都给出了几乎相同的图表,并且在达到它们没有完全保存在内存中之前设法几乎最大化缓冲池,因此似乎对多少内存没有任何特别的限制两者都可以消费。