本文通过一个案例来看看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过滤之。

这个案例中因为数据量很小,性能还相差不大,但如果生产环境中数据是千万或者亿级别的时候性能就会差非常非常非常大。通过简单的Hint可以解决这个问题。

 
 

0111mysql如何选择Join的顺序的更多相关文章

  1. SQL中的多表查询,以及JOIN的顺序重要么?

    说法是,一般来说,JOIN的顺序不重要,除非你要自己定制driving table. 示例: SELECT a.account_id, c.fed_id, e.fname, e.lname -> ...

  2. D3.js的基础部分之选择集的处理 过滤器、选择集的顺序、each()和call()的应用(v3版本)

    选择集的处理 : 过滤器 有时候需要根据绑定数据对某选择集的元素进行过滤,例如某公司,只对id大于100的员工进行奖励.某学校只选拔身高超过170cm的学生等.类似这样的问题,需要根据条件获取选择集的 ...

  3. Spark SQL如何选择join策略

    前言 众所周知,Catalyst Optimizer是Spark SQL的核心,它主要负责将SQL语句转换成最终的物理执行计划,在一定程度上决定了SQL执行的性能. Catalyst在由Optimiz ...

  4. 通过join方法顺序执行多个线程

    方法一:直接用多线程之间的通讯去解决 package com.toov5.test; import javax.imageio.ImageTypeSpecifier; class Res1{ char ...

  5. MySQL优化器cost计算

    记录MySQL 5.5上,优化器进行cost计算的方法. 第一篇: 单表的cost计算 数据结构: 1. table_share: 包含了表的元数据,其中索引部分: key_info:一个key的结构 ...

  6. CMU Database Systems - Query Optimization

    查询优化应该是数据库领域最难的topic 当前查询优化,主要有两种思路, Rules-based,基于先验知识,用if-else把优化逻辑写死 Cost-based,试图去评估各个查询计划的cost, ...

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

    转自http://www.jb51.net/article/67007.htm,感谢博主 本文通过一个案例来看看MySQL优化器如何选择索引和JOIN顺序.表结构和数据准备参考本文最后部分" ...

  8. Join的表顺序

    在今天的文章里,我想谈下SQL Server里一个非常有趣的话题:在表联接里,把表指定顺序的话是否有意义?每次我进行查询和性能调优的展示时,大家都会问我他们是否应该把联接中的表指定下顺序,是否会帮助查 ...

  9. MySQL优化器join顺序

    前一篇介绍了cost的计算方法,下面测试一下两表关联的查询: 测试用例 CREATE TABLE `xpchild` ( `id` int(11) NOT NULL, `name` varchar(1 ...

随机推荐

  1. 寻找不到iframe元素

    一直找不到元素,是因为有两层iframe的 找iFrame元素方法如下 1.iFrame有ID 或者 name的情况//进入id="frame1"的frame中,定位id=&quo ...

  2. linux下的开源移动图像监测程序--motion编译与配置【转】

    本文转载自:http://www.cnblogs.com/qinyg/p/3355707.html 前几天在网上偶然看到一篇博客,是利用linxu下的开源的motion搭建嵌入式视频动态监控系统,感觉 ...

  3. bzoj4887: [Tjoi2017]可乐

    一眼暴力宽搜(最近比赛想暴力想疯了... 很明显的矩乘,然后自爆可以看作走向向一个无出边的点 然后没啥难的了吧. #include<cstdio> #include<iostream ...

  4. [codeforces contest 1119 F] Niyaz and Small Degrees 解题报告 (树形DP+堆)

    interlinkage: http://codeforces.com/contest/1119/problem/F description: 有一颗$n$个节点的树,每条边有一个边权 对于一个$x$ ...

  5. js设计模式-工厂模式(XHR工厂)

    场景:如果代码中需要多次执行Ajax请求,那么明智的做法是把创建这种对象的代码提取到一个类中,并创建一个包装器来包装在实际请求时所要经历的一系列步骤.简单工厂非常适合这种场合. /*AjaxHandl ...

  6. Python基本数据类型之字典dict

    字典dict 是一个键(key)值(value)对,结构为{},大括号 创建字典 ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 info = { # k ...

  7. Hashlib 用户名密码加密 2.0

    #!/usr/bin/env python# -*- coding: utf-8 -*-# @Time : 2018/7/10 0008 11:44# @Author : Anthony.Waa# @ ...

  8. 利用js实现进入页面首先执行刷新操作,且只刷新一次

    让页面进行刷新,可以使用location.reload()方法,但是这种方法会让页面一直不断的刷新,这是因为当页面加载完成以后,我们让它刷新一次,那么浏览器就会重新向服务器请求数据, 界面会重新加载, ...

  9. Android ToolBar标题文字居中的方法

    在项目的开发中,使用苹果手机的产品,出的界面效果图极有可能(我这里是一定)完全是按照苹果的界面风格来出的,例如界面顶部的title文字位置是水平居中 如图: 那么问题来了,当我们使用ToolBar控件 ...

  10. ubuntu下svn up 出现 Can't convert string from 'UTF-8' to native encoding

    root@ubuntu:/data/www# svn up svn: warning: cannot set LC_CTYPE locale svn: warning: environment var ...