Mar*_*ith 465 sql-server t-sql temporary-tables
这似乎是一个有很多神话和相互冲突的领域。
那么SQL Server中的表变量和本地临时表有什么区别呢?
Mar*_*ith 695
内容

警告
此答案讨论了 SQL Server 2000 中引入的“经典”表变量。内存中的 SQL Server 2014 OLTP 引入了内存优化表类型。这些表变量实例在许多方面与下面讨论的不同!(更多细节)。
存储位置
没有不同。两者都存储在tempdb.
我已经看到它建议对于表变量,情况并非总是如此,但这可以从下面进行验证
DECLARE @T TABLE(X INT)
INSERT INTO @T VALUES(1),(2)
SELECT sys.fn_PhysLocFormatter(%%physloc%%) AS [File:Page:Slot]
FROM @T
Run Code Online (Sandbox Code Playgroud)
示例结果(显示tempdb存储在 2 行中的位置)
File:Page:Slot
----------------
(1:148:0)
(1:148:1)
Run Code Online (Sandbox Code Playgroud)
逻辑位置
@table_variables表现得更像是它们是当前数据库的一部分而不是#temp表。对于表变量(自 2005 年以来),如果未明确指定,列排序规则将是当前数据库#temp的排序规则,而对于表,它将使用tempdb(更多详细信息)的默认排序规则。此外,用户定义的数据类型和 XML 集合必须在 tempdb 中才能用于#temp表,但表变量可以从当前数据库 ( Source )使用它们。
SQL Server 2012 引入了包含的数据库。这些临时表的行为不同(h/t Aaron)
在包含数据库的临时表中,数据在包含数据库的整理中进行整理。
- 与临时表关联的所有元数据(例如,表和列名称、索引等)都将在目录排序规则中。
- 命名约束不能在临时表中使用。
- 临时表可能不引用用户定义的类型、XML 模式集合或用户定义的函数。
不同范围的可见性
@table_variables只能在声明它们的批次和范围内访问。#temp_tables可在子批次(嵌套触发器、过程、exec调用)中访问。#temp_tables在外部作用域 ( @@NESTLEVEL=0)创建的也可以跨越批次,因为它们会持续到会话结束。两种类型的对象都不能在子批次中创建,也不能在调用范围内访问,但如下所述(尽管可以使用全局##temp表)。
寿命
@table_variables在DECLARE @.. TABLE执行包含语句的批处理时(在该批处理中的任何用户代码运行之前)隐式创建,并在最后隐式删除。
尽管解析器不允许您在DECLARE语句之前尝试使用表变量,但可以在下面看到隐式创建。
IF (1 = 0)
BEGIN
DECLARE @T TABLE(X INT)
END
--Works fine
SELECT *
FROM @T
Run Code Online (Sandbox Code Playgroud)
#temp_tables在CREATE TABLE遇到TSQL语句时显式创建,并且可以显式删除DROP TABLE或将在批处理结束时(如果在子批处理中创建@@NESTLEVEL > 0)或会话结束时隐式删除。
注意:在存储例程中,两种类型的对象都可以缓存,而不是重复创建和删除新表。对何时可以发生这种缓存有一些限制,#temp_tables但是这些限制可能会违反,但@table_variables无论如何都会阻止这些限制。缓存#temp表的维护开销略大于表变量的维护开销,如此处所示。
对象元数据
这对于两种类型的对象基本上是相同的。它存储在系统基表中tempdb。但是,查看#temp表更直接,因为 OBJECT_ID('tempdb..#T')它可用于键入系统表,并且内部生成的名称与CREATE TABLE语句中定义的名称更密切相关。对于表变量,该object_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'
Run Code Online (Sandbox Code Playgroud)
输出
Duplicate key was ignored.
Run Code Online (Sandbox Code Playgroud)
| object_id | 姓名 | 行 | 类型描述 | 总页数 | 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 | 没有任何 |
交易
上的操作@table_variables作为系统事务执行,独立于任何外部用户事务,而等效的#temp表操作将作为用户事务本身的一部分执行。出于这个原因,一个ROLLBACK命令会影响一个#temp表,但@table_variable保持不变。
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
Run Code Online (Sandbox Code Playgroud)
日志记录
两者都生成日志记录到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
Run Code Online (Sandbox Code Playgroud)
退货
###详细视图

###Summary 视图(包括隐式删除和系统基表的日志记录)

据我所知,两者的操作都会产生大致相等的日志记录量。
虽然日志记录的数量非常相似,但一个重要的区别是,#temp在任何包含用户事务完成之前,无法清除与表相关的日志记录,因此长时间运行的事务在某些时候写入#temp表将阻止日志截断,tempdb而自治事务不是为表变量生成的。
表变量不支持,TRUNCATE因此当需要从表中删除所有行时可能会处于记录劣势(尽管对于非常小的表DELETE 可以更好地工作)
基数
许多涉及表变量的执行计划将显示单行估计为它们的输出。检查表变量属性表明 SQL Server 认为表变量具有零行(@Paul White在这里解释了为什么它估计将从零行表发出 1 行)。
然而在上一节中所示的结果确实显示准确rows的计数sys.partitions。问题是在大多数情况下,引用表变量的语句是在表为空时编译的。如果语句在@table_variable填充后(重新)编译,那么这将用于表基数(这可能是由于显式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
Run Code Online (Sandbox Code Playgroud)
计划在延迟编译后显示准确的估计行数。

在 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提示可用于设定室温#temp的表一样的永久表。
所有这一切的最终结果是,为#temp表生成的执行计划通常比@table_variables涉及许多行的执行计划要好几个数量级,因为 SQL Server 有更好的信息可供使用。
NB1:表变量没有统计信息,但仍会在跟踪标志 2453 下引发“Statistics Changed”重新编译事件(不适用于“平凡”计划)这似乎发生在与上面的临时表相同的重新编译阈值下,带有另外一个,如果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)
Run Code Online (Sandbox Code Playgroud)
对于SELECT来自表变量的查询,Paul White 在注释中指出,这些自动带有隐式NOLOCK提示。这如下所示
DECLARE @T TABLE(X INT);
SELECT X
FROM @T
OPTION (RECOMPILE, QUERYTRACEON 3604, QUERYTRACEON 8607)
Run Code Online (Sandbox Code Playgroud)
###输出
*** Output Tree: (trivial plan) ***
PhyOp_TableScan TBL: @T Bmk ( Bmk1000) IsRow: COL: IsBaseRow1002 Hints( NOLOCK )
Run Code Online (Sandbox Code Playgroud)
然而,这对锁定的影响可能很小。
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
Run Code Online (Sandbox Code Playgroud)
这些都不会以索引键顺序返回结果,表明 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
Run Code Online (Sandbox Code Playgroud)
表变量的锁定输出确实非常少,因为 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
Run Code Online (Sandbox Code Playgroud)
对于这两种情况,当在显式用户事务之外运行时,检查时返回的唯一锁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(尽管设置那个没有意义,因为它只会对索引重建产生任何影响,并且您不能在表变量上重建索引!)
此外,表变量不支持INCLUDEd 列、过滤索引(直到 2016 年)或分区,#temp表支持(必须在 中创建分区方案tempdb)。
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*/
);
Run Code Online (Sandbox Code Playgroud)
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*/
)
Run Code Online (Sandbox Code Playgroud)
并行性
插入(或以其他方式修改)的查询@table_variables不能有并行计划,#temp_tables不受这种方式的限制。
有一个明显的解决方法,如下重写确实允许SELECT部分并行发生,但最终使用隐藏的临时表(在幕后)
INSERT INTO @DATA ( ... )
EXEC('SELECT .. FROM ...')
Run Code Online (Sandbox Code Playgroud)
从表变量中选择的查询没有这样的限制,如我在此处的回答所示
其他功能差异
#temp_tables不能在函数内部使用。@table_variables可以在标量或多语句表 UDF 中使用。@table_variables 不能有命名约束。@table_variables不能是SELECT-ed INTO, ALTER-ed, TRUNCATEd 或者是DBCC诸如DBCC CHECKIDENTor of之类的命令的目标,SET IDENTITY INSERT并且不支持诸如WITH (FORCESCAN)CHECK 优化器不考虑对表变量的约束以进行简化、隐含谓词或矛盾检测。PAGELATCH_EX等待。(示例)只有内存?
如开头所述,两者都存储在tempdb. 但是,我没有说明在将这些页面写入磁盘时是否存在任何行为差异。
我现在已经对此进行了少量测试,到目前为止还没有看到这种差异。在我对 SQL Server 250 页实例进行的特定测试中,似乎是写入数据文件之前的截止点。
注意:在 SQL Server 2014 或SQL Server 2012 SP1/CU10 或 SP2/CU1 中不再发生以下行为,急切写入器不再急切地将页面写入磁盘。有关SQL Se
- 我发现,与等效的表变量相比,SQL Server 在创建临时表(即使使用缓存)时会获取更多的锁存器。您可以使用latch_acquired debug XE 并创建一个大约有35 列左右的表来进行测试。我发现表变量需要 4 个锁存器,临时表需要大约 70 个锁存器。 (2认同)
Kah*_*ahn 41
有几件事我想基于特定的经验而不是学习来指出。作为一名 DBA,我很新,所以请在需要时纠正我。
| 归档时间: |
|
| 查看次数: |
244654 次 |
| 最近记录: |