为什么表变量强制索引扫描而临时表使用查找和书签查找?

8kb*_*8kb 18 sql-server optimization sql-server-2008-r2 user-defined-table-type bookmark-lookup

我试图理解为什么使用表变量会阻止优化器使用索引查找然后书签查找与索引扫描。

填充表:

CREATE TABLE dbo.Test 
(
    RowKey INT NOT NULL PRIMARY KEY, 
    SecondColumn CHAR(1) NOT NULL DEFAULT 'x',
    ForeignKey INT NOT NULL 
) 

INSERT dbo.Test 
(
    RowKey, 
    ForeignKey
) 
SELECT TOP 1000000 
    ROW_NUMBER() OVER (ORDER BY (SELECT 0)),
    ABS(CHECKSUM(NEWID()) % 10)     
FROM sys.all_objects s1
CROSS JOIN sys.all_objects s2 

CREATE INDEX ix_Test_1 ON dbo.Test (ForeignKey) 
Run Code Online (Sandbox Code Playgroud)

使用单个记录填充表变量,并尝试通过搜索外键列来查找主键和第二列:

DECLARE @Keys TABLE (RowKey INT NOT NULL) 

INSERT @Keys (RowKey) VALUES (10)

SELECT 
    t.RowKey,
    t.SecondColumn
FROM
    dbo.Test t 
INNER JOIN 
    @Keys k
ON
    t.ForeignKey = k.RowKey
Run Code Online (Sandbox Code Playgroud)

下面是执行计划:

在此处输入图片说明

现在使用临时表进行相同的查询:

CREATE TABLE #Keys (RowKey INT NOT NULL) 

INSERT #Keys (RowKey) VALUES (10) 

SELECT 
    t.RowKey,
    t.SecondColumn
FROM
    dbo.Test t 
INNER JOIN 
    #Keys k
ON
    t.ForeignKey = k.RowKey
Run Code Online (Sandbox Code Playgroud)

此查询计划使用查找和书签查找:

在此处输入图片说明

为什么优化器愿意使用临时表而不是表变量进行书签查找?

在此示例中,表变量用于表示通过存储过程中用户定义的表类型传入的数据。

我意识到如果外键值出现数十万次,索引查找可能不合适。在这种情况下,扫描可能是更好的选择。对于我创建的场景,没有值为 10 的行。我仍然认为这种行为很有趣,想知道是否有原因。

SQL小提琴

添加OPTION (RECOMPILE)并没有改变行为。UDDT 有一个主键。

@@VERSION 是 SQL Server 2008 R2 (SP2) - 10.50.4042.0 (X64)(内部版本 7601:Service Pack 1)(管理程序)

Aar*_*and 15

该行为的原因是 SQL Server 无法确定有多少行将与 ForeignKey 匹配,因为没有以 RowKey 作为前导列的索引(它可以从 #temp 表的统计信息中推断出这一点,但那些没有表变量/UDTT 存在),因此它估计有 100,000 行,扫描处理比搜索 + 查找更好。当 SQL Server 意识到只有一行时,为时已晚。

您可能能够以不同的方式构建您的 UDTT;在更现代的 SQL Server 版本中,您可以在表变量上创建二级索引,但此语法在 2008 R2 中不可用。

顺便说一句,如果您尝试通过提示嵌套循环连接来避免位图/探针,您可以获得搜索行为(至少在我有限的试验中):

DECLARE @Keys TABLE (RowKey INT PRIMARY KEY); -- can't hurt

INSERT @Keys (RowKey) VALUES (10);

SELECT 
     t.RowKey
    ,t.SecondColumn
FROM
    dbo.Test t 
INNER JOIN 
    @Keys k
ON
    t.ForeignKey = k.RowKey
    OPTION (LOOP JOIN);
Run Code Online (Sandbox Code Playgroud)

几年前我从保罗怀特那里学到了这个技巧。当然,您应该小心在生产代码中放置任何类型的连接提示 - 如果人们对底层对象进行更改并且该特定类型的连接不再可能或不再是最佳的,这可能会失败。

对于更复杂的查询,当您迁移到 SQL Server 2012 或更高版本时,跟踪标志 2453可能会有所帮助。但是,该标志对这个简单的连接没有帮助。同样的免责声明也适用 - 这只是在没有大量文档和严格的回归测试程序的情况下您通常不应该做的另一种事情。

此外,Service Pack 1 早已停止支持,您应该获取Service Pack 3 + MS15-058