某些优化适用于使用 IN(或 =ANY)运算符测试子查询结果的比较。本节讨论这些优化,特别是关于 NULL 值带来的挑战。讨论的最后部分建议了如何帮助优化器。
考虑以下子查询比较
outer_expr IN (SELECT inner_expr FROM ... WHERE subquery_where)MySQL 从外向内评估查询。也就是说,它首先获取外部表达式 outer_expr 的值,然后运行子查询并捕获它产生的行。
一个非常有用的优化是“告知”子查询,唯一感兴趣的行是内部表达式 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_exprinner_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)
X,当 “关联” 外部表达式oe_i不为NULL时TRUE,当 “关联” 外部表达式oe_i为NULL时
触发器函数 不是 使用 CREATE TRIGGER 创建的触发器。
包装在 trigcond() 函数中的等式不是查询优化器的一流谓词。大多数优化无法处理可能在查询执行时开启或关闭的谓词,因此它们假设任何 trigcond( 都是一个未知函数并忽略它。触发等式可供以下优化使用X)
引用优化:
trigcond(可用于构建X=Y[ORYIS 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 但实际上从未为 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 节,“可切换优化”。