Answers:
内容
警告
此答案讨论了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
存储两行中的显示位置)
File:Page:Slot
----------------
(1:148:0)
(1:148:1)
逻辑位置
@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
#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'
输出量
Duplicate key was ignored.
+-----------+-----------+------+-------------------+-------------+------------+------------+-----------------------+
| object_id | name | rows | type_desc | total_pages | used_pages | data_pages | data_compression_desc |
+-----------+-----------+------+-------------------+-------------+------------+------------+-----------------------+
| 574625090 | #22401542 | 13 | IN_ROW_DATA | 2 | 2 | 1 | PAGE |
| 574625090 | #22401542 | 13 | LOB_DATA | 24 | 19 | 0 | PAGE |
| 574625090 | #22401542 | 13 | ROW_OVERFLOW_DATA | 16 | 14 | 0 | PAGE |
| 574625090 | #22401542 | 13 | IN_ROW_DATA | 2 | 2 | 1 | NONE |
+-----------+-----------+------+-------------------+-------------+------------+------------+-----------------------+
交易次数
操作@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
记录中
两者都生成到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认为该表变量具有零行(为什么它估计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
计划显示延迟的编译后准确的估计行数。
在SQL Server 2012 SP2中,引入了跟踪标志2453。更多详细信息在此处的 “关系引擎”下。
启用此跟踪标志后,它可能导致自动重新编译考虑更改的基数,这将在不久之后进一步讨论。
注意:在Azure的兼容级别150中,该语句的编译现在推迟到第一次执行。这意味着它将不再受到零行估计问题的困扰。
无栏统计
具有更准确的表基数并不意味着估计的行数会更准确(除非对表中的所有行都执行操作)。SQL Server根本不维护表变量的列统计信息,因此将基于比较谓词(例如,=
针对非唯一列返回表的10%或进行>
比较时返回表的10%)。相反,维护#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)
对于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
和FILLFACTOR
(尽管设置一个s 没有意义,因为它只会对索引重建产生任何影响,而您不能重建表变量的索引!)
此外,表变量不支持INCLUDE
d列,过滤索引(到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*/
);
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*/
)
平行性
插入(或以其他方式修改)的查询@table_variables
不能具有并行计划,#temp_tables
并且不以此方式进行限制。
有一个明显的解决方法,即如下重写确实允许SELECT
零件并行发生,但最终使用了隐藏的临时表(在幕后)。
INSERT INTO @DATA ( ... )
EXEC('SELECT .. FROM ...')
从表变量中选择的查询没有这种限制,如我的答案所示
其他功能差异
#temp_tables
不能在函数内部使用。@table_variables
可以在标量或多语句表UDF中使用。@table_variables
不能有命名约束。@table_variables
不能为SELECT
-ed INTO
,ALTER
-ed,TRUNCATE
d或为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
tempdb
使用Process Monitor 监视监视写入数据文件时,我看不到(偶尔在偏移量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
报告缓冲池页面的空闲服务器上,分配的缓冲池页面约为1,843,000 KB(约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'
table变量和#temp
table都给出了几乎相同的图形,并设法使缓冲池最大程度地溢出,直到它们没有完全保留在内存中为止,因此似乎对多少内存没有特别的限制。两者都可以消耗。
我想指出的几件事更多是基于特定的经验而不是学习。作为一名DBA,我是新手,因此请在需要时进行纠正。