10.2.2.3 使用 EXISTS 策略优化子查询
某些优化适用于使用 IN
(或 =ANY
)运算符测试子查询结果的比较。本节讨论这些优化,特别是关于 NULL
值带来的挑战。讨论的最后一部分建议您如何帮助优化器。
考虑以下子查询比较:
outer_expr IN (SELECT inner_expr FROM ... WHERE subquery_where)
MySQL 评估查询 “从外到内。” 也就是说,它首先获取外部表达式 outer_expr
的值,然后运行子查询并捕获它生成的 rows。
一个非常有用的优化是 “通知” 子查询,唯一感兴趣的行是内部表达式 inner_expr
等于 outer_expr
的行。这是通过将适当的等式推送到子查询的 WHERE
子句中以使其更具限制性来完成的。转换后的比较如下所示:
EXISTS (SELECT 1 FROM ... WHERE subquery_where AND outer_expr=inner_expr)
转换后,MySQL 可以使用下推的等式来限制它必须检查的行数以评估子查询。
更一般地,将 N
个值与返回 N
值行的子查询进行比较,也要进行相同的转换。如果 oe_i
和 ie_i
表示对应的外部和内部表达式值,则此子查询比较:
(oe_1, ..., oe_N) IN
(SELECT ie_1, ..., ie_N FROM ... WHERE subquery_where)
变成:
EXISTS (SELECT 1 FROM ... WHERE subquery_where
AND oe_1 = ie_1
AND ...
AND oe_N = ie_N)
为简单起见,以下讨论假设一对外部和内部表达式值。
如果满足以下任一条件,则 “下推” 策略才有效:
假设 outer_expr
已知为非 NULL
值,但子查询不生成 outer_expr
= inner_expr
的行。然后
评估如下:outer_expr
IN (SELECT ...)
在这种情况下,查找
行的方法不再有效。有必要查找此类行,但如果未找到,还要查找 outer_expr
= inner_expr
inner_expr
为 NULL
的行。粗略地说,子查询可以转换为如下内容:
EXISTS (SELECT 1 FROM ... WHERE subquery_where AND
(outer_expr=inner_expr OR inner_expr IS NULL))
需要评估额外的 IS NULL
条件是 MySQL 具有 ref_or_null
访问方法的原因:
mysql> EXPLAIN
SELECT outer_expr IN (SELECT t2.maybe_null_key
FROM t2, t3 WHERE ...)
FROM t1;
*************************** 1. row ***************************
id: 1
select_type: PRIMARY
table: t1
...
*************************** 2. row ***************************
id: 2
select_type: DEPENDENT SUBQUERY
table: t2
type: ref_or_null
possible_keys: maybe_null_key
key: maybe_null_key
key_len: 5
ref: func
rows: 2
Extra: Using where; Using index
...
unique_subquery
和 index_subquery
子查询特定的访问方法也具有 “或 NULL
” 变体。
额外的 OR ... IS NULL
条件使查询执行稍微复杂一些(并且子查询中的一些优化变得不适用),但通常这是可以容忍的。
当 outer_expr
可以为 NULL
时,情况要糟糕得多。根据 SQL 对 NULL
作为 “未知值” 的解释,NULL IN (SELECT
应评估为:inner_expr
...)
为了进行正确的评估,必须能够检查 SELECT
是否已产生任何行,因此
无法下推到子查询中。这是一个问题,因为除非可以将相等性下推,否则许多实际的子查询会变得非常慢。outer_expr
= inner_expr
本质上,根据 outer_expr
的值,必须有不同的方法来执行子查询。
优化器选择 SQL 兼容性而不是速度,因此它考虑了 outer_expr
可能为 NULL
的可能性:
-
如果
outer_expr
为NULL
,要评估以下表达式,则需要执行SELECT
以确定它是否产生任何行:NULL IN (SELECT inner_expr FROM ... WHERE subquery_where)
在这里,有必要执行原始的
SELECT
,不使用前面提到的任何下推等式。 -
另一方面,当
outer_expr
不为NULL
时,绝对必须进行以下比较:outer_expr IN (SELECT inner_expr FROM ... WHERE subquery_where)
将其转换为使用下推条件的表达式:
EXISTS (SELECT 1 FROM ... WHERE subquery_where AND outer_expr=inner_expr)
如果不进行此转换,子查询会很慢。
为了解决是否将条件下推到子查询中的困境,条件被包装在 “触发器” 函数中。因此,以下形式的表达式:
outer_expr IN (SELECT inner_expr FROM ... WHERE subquery_where)
被转换为:
EXISTS (SELECT 1 FROM ... WHERE subquery_where
AND trigcond(outer_expr=inner_expr))
更一般地说,如果子查询比较基于多对外层和内层表达式对,则转换采用以下比较:
(oe_1, ..., oe_N) IN (SELECT ie_1, ..., ie_N FROM ... WHERE subquery_where)
并将其转换为以下表达式:
EXISTS (SELECT 1 FROM ... WHERE subquery_where
AND trigcond(oe_1=ie_1)
AND ...
AND trigcond(oe_N=ie_N)
)
每个 trigcond(
是一个特殊函数,其计算结果为以下值:X
)
-
当““链接的”外层表达式
oe_i
不为NULL
时,值为X
-
当““链接的”外层表达式
oe_i
为NULL
时,值为TRUE
触发器函数 不是 使用 CREATE TRIGGER
创建的那种触发器。
包装在 trigcond()
函数中的等式不是查询优化器的第一类谓词。大多数优化无法处理在查询执行时可能被打开和关闭的谓词,因此它们假定任何 trigcond(
为未知函数并忽略它。触发等式可用于以下优化:X
)
-
引用优化:
trigcond(
可用于构建X
=Y
[ORY
IS NULL])ref
、eq_ref
或ref_or_null
表访问。 -
基于索引查找的子查询执行引擎:
trigcond(
可用于构建X
=Y
)unique_subquery
或index_subquery
访问。 -
表条件生成器:如果子查询是多个表的联接,则会尽快检查触发条件。
当优化器使用触发条件创建某种基于索引查找的访问(如前面列表中的前两项)时,它必须有一个针对条件关闭情况的后备策略。这个后备策略始终是相同的:执行全表扫描。在 EXPLAIN
输出中,后备策略在 Extra
列中显示为 Full scan on NULL key
:
mysql> EXPLAIN SELECT t1.col1,
t1.col1 IN (SELECT t2.key1 FROM t2 WHERE t2.col2=t1.col2) FROM t1\G
*************************** 1. row ***************************
id: 1
select_type: PRIMARY
table: t1
...
*************************** 2. row ***************************
id: 2
select_type: DEPENDENT SUBQUERY
table: t2
type: index_subquery
possible_keys: key1
key: key1
key_len: 5
ref: func
rows: 2
Extra: Using where; Full scan on NULL key
如果运行 EXPLAIN
后跟 SHOW WARNINGS
,你可以看到触发条件:
*************************** 1. row ***************************
Level: Note
Code: 1003
Message: select `test`.`t1`.`col1` AS `col1`,
<in_optimizer>(`test`.`t1`.`col1`,
<exists>(<index_lookup>(<cache>(`test`.`t1`.`col1`) in t2
on key1 checking NULL
where (`test`.`t2`.`col2` = `test`.`t1`.`col2`) having
trigcond(<is_not_null_test>(`test`.`t2`.`key1`))))) AS
`t1.col1 IN (select t2.key1 from t2 where t2.col2=t1.col2)`
from `test`.`t1`
触发条件的使用会对性能产生一些影响。现在,NULL IN (SELECT ...)
表达式可能会导致全表扫描(速度很慢),而以前不会。这是为了获得正确结果而付出的代价(触发条件策略的目标是提高合规性,而不是速度)。
对于多表子查询,NULL IN (SELECT ...)
的执行速度特别慢,因为连接优化器没有针对外部表达式为 NULL
的情况进行优化。它假定左侧带有 NULL
的子查询求值非常少见,即使有统计数据表明并非如此。另一方面,如果外部表达式可能为 NULL
但实际上永远不是,则不会有性能损失。
为了帮助查询优化器更好地执行查询,请使用以下建议:
-
如果列确实为
NOT NULL
,则将其声明为NOT NULL
。这也有助于优化器的其他方面,通过简化对该列的条件测试。 -
如果不需要区分
NULL
和FALSE
子查询结果,则可以轻松避免缓慢的执行路径。将类似于以下内容的比较替换为:outer_expr [NOT] IN (SELECT inner_expr FROM ...)
使用以下表达式:
(outer_expr IS NOT NULL) AND (outer_expr [NOT] IN (SELECT inner_expr FROM ...))
然后,
NULL IN (SELECT ...)
永远不会被求值,因为一旦表达式结果明确,MySQL 就会停止求值AND
部分。另一种可能的改写:
[NOT] EXISTS (SELECT inner_expr FROM ... WHERE inner_expr=outer_expr)
subquery_materialization_cost_based
标志(属于 optimizer_switch
系统变量)可以控制在子查询物化和 IN
到 EXISTS
子查询转换之间进行选择。请参阅 第 10.9.2 节,“可切换优化”。