如何解决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的默认排序规则(更多详细信息)。
用户定义的数据类型和 XML 集合必须在tempdb中才能用于#temp
表和表变量 ( Source )。表的tempdb中必须存在用户定义的别名类型,但表变量使用上下文数据库。#temp
sql Server 2012 引入了包含的数据库。这些临时表的行为不同(h/t Aaron)
表变量只能在声明它们的批处理和范围内访问。#temp
可以在子批次(嵌套触发器、过程、exec
调用)中访问表。#temp
在外部范围 ( @@nesTLEVEL=0
) 创建的表也可以跨越批次,因为它们会一直持续到会话结束。两种类型的对象都不能在子批处理中创建并在调用范围内访问,但是如下所述(尽管可以##temp
是全局表)。
DECLARE @.. TABLE
执行包含语句的批处理时(在该批处理中的任何用户代码运行之前)隐式创建表变量,并在最后隐式删除。
尽管解析器不允许您在DECLARE
语句之前尝试使用表变量,但可以在下面看到隐式创建。
IF (1 = 0)
BEGIN
DECLARE @T TABLE(X INT)
END
--Works fine
SELECT *
FROM @T
#temp
表在CREATE TABLE
遇到 Tsql 语句时显式创建,并且可以在批处理结束时显式删除DROP TABLE
或将在批处理结束时隐式删除(如果在子批处理中创建@@nesTLEVEL > 0
)或会话结束时以其他方式结束。
注意:在存储例程中,两种类型的对象都可以被缓存,而不是重复创建和删除新表。对于何时可以发生这种缓存有一些限制,但是对于#temp
表可能会违反这些限制,但对表变量的限制无论如何都会阻止这些限制。#temp
缓存表的维护开销略大于表变量
这对于两种类型的对象基本上是相同的。它存储在tempdb的系统基表中。#temp
但是,对于表 来说,查看起来更直接,因为OBJECT_ID('tempdb..#T')
它可以用于键入系统表,并且内部生成的名称与CREATE TABLE
语句中定义的名称更密切相关。对于表变量,该object_id
函数不起作用,内部名称完全是系统生成的,与变量名称无关(名称是对象 id 的十六进制形式)。
下面演示了元数据仍然存在,但是通过键入(希望是唯一的)列名。DBCC PAGE
对于没有唯一列名的表,只要它们不为空,就可以使用 object_id 来确定它们。
/*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.
object_id | 姓名 | 行 | type_desc | 总页数 | used_pages | 数据页 | data_compression_desc |
---|---|---|---|---|---|---|---|
574625090 | #22401542 | 13 | IN_ROW_DATA | 2 | 2 | 1 | 页 |
574625090 | #22401542 | 13 | LOB_DATA | 24 | 19 | 0 | 页 |
574625090 | #22401542 | 13 | ROW_OVERFLOW_DATA | 16 | 14 | 0 | 页 |
574625090 | #22401542 | 13 | IN_ROW_DATA | 2 | 2 | 1 | 没有任何 |
在 sql Server 2012 之前,临时表和表变量的对象 ID 为正数。从 sql Server 2012 开始,临时表和表变量的对象 ID 始终为负数(设置高位)。
对表变量的操作作为系统事务执行,独立于任何外部用户事务,而等效的#temp
表操作将作为用户事务本身的一部分执行。由于这个原因,一个ROLLBACK
命令会影响一个#temp
表,但不会改变一个表变量。
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
两者都为事务日志生成日志记录。一个常见的误解是表变量并非如此,因此下面的脚本演示了这一点,它声明了一个表变量,添加了几行,然后更新它们并删除它们。
因为表变量是在批处理开始和结束时隐式创建和删除的,所以需要使用多个批处理才能查看完整的日志记录。
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
无论如何都可以更好地工作
许多涉及表变量的执行计划将显示单行估计为它们的输出。检查表变量属性表明 sql Server 认为表变量行(Paul White 解释了为什么它估计会从零行表中发出一行)。
但是,上一节中显示的结果确实显示了准确的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 取决于表类型和大小。
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 的说明)
该KEEP PLAN
提示可用于将表的 RT 设置为#temp
与永久表相同。
所有这一切的最终结果是,#temp
当涉及许多行时,为表生成的执行计划通常比为表变量生成的执行计划好几个数量级,因为 sql Server 有更好的信息可以使用。
NB1:表变量没有统计信息,但仍会在跟踪标志 2453 下引发“统计信息已更改”重新编译事件(不适用于“琐碎”计划)这似乎发生在与上述临时表相同的重新编译阈值下额外的一个 if N=0 -> RT = 1
。即当表变量为空时编译的所有语句最终将在非空时第一次执行时重新编译并更正TableCardinality 。编译时间表基数存储在计划中,如果语句以相同的基数再次执行(由于控制语句的流或缓存计划的重用),则不会发生重新编译。
NB2:对于存储过程中的缓存临时表,重新编译的故事比上面描述的要复杂得多。有关所有详细信息,请参阅存储过程中的临时表。
除了上面描述的基于修改的重新编译之外,#temp
表还可以与其他编译相关联,因为它们允许对触发编译的表变量进行禁止的操作(例如 DDL 更改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
提示。如下所示
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
与记录周围用户事务的问题类似,这可能意味着表的锁定时间更长#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
.
取消注释BEGIN TRAN ... ROLLBACK
时返回 26 行,表明对象本身和系统表行都持有锁,以允许回滚并防止其他事务读取未提交的数据。等效的表变量操作不受用户事务回滚的影响,并且不需要为我们在下一条语句中检查而持有这些锁,但是跟踪在 Profiler 中获取和释放的锁或使用跟踪标志 1200 显示大量锁定事件仍然存在发生。
对于 sql Server 2014 之前的版本,索引只能在表变量上隐式创建,这是添加唯一约束或主键的副作用。这当然意味着只支持唯一索引。UNIQUE NONCLUSTERED
可以通过简单地声明它并将 CI 键添加到所需 NCI 键的末尾来模拟具有唯一聚集索引的表上的非唯一非聚集索引(sql Server无论如何都会在幕后执行此操作,即使非唯一NCI 可以指定)
如前所述index_option
,可以在约束声明中指定各种 s ,包括DATA_COMPRESSION
, IGnorE_DUP_KEY
, and FILLFACTOR
(尽管设置它没有意义,因为它只会对索引重建产生任何影响,并且您不能重建表变量上的索引!)
此外,表变量不支持INCLUDE
d 列、过滤索引(直到 2016 年)或分区,#temp
表支持(分区方案必须在 中创建tempdb
)。
可以在 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*/
);
从 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
不能在函数内部使用。表变量可以在标量或多语句表 UDF 中使用。 - 表变量不能有命名约束。
- 表变量不能是
SELECT
-edINTO
、ALTER
-ed 、TruncATE
d 或作为or of等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
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
结果
is_modified | page_count |
---|---|
0 | 192 |
1 | 61 |
显示已写入 192 页并清除了脏标志。它还表明,被写入并不意味着页面将立即从缓冲池中逐出。仍然可以完全从内存中满足针对此表变量的查询。
在max server memory
设置为 2000 MB 并DBCC MEMORYSTATUS
报告 Buffer Pool Pages Allocated 为大约 1,843,000 KB(c. 23,000 页)的空闲服务器上,我以 1,000 行/页的批次插入到上面的表中,并记录了每次迭代。
SELECT Count(*)
FROM sys.dm_os_buffer_descriptors
WHERE database_id = 2
AND allocation_unit_id = @alLocid
AND page_type = 'DATA_PAGE'
表变量和#temp
表都给出了几乎相同的图表,并且在达到它们没有完全保存在内存中之前设法几乎最大化缓冲池,因此似乎对多少内存没有任何特别的限制两者都可以消费。
解决方法
这似乎是一个有很多神话和相互矛盾的观点的领域。
那么 SQL Server 中的表变量和本地临时表有什么区别呢?
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。