MySQL如何关联查询
总的来说,mysql认为任何一个查询都是一次关联,并不仅仅是一个查询需要用到两个表匹配才叫关联,所以,在mysql中,每一个查询,每一个片段(包括子查询,甚至单表select)都可能是关联。所以,理解mysql如何执行关联查询至关重要,先来看一个union的例子,对于union,mysql先将一系列的单个查询结果放到一个临时表中,然后再重新读出临时表数据来完成union,在mysql的概念中,每个查询都是一次关联,所以读取结果临时表也是一次关联。
当前mysql关联执行的策略很简单,mysql对任何关联都执行嵌套循环关联操作,即,mysql先在一个表中循环读取单条数据,然后再嵌套循环到下一个表中寻找匹配的行,依次下去,直到找到所有表中匹配的行为止。然后根据各个表匹配的行,返回查询中需要的各个列,mysql会尝试在最后一个关联表中找到所有匹配的行,如果最后一个关联表无法找到更多的行以后,mysql返回到上一层次关联表,看是否能够找到更多的匹配记录,依次类推迭代执行。照这样的方法查找第一个表记录,再嵌套查询下一个关联表,然后回溯到上一个表,在mysql中是通过嵌套循环的方式实现(正如其名:嵌套循环关联)
从本质上说,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。