为什么查询优化器选择完全不同的查询计划?

Rad*_*ača 11 sql sql-server query-optimization

让我们在SQL Server 2016中有下表

-- generating 1M test table with four attributes
WITH x AS 
(
  SELECT n FROM (VALUES (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) v(n)
), t1 AS
(
  SELECT ones.n + 10 * tens.n + 100 * hundreds.n + 1000 * thousands.n + 10000 * tenthousands.n + 100000 * hundredthousands.n as id  
  FROM x ones,     x tens,      x hundreds,       x thousands,       x tenthousands,       x hundredthousands
)
SELECT  id,
        id % 50 predicate_col,
        row_number() over (partition by id % 50 order by id) join_col, 
        LEFT('Value ' + CAST(CHECKSUM(NEWID()) AS VARCHAR) + ' ' + REPLICATE('*', 1000), 1000) as padding
INTO TestTable
FROM t1
GO

-- setting the `id` as a primary key (therefore, creating a clustered index)
ALTER TABLE TestTable ALTER COLUMN id int not null
GO
ALTER TABLE TestTable ADD CONSTRAINT pk_TestTable_id PRIMARY KEY (id)

-- creating a non-clustered index
CREATE NONCLUSTERED INDEX ix_TestTable_predicate_col_join_col
ON TestTable (predicate_col, join_col)
GO
Run Code Online (Sandbox Code Playgroud)

好的,现在当我运行以下只有略微不同的谓词的查询时(b.predicate_col <= 0 vs. b.predicate_col = 0)我得到了完全不同的计划.

-- Q1
select b.id, b.predicate_col, b.join_col, b.padding
from TestTable b
join TestTable a on b.join_col = a.id
where a.predicate_col = 1 and b.predicate_col <= 0
option (maxdop 1)

-- Q2
select b.id, b.predicate_col, b.join_col, b.padding
from TestTable b
join TestTable a on b.join_col = a.id
where a.predicate_col = 1 and b.predicate_col = 0
option (maxdop 1)
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

如果我查看查询计划,那么很明显他选择首先加入密钥查找和非聚集索引查找,然后在Q1的情况下与非聚集索引进行最终连接(这是不好的).在Q2的情况下,更好的解决方案是:他首先加入非聚集索引,然后进行最终的密钥查找.

问题是:为什么会这样,我能以某种方式改进它吗?

在我对直方图的直观理解中,应该很容易估计谓词(b.predicate_col <= 0 vs. b.predicate_col = 0)的两种变体的正确结果,因此,为什么不同的查询计划?

编辑:

实际上,我不想更改表的索引或物理结构.我想了解为什么他在Q1的情况下选择了这么糟糕的查询计划.因此,我的问题恰恰是这样的: 为什么他在Q1的情况下选择了这么糟糕的查询计划,我可以在不改变物理设计的情况下改进吗?

我已经检查了查询计划中的结果估计,并且两个查询计划都具有每个运算符的精确行数估计!我已经检查了结果备忘录结构(OPTION (QUERYTRACEON 3604, QUERYTRACEON 8615, QUERYTRACEON 8620))和编译期间应用的规则(OPTION (QUERYTRACEON 3604, QUERYTRACEON 8619, QUERYTRACEON 8620)),看来他在完成第一个计划后就完成了查询计划搜索.这是这种行为的原因吗?

pac*_*ely 1

这是由于 SQL Server 无法使用不等式搜索右侧的索引列造成的。

此代码产生相同的问题:

SELECT * FROM TestTable WHERE predicate_col <= 0 and join_col = 1
SELECT * FROM TestTable WHERE predicate_col = 0 and join_col <= 1
Run Code Online (Sandbox Code Playgroud)

不等式查询(例如 >= 或 <=)对 SQL 施加了限制,优化器无法使用索引中的其余列,因此当您在 [predicate_col] 上放置不等式时,索引的其余部分将变得无用,SQL 无法充分利用索引并产生备用(坏)计划。[join_col] 是索引中的最后一列,因此在第二个查询中 SQL 仍然可以充分利用索引。

SQL 选择哈希匹配的原因是因为它无法保证从表 B 中出来的数据的顺序。不等式使得索引中的 [join_col] 毫无用处,因此 SQL 必须为连接上的未排序数据做好准备,即使行数是相同的。

解决问题的唯一方法(即使您不喜欢它)是更改索引,使“等式”列位于“不等式”列之前。

  • 索引键列的顺序解释了为什么它不选择嵌套循环连接(`predicate_col, join_col` 上的索引不支持对 `join_col = a.id and b.predicate_col &lt;= 0` 进行有效查找,但它没有解释为什么(已经决定哈希连接更合适)它不使用内部连接的交换性来生成这样的计划 https://i.stack.imgur.com/QuPAr.png 从而执行 400查找而不是 20,000。这就是问题实际要问的。**“更好的解决方案是......首先加入非聚集索引,然后进行最终的键查找。”** (2认同)