让我们在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
好的,现在当我运行以下只有略微不同的谓词的查询时(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)
如果我查看查询计划,那么很明显他选择首先加入密钥查找和非聚集索引查找,然后在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)
),看来他在完成第一个计划后就完成了查询计划搜索。这是这种行为的原因吗?
答案 0 :(得分: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
不等式查询(例如&gt; =或&lt; =)对SQL进行限制,优化工具无法使用索引中的其余列,因此当您在[predicate_col]上设置不等式时,您需要&#39 ;为了使索引的其余部分无效,SQL无法充分利用索引并产生备用(坏)计划。 [join_col]是索引中的最后一列,因此在第二个查询中,SQL仍然可以充分利用索引。
SQL选择哈希匹配的原因是因为它无法保证表B中出现的数据的顺序。不等式使得索引中的[join_col]无用,因此SQL必须为未分类的数据做准备连接,即使行数相同。
解决问题的唯一方法(即使你不喜欢它)是改变索引,以便Equality列出现在Inequality列之前。
答案 1 :(得分:0)
好的回答也可以从Statistics and histogram
的角度来看。
答案也可以从index structure
安排的角度来看。
好的我想尝试从index structure
回答这个问题。
虽然在两个查询中都得到相同的结果,因为没有predicate_col < 0 records
如果Range predicate
中有composite index
,则无法使用这两个索引。还有很多其他原因导致指数无法使用。
-- 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那样的计划,那么我们可以创建另一个复合索引。
-- creating a non-clustered index
CREATE NONCLUSTERED INDEX ix_TestTable_predicate_col_join_col_1
ON TestTable (join_col,predicate_col)
GO
我们的查询计划与Q2完全相同。
另一种方法是在CHECK constraint
predicate_col
Alter table TestTable ADD check (predicate_col>=0)
GO
这也提供与Q2相同的查询计划。
虽然在实际的表和数据中,您是否可以创建CHECK Constraint
或创建另一个composite index
是另一种讨论。