加入 NULL 键列优化为表和索引扫描

Pau*_*ams 6 null join sql-server execution-plan sql-server-2014

我有一个关于这个查询计划的问题。

我们在测试环境中有一个表 Order_Details_Taxes,它有 11,225,799 行。该表有一个列 OrdTax_PLTax_LoadDtl_Key,它在每一行上都是 NULL。此测试环境的配置方式使此列始终为 NULL。此列上有一个索引。

我使用列的 NULL 值对该表运行了一些查询。NULL INNER JOIN 永远不会产生任何结果。

declare @Keys table (KeyValue decimal(15,0))
insert into @Keys (KeyValue) values (null)

select OrdTax_PLTax_LoadDtl_Key
from @Keys
inner join Order_Details_Taxes
    on OrdTax_PLTax_LoadDtl_Key = KeyValue

select *
from @Keys
inner join Order_Details_Taxes
    on OrdTax_PLTax_LoadDtl_Key = KeyValue
Run Code Online (Sandbox Code Playgroud)

这些是查询计划中的第一个查询。第一个select从亿行表开始并连接到@Keys。第二个select从@Keys 开始,但它对这个表进行聚集索引扫描。

我知道在大多数情况下临时@Tables 是有问题的,所以我将查询更改为使用临时 #Table:

if object_id ('tempdb..#Keys') is not null
    drop table #Keys
create table #Keys (KeyValue decimal(15,0))
insert into #Keys (KeyValue) values (null)

select OrdTax_PLTax_LoadDtl_Key
from #Keys
inner join Order_Details_Taxes
    on OrdTax_PLTax_LoadDtl_Key = KeyValue

select *
from #Keys
inner join Order_Details_Taxes
    on OrdTax_PLTax_LoadDtl_Key = null
Run Code Online (Sandbox Code Playgroud)

这些查询经过优化并完全按照我的预期运行——首先获取 #Keys NULL 值并寻找 Order_Details_Taxes。它们是链接的查询计划中的最后一个查询。

当我从具有单个 NULL 值的表连接到该键值中只有 NULL 的表时,为什么我使用 @Table 变量的查询在这个大表上执行索引和表扫描?

我假设答案是@Table 变量的统计和/或基数限制,但结果查询计划对我来说并不直观。

ANSI_NULLs 此表和我的 SQL 会话已启用。

Joe*_*ish 6

您所看到的行为是由于缺少表变量的统计信息造成的。当我想了解更多关于查询优化器选择特定计划的原因时,我有时会添加提示并并排比较查询。这种方法在这里很有帮助。

首先,我将创建一个结构与您的结构足够接近的表,以查看相同的行为:

CREATE TABLE dbo.Order_Details_Taxes (
    OrdTax_PLTax_LoadDtl_Key decimal(15,0),
    FILLER VARCHAR(30)
);

INSERT INTO dbo.Order_Details_Taxes WITH (TABLOCK)
SELECT NULL, REPLICATE('Z', 30)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

CREATE INDEX [IX_OrdTax_PLTax_LoadDtl_Key] ON Order_Details_Taxes (OrdTax_PLTax_LoadDtl_Key);
Run Code Online (Sandbox Code Playgroud)

要查看查询优化器如何花费不同的连接类型,我可以获得以下内容的估计计划:

declare @Keys table (KeyValue decimal(15,0))
insert into @Keys (KeyValue) values (null)

select OrdTax_PLTax_LoadDtl_Key
from @Keys
inner join Order_Details_Taxes
    on OrdTax_PLTax_LoadDtl_Key = KeyValue;

select OrdTax_PLTax_LoadDtl_Key
from @Keys
inner join Order_Details_Taxes
    on OrdTax_PLTax_LoadDtl_Key = KeyValue
OPTION (LOOP JOIN, MAXDOP 1);

select OrdTax_PLTax_LoadDtl_Key
from @Keys
inner join Order_Details_Taxes
    on OrdTax_PLTax_LoadDtl_Key = KeyValue
OPTION (HASH JOIN, MAXDOP 1);
Run Code Online (Sandbox Code Playgroud)

这是估计计划的屏幕截图:

表变量计划

SQL Server 对表变量中行的值一无所知,因此它使用 上的统计信息的密度创建嵌套循环计划OrdTax_PLTax_LoadDtl_Key。所有行在 stats 中都具有相同的值,因此密度为 1。查询优化器模型的一般假设之一是,如果最终用户正在寻找数据,则数据存在。因此,尽管直方图仅包含 NULL,但您的索引查找预计将返回与扫描相同的行数并具有相同的成本。在这种情况下,优化器不会返回并应用有关 NULL 的特殊知识来更改计划。您可能会争辩说可以改进优化器来做到这一点,但这似乎是一种不常见的情况。

计划成本的差异最终归结为联合运营商本身的成本。无论出于何种原因,查询优化器的循环连接成本都高于合并连接。散列连接的成本也很高,但为此,SQL Server 预计需要计算数百万个散列,因此 imo 更容易理解更高的成本。

如果您使用没有统计信息的临时表获得相同的计划,会发生什么情况?正确的方法是禁用表的自动统计信息创建,但我会走捷径:

if object_id ('tempdb..#Keys') is not null
    drop table #Keys
create table #Keys (KeyValue decimal(15,0))
CREATE STATISTICS s1 on #Keys (KeyValue) WITH NORECOMPUTE;
insert into #Keys (KeyValue) values (null)
Run Code Online (Sandbox Code Playgroud)

一切看起来都与表变量计划相同:

临时表没有统计

这就是为什么我说这种行为是由于缺乏统计数据造成的。当您使用临时表并允许创建自动统计信息时,优化器在临时表的列上有一个直方图。它可以使用该信息为嵌套循环连接计划和索引查找生成更准确的基数估计:

临时表统计

直方图表明不会匹配任何列,因此您最终会得到 1 行的最小基数估计值。循环连接和查找的成本相应降低,嵌套循环连接计划的成本是迄今为止三种连接类型中最低的。

在联接的外部表中具有一些 NULL 值是比联接到具有所有 NULL 的表更常见的情况。换句话说,我希望有更好的模型支持来比较两个包含 NULL 的直方图与直方图相比,仅包含 NULL 与未知值相比。通过更好的模型支持,您可以获得更好的基数估计,在这种情况下,更好的基数估计会导致查询计划的效率显着提高。