当前位置: 移动技术网 > IT编程>数据库>Mysql > 探究MySQL优化器对索引和JOIN顺序的选择

探究MySQL优化器对索引和JOIN顺序的选择

2017年12月12日  | 移动技术网IT编程  | 我要评论

校园99abcd,2011十大网络流行语,好日子香烟价格表图

本文通过一个案例来看看mysql优化器如何选择索引和join顺序。表结构和数据准备参考本文最后部分"测试环境"。这里主要介绍mysql优化器的主要执行流程,而不是介绍一个优化器的各个组件(这是另一个话题)。

   我们知道,mysql优化器只有两个自由度:顺序选择;单表访问方式;这里将详细剖析下面的sql,看看mysql优化器如何做出每一步的选择。

explain
select *
from
 employee as a,department as b
where
   a.lastname = 'zhou'
 and b.departmentid = a.departmentid
 and b.departmentname = 'tbx';

1. 可能的选择

   这里看到join的顺序可以是a|b或者b|a,单表访问方式也有多种,对于a表可以选择:全表扫描和索引`ind_l_d`(a.lastname = 'zhou')或者`ind_did`(b.departmentid = a.departmentid)。对于b也有三个选择:全表扫描、索引ind_d、ind_dn。
2. mysql优化器如何做
2.1 概述

   mysql优化器主要工作包括以下几部分:query rewrite(包括outer join转换等)、const table detection、range analysis、join optimization(顺序和访问方式选择)、plan refinement。这个案例从range analysis开始。
2.2 range analysis

   这部分包括所有range和index merge成本评估(参考1 参考2)。这里,等值表达式也是一个range,所以这里会评估其成本,计算出found records(表示对应的等值表达式,大概会选择出多少条记录)。

   本案例中,range analysis会针对a表的条件a.lastname = 'zhou'和b表的b.departmentname = 'tbx'分别做分析。其中:

表a a.lastname = 'zhou' found records: 51
表b b.departmentname = 'tbx' found records: 1

   这两个条件都不是range,但是这里计算的值仍然会存储,在后面的ref访问方式评估的时候使用。这里的值是根据records_in_range接口返回,而对于innodb每次调用这个函数都会进行一次索引页的采样,这是一个很消耗性能的操作,对于很多其他的关系数据库是使用"直方图"的统计数据来避免这次操作(相信mariadb后续版本也将实现直方图统计信息)。
2.3 顺序和访问方式的选择:穷举

   mysql通过枚举所有的left-deep树(也可以说所有的left-deep树就是整个mysql优化器的搜索空间),来找到最优的执行顺序和访问方式。
2.3.1 排序

   优化器先根据found records对所有表进行一个排序,记录少的放前面。所以,这里顺序是b、a。
2.3.2 greedy search

   当表的数量较少(少于search_depth,默认是63)的时候,这里直接蜕化为一个穷举搜索,优化器将穷举所有的left-deep树找到最优的执行计划。另外,优化器为了减少因为搜索空间庞大带来巨大的穷举消耗,所以使用了一个"偷懒"的参数prune_level(默认打开),具体如何"偷懒",可以参考join顺序选择的复杂度。不过至少需要有三个表以上的关联才会有"偷懒",所以本案例不适用。
2.3.3 穷举

   join的第一个表可以是:a或者b;如果第一个表选择了a,第二个表可以选择b;如果第一个表选择了b,第二个表可以选择a;

   因为前面的排序,b表的found records更少,所以join顺序穷举时的第一个表先选择b(这个是有讲究的)。

(*) 选择第一个join的表为b
  (**) 确定b表的访问方式
    因为b表为第一个表,所以无法使用索引ind_d(b.departmentid = a.departmentid),而只能使用ind_dn(b.departmentname = 'tbx')
      使用ind_dn索引的成本计算:1.2;其中io成本为1。
      是否使用全表扫描:这里会比较使用索引的io成本和全表扫描的io成本,前者为1,后者为2;所以忽略全表扫描
    所以,b表的访问方式ref,使用索引ind_d

  (**) 从剩余的表中穷举选出第二个join的表,这里剩余的表为:a
  (**) 将a表加入join,并确定其访问方式
    可以使用的索引为:`ind_l_d`(a.lastname = 'zhou')或者`ind_did`(b.departmentid = a.departmentid)
    依次计算使用索引ind_l_d、ind_did的成本:
    (***) ind_l_d a.lastname = 'zhou'
          在range analysis阶段给出了a.lastname = 'zhou'对应的记录约为:51。
          所以,计算io成本为:51;ref做io成本计算时会做一次修正,将其修正为worst_seek(参考)
          修正后io成本为:15,总成本为:25.2
    (***) ind_did b.departmentid = a.departmentid
          这是一个需要知道前面表的结果,才能计算的成本。所以range analysis是无法分析的
          这里,我们看到前面表为b,found_record是1,所以a.departmentid只需要对应一条记录就可以了
          因为具体取值不知道,也没有直方图,所以只能简单依据索引统计信息来计算:
            索引ind_did的列a.departmentid的cardinality为1349,全表记录数为1349
            所以,每一个值对应一条记录,而前面表b只有一条记录,所以这里的found_record计算为1*1 = 1
            所以io成本为:1,总成本为1.2
    (***) ind_l_d成本为25.2;ind_did成本为1.2,所以选择后者为当前表的访问方式
  (**) 确定a使用索引ind_did,访问方式为ref
  (**) join顺序b|a,总成本为:1.2+1.2 = 2.4

(*) 选择第一个join的表为a
  (**) 确定a表的访问方式
       因为a表是第一个表,所以无法使用索引`ind_did`(b.departmentid = a.departmentid)
       那么只能使用索引`ind_l_d`(a.lastname = 'zhou')
         使用ind_l_d索引的成本计算,总成本为25.2;参考前面计算;
  (**) 这里访问a表的成本已经是25.2,比之前的最优成本2.4要大,忽略该顺序
       所以,这次穷举搜索到此结束

   把上面的过程简化如下:

(*) 选择第一个join的表为b
  (**) 确定b表的访问方式
  (**) 从剩余的表中穷举选出第二个join的表,这里剩余的表为:a
  (**) 将a表加入join,并确定其访问方式
    (***) ind_l_d a.lastname = 'zhou'
    (***) ind_did b.departmentid = a.departmentid
    (***) ind_l_d成本为25.2;ind_did成本为1.2,所以选择后者为当前表的访问方式
  (**) 确定a使用索引ind_did,访问方式为ref
  (**) join顺序b|a,总成本为:1.2+1.2 = 2.4

(*) 选择第一个join的表为a
  (**) 确定a表的访问方式
  (**) 这里访问a表的成本已经是25.2,比之前的最优成本2.4要大,忽略该顺序

   至此,mysql优化器就确定了所有表的最佳join顺序和访问方式。
3. 测试环境

mysql: 5.1.48-debug-log innodb plugin 1.0.9

create table `department` (
 `departmentid` int(11) default null,
 `departmentname` varchar(20) default null,
 key `ind_d` (`departmentid`),
 key `ind_dn` (`departmentname`)
) engine=innodb default charset=gbk;

create table `employee` (
 `lastname` varchar(20) default null,
 `departmentid` int(11) default null,
 key `ind_l_d` (`lastname`),
 key `ind_did` (`departmentid`)
) engine=innodb default charset=gbk;

for i in `seq 1 1000` ; do mysql -vvv -uroot test -e 'insert into department values (600000*rand(),repeat(char(65+rand()*58),rand()*20))'; done
for i in `seq 1 1000` ; do mysql -vvv -uroot test -e 'insert into employee values (repeat(char(65+rand()*58),rand()*20),600000*rand())'; done

for i in `seq 1 50` ; do mysql -vvv -uroot test -e 'insert into employee values ("zhou",27760)'; done
for i in `seq 1 200` ; do mysql -vvv -uroot test -e 'insert into employee values (repeat(char(65+rand()*58),rand()*20),27760)'; done
for i in `seq 1 1` ; do mysql -vvv -uroot test -e 'insert into department values (27760,"tbx")'; done

show index from employee;
+----------+------------+----------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+
| table  | non_unique | key_name | seq_in_index | column_name | collation | cardinality | sub_part | packed | null | index_type | comment |
+----------+------------+----------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+
| employee |     1 | ind_l_d |      1 | lastname   | a     |    1349 |   null | null  | yes | btree   |     |
| employee |     1 | ind_did |      1 | departmentid | a     |    1349 |   null | null  | yes | btree   |     |
+----------+------------+----------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+

show index from department;
+------------+------------+----------+--------------+----------------+-----------+-------------+----------+--------+------+------------+---------+
| table   | non_unique | key_name | seq_in_index | column_name  | collation | cardinality | sub_part | packed | null | index_type | comment |
+------------+------------+----------+--------------+----------------+-----------+-------------+----------+--------+------+------------+---------+
| department |     1 | ind_d  |      1 | departmentid  | a     |    1001 |   null | null  | yes | btree   |     |
| department |     1 | ind_dn  |      1 | departmentname | a     |    1001 |   null | null  | yes | btree   |     |
+------------+------------+----------+--------------+----------------+-----------+-------------+----------+--------+------+------------+---------+

4. 构造一个bad case

   因为关联条件中mysql使用索引统计信息做成本预估,所以数据分布不均匀的时候,就容易做出错误的判断。简单的我们构造下面的案例:

   表和索引结构不变,按照下面的方式构造数据:

for i in `seq 1 10000` ; do mysql -uroot test -e 'insert into department values (600000*rand(),repeat(char(65+rand()*58),rand()*20))'; done
for i in `seq 1 10000` ; do mysql -uroot test -e 'insert into employee values (repeat(char(65+rand()*58),rand()*20),600000*rand())'; done

for i in `seq 1 1` ; do mysql -uroot test -e 'insert into employee values ("zhou",27760)'; done
for i in `seq 1 10` ; do mysql -uroot test -e 'insert into department values (27760,"tbx")'; done
for i in `seq 1 1000` ; do mysql -uroot test -e 'insert into department values (27760,repeat(char(65+rand()*58),rand()*20))';
done

explain
select *
from
 employee as a,department as b
where
   a.lastname = 'zhou'
 and b.departmentid = a.departmentid
 and b.departmentname = 'tbx';
+----+-------------+-------+------+-----------------+---------+---------+---------------------+------+-------------+
| id | select_type | table | type | possible_keys  | key   | key_len | ref         | rows | extra    |
+----+-------------+-------+------+-----------------+---------+---------+---------------------+------+-------------+
| 1 | simple   | a   | ref | ind_l_d,ind_did | ind_l_d | 43   | const        |  1 | using where |
| 1 | simple   | b   | ref | ind_d,ind_dn  | ind_d  | 5    | test.a.departmentid |  1 | using where |
+----+-------------+-------+------+-----------------+---------+---------+---------------------+------+-------------+

   可以看到这里,mysql执行计划对表department使用了索引ind_d,那么a表命中一条记录为(zhou,27760);根据b.departmentid=27760将返回1010条记录,然后根据条件departmentname = 'tbx'进行过滤。

   这里可以看到如果b表选择索引ind_dn,效果要更好,因为departmentname = 'tbx'仅仅返回10条记录,再根据条件a.departmentid=b.departmentid过滤之。

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网