SQL Server 中的临时表和表变量有什么区别?

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_variablesDECLARE @.. TABLE执行包含语句的批处理时(在该批处理中的任何用户代码运行之前)隐式创建,并在最后隐式删除。

尽管解析器不允许您在DECLARE语句之前尝试使用表变量,但可以在下面看到隐式创建。

IF (1 = 0)
BEGIN
DECLARE @T TABLE(X INT)
END

--Works fine
SELECT *
FROM @T
Run Code Online (Sandbox Code Playgroud)

#temp_tablesCREATE 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_pa​​ges 数据页 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 取决于桌子类型和大小。

来自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 的说明)

KEEP PLAN提示可用于设定室温#temp的表一样的永久表。

所有这一切的最终结果是,为#temp表生成的执行计划通常比@table_variables涉及许多行的执行计划要好几个数量级,因为 SQL Server 有更好的信息可供使用。

NB1:表变量没有统计信息,但仍会在跟踪标志 2453 下引发“Statistics Changed”重新编译事件(不适用于“平凡”计划)这似乎发生在与上面的临时表相同的重新编译阈值下,带有另外一个,如果N=0 -> RT = 1。即所有在表变量为空时编译的语句最终都会重新编译并TableCardinality在它们第一次执行时更正。编译时间表基数存储在计划中,如果语句以相同的基数再次执行(由于控制语句流或缓存计划的重用),则不会发生重新编译。

NB2:对于存储过程中的缓存临时表,重新编译的过程比上面描述的要复杂得多。有关所有详细信息,请参阅存储过程中的临时表

重新编译

除了上面描述的基于修改的重新编译外,#temp还可以与其他编译相关联,因为它们允许对触发编译的表变量进行禁止的操作(例如 DDL 更改CREATE INDEXALTER 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_locksDATABASE.

取消注释时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,我很新,所以请在需要时纠正我。

  1. #temp 表默认使用 SQL Server 实例的默认排序规则。因此,除非另有说明,否则如果 masterdb 与数据库的排序规则不同,您可能会遇到比较或更新 #temp 表和数据库表之间的值的问题。请参阅:http : //www.mssqltips.com/sqlservertip/2440/create-sql-server-temporary-tables-with-the-correct-collat​​ion/
  2. 完全基于个人经验,可用内存似乎对哪个性能更好有影响。MSDN 建议使用表变量来存储较小的结果集,但大多数情况下这种差异甚至不明显。然而,在较大的集合中,在某些情况下很明显表变量需要更多的内存,并且可以将查询减慢到爬行。

  • 另请注意,如果您使用的是 SQL Server 2012 并且包含数据库,则 #temp 表上的排序规则*可以*继承调用数据库的排序规则。 (6认同)