从本质上说,mysql对所有的类型的查询都以同样的方式运行,如:mysql在from子句中遇到子查询,先执行子查询并将结果放到一个临时表中,然后将这个临时表当作一个普通表对待(正如其名:派生表),mysql在执行union查询时也使用类似的临时表,在遇到右外连接是,mysql将其改写成等价的左外连接,换而言之,当前版本的mysql会将所有的查询类型都换换成类似的执行计划,不过,不是所有的查询都可以转换成上面的形式,如:全外连接就无法通过嵌套循环和回溯的方式完成,这也是mysql并不支持全外连接的原因。
注意:mysql临时表没有任何索引,在编写复杂的子查询和关联查询的时候需要注意这一点,这一点对union查询也一样,在mysql5.6和mariadb中有了重大改变,这两个版本都引入了更加复杂的执行计划。
执行计划:
和很多其他关系数据库不同,mysql并不会生成查询字节码来执行查询,mysql生成查询的一棵指令树,然后通过存储引擎执行完成这棵指令树并返回结果,最终的执行计划包含了重构查询的全部信息,如对某个查询执行explain extended后,再执行show warnings,就可以查看到重构的查询。mysql总是从一个表开始一直嵌套循环,回溯完成所有表的关联。所以,mysql的执行计划是一棵左侧深度优先的树,然后回溯到上一层关联:
关联查询优化器:
mysql优化器最重要的一部分就是关联查询优化,它决定了多个表关联时的顺序,通常多表关联的时候,可以有多种不同的关联顺序来获得相同的执行效果,关联查询优化器则通过评估不同顺序时的成本来选择一个代价最小的关联顺序。
示例:
explain select film.film_id,film.title,film.release_year,actor.actor_id,actor.first_name,actor.last_name from sakila.film join sakila.film_actor using(film_id) join sakila.actor using(actor_id)\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: actor
type: ALL
possible_keys: PRIMARY
key: NULL
key_len: NULL
ref: NULL
rows: 200
Extra: NULL
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: film_actor
type: ref
possible_keys: PRIMARY,idx_fk_film_id
key: PRIMARY
key_len: 2
ref: sakila.actor.actor_id
rows: 13
Extra: Using index
*************************** 3. row ***************************
id: 1
select_type: SIMPLE
table: film
type: eq_ref
possible_keys: PRIMARY
key: PRIMARY
key_len: 2
ref: sakila.film_actor.film_id
rows: 1
Extra: NULL
3 rows in set (0.00 sec)
执行计划中可以看到,mysql从actor表开始,使用film_actor表的索引film_id来查找对应的actor_id值,然后根据film表的主键找到对应的记录,oracle用户会用下面的术语描述:actor表作为驱动表先找到file_actor表,然后以此结果为驱动表再查找film表。
下面使用straight_join关键字指定关联表顺序,如下:
explain select straight_join film.film_id,film.title,film.release_year,actor.actor_id,actor.first_name,actor.last_name from sakila.film join sakila.film_actor using(film_id) join sakila.actor using(actor_id)\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: film
type: ALL
possible_keys: PRIMARY
key: NULL
key_len: NULL
ref: NULL
rows: 1000
Extra: NULL
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: film_actor
type: ref
possible_keys: PRIMARY,idx_fk_film_id
key: idx_fk_film_id
key_len: 2
ref: sakila.film.film_id
rows: 2
Extra: Using index
*************************** 3. row ***************************
id: 1
select_type: SIMPLE
table: actor
type: eq_ref
possible_keys: PRIMARY
key: PRIMARY
key_len: 2
ref: sakila.film_actor.actor_id
rows: 1
Extra: NULL
3 rows in set (0.00 sec)
从上面的explain结果上看,mysql自动选择的关联顺序与使用straight_join关键字的关联顺序完全倒转过来,从上面的对比结果上可以看出,mysql把关联表倒转过来后,film只扫描一行,而film在最前时扫描行数是1000.
如果mysql选择首先扫描actor表,只会返回200条记录进行后面的嵌套循环查询,即,倒转关联顺序会让查询进行更少的嵌套循环和回溯操作,为了验证优化器的选择是否正确,我们单独执行了这两个查询,并且看last_query_cost状态值(这里我使用的虚拟机做测试),结果mysql选择的顺序预估成本为3883.340463,而我们手工指定的顺序预估成本为4012.731064。
这个简单的示例说明mysql是如何选项合适的关联顺序来让查询执行的成本尽可能低,重新定义关联的顺序是优化器非常重要的一部分,不过有时候,优化器给出的并不是最优的关联顺序,这时可以使 用straight_join关键字重写查询,让优化器按照你认为的最优的关联顺序执行,不过,人的判断很难那么精准,所以大部分时候,优化器做出的选择比普通人判断的要更精准。
关联优化器会尝试在所有的关联顺序中选择一个成本最小的来生成执行计划树,如果可能,优化器会遍历每一个表后逐个做嵌套循环计算每一颗可能的执行计划树的成本,最后返回一个最优的执行计划。
但是,糟糕的是,如果有超过n个表关联,那么需要检查n的阶乘种关联顺序,我们称之为所有可能的执行计划的“搜索空间”,搜索空间的增长速度非常快,如:若是10个关联,那么共有3628800种关联顺序,当搜索空间非常大的时候,优化器不可能逐一评估每一种关联顺序的成本,这时,优化器选择使用贪婪搜索的方式查找最优的关联顺序,实际上,当需要关联的表超过optimizer_search_depth的限制的时候,就会选择贪婪搜索模式了,optimizer_search_depth这个参数可以根据需要动态指定大小,默认为62。