当MYSQL 收到一条查询请求时,会首先通过关键字对SQL语句进行解析,生成一颗“解析树”,然后预处理器会校验“解析树”是否合法(主要校验数据列和表明是否存在,别名是否有歧义等),当“解析树”被认为合法后,查询优化器会对这颗“解析树”进行优化,并确定它认为最完美的执行计划。
MYSQL查询优化器衡量某个执行计划是否完美的标准是“使用该执行计划时的成本”,该成本的最小单位是读取一个4K数据页的成本。
下面图中的数据说明,当使用id为条件查询时,查询的成本只有一个数据页,而使用mean(非索引)查询时,成本将近是22334个数据页
下面列出了一些优化器常用的优化手段,但实际上远远不止这些
1. 重新定义关联表的顺序
对于多表关联的查询(INTER JOIN),优化器会根据数据的选择性来重新决定关联的顺序,选择性高的会被置前。
如果关联设计到N张表,优化器会尝试N!种的关联顺序,从中选出一种最优的排列顺序,如果有10张表进行关联,就有3628800种排序的可能,优化器可能需要经过3628800次的尝试才能得到一个最优的顺序。面对这种数量很大的排序任务,优化器并没有老老实实的尝试3628800次,而是当达到optimizer_search_depth指定的次数后,就会采用“贪婪模式”进行处理。这也表明关联表的数量不能太多
此功能可以通过STRAIGHT_JOIN关键字来进行屏蔽
2. 排序优化
当不能使用索引进行排序时,MYSQL会对结果集进行排序,这时候会采取两种策略:(1)如果结果集的容量小于“排序缓冲区”的容量,在内存中进行排序(2)如果查询的结果大于“排序缓冲区”,则先将结果集拆分成多个“排序缓冲区”可以容纳的子集,然后把各个子集排序的结果存放在磁盘上,最后对各个子集进行合并
在排序的过程中使用临时表的存储空间可能要比实际的存储空间大很多,主要是因为在排序的时候都会为每个字段保留最大的存储空间
当进行关联查询排序时,如果order by的字段全部来自第一张表,则在对第一张表进行关联处理时,就会进行排序动作(Extra 中会包含Using filesort),否则会对多表关联后的结果进行排序(Extra 中包含,Using temporary, Using filesort)。
在MYSQL5.6 之前的版本中, LIMIT关键字的作用只会在排序完之后才生效,所以即使在查询中包含了LIMIT,查询还是会对大量的数据进行处理
3. 等价规则
例如 出现 where 5=5 and a>5 会转化成where a>5
4. COUNT(),MIN(),MAX()
对于B-Tree索引而言,Max()/Min()的结果分别返回的是二叉树中最左边以及最右边的值,所以不需要进行表的访问就可以直接取到对应的值。
对于Count()函数而言,在MYISAM引擎中维护了一个对应的常量值,也不需要对表进行访问就可以直接取到Count的值。
经过这种优化过的SQL,在EXTRA中会出现 “Selecttables optimized away”的字样
5. 转化为常数表达式
首先要说明的是,在数据库对查询进行处理的时候, 以常数(Constant)的方式进行处理的速度是最快的。查询优化器在优化的过程中,如果发现一个表达式可以转换为常数,就会将表达式转换为常数进行处理。
在优化阶段,一个查询也可以转换为常数,例如 在索引列上执行Min(),在where中对主键或者是唯一键进行条件限制等。
6. 覆盖索引扫描
7. 提前终止
在下列几种情况中,查询会提前终止,并不再对表进行扫描
· 当优化器发现查询的结果已经满足查询需求的时候。比如查询中用到了LIMIT
· Where的条件不成立的时候。例如 where id>100 and id <10
8. 等值传播
对于通过列关联的查询,某列的where条件可以自动的从一张表传递到另外一张表,例如
Select film.filmid from film
Inter join film_actor using (filmid)
Where film.filmid>50;
上面的查询只是显示的指出film.filmid>50,但是优化器在优化的工程中会将其转化为
Where film.filmid>50 and film_actor.filmid>50.
9. 列表IN()的比较
…..where id in(2,4,1,3,8,6) 这种类型的限制条件在很多的RDBMS中等同于
where id=2 or id=4 or id=3 or id=8 or id=6. 这种算法的复杂度是O(n).
而在MYSQL中,首先会对In列表进行排序,然后通过二分查找的方式进行比较,该方式的算法复杂度是O(log n).如果IN列表中的数据量非常的大,则效果会非常的明显
在“查询优化器常用的方式”一文中列出了一些优化器常用的优化手段。查询优化器在提供这些特性的同时,也存在一定的局限性,这些局限性往往会随着MYSQL版本的升级而得到改善,所以本文会列出一些常见的局限性,且不包含所有的。
描述:
因为select …from table1 t1 where t1.id in(select t2.fk from table2 t2 wheret2.id=’…’) 类型的语句往往会被优化成 select …. From table1 t1 where exists (select* from table2 t2 where t2.id=’…’ and t2.fk=t1.id), 由于在进行tabl2查询时, table1的值还无法确定, 所以会对table1进行全表扫描
解决方案:
尽量用 INNER JOIN 替代 IN(),重写成 select * from table1 t1 inner jointable2 t2 using (id) where t2.id=’…’
描述:
UNION操作不会把UNION外的操作推送到每个子集
解决方案:
为每个子操作单独的添加限制条件
例如 学生表有10000条记录,会员表有10000表记录,如果想按照姓名排序取两个表的前20条记录,如果在各个子查询中添加limit的话,则最外层的limit操作将会从40条记录中取20条,否则是从20000条中取20条
在进行查询操作的时候 IN,ON,Using,等操作往往会把一个列表的值在多个表之间共享,而优化器为了优化的方便会把列表里的值为每个相关表都拷贝一份,如果这个列表非常的大,会对性能造成一定的影响.
目前为止还没有好的策略应对这个问题
目前為止,MYSQL不支持
目前MYSQL唯一支持的是循环嵌套关联,不支持HASH关联
描述:所谓的松散索引就是当对表进行扫描是,可以智能的跳过一些记录,以此来减少需要扫描的记录行数.为了更清楚的说明这个问题,举个例子来说明松散索引扫描的好处,例如table1表上有索引(a,b),执行 select * from tabl1 where b between2 and 3时,支持/不支持松散扫描的表扫描方式分别如下
由于B列是按照顺序排列的,所以只需要在固定的区间内查找就可以了,其余的记录可以跳过
B不是索引的第一字段,所以只能从第一条找到最后一条
上面两个图可以很明显的说明松散索引的好处,但是Mysql对这个特性的支持不是很好,只针对某些特殊的查询才提供此优化,具体的要看各个版本的手册
问题描述:
当执行 select max(id) from table1 where name=’sun’ 时,如果name没有建立相应的索引,MYSQL会进行全表扫描
解决方案:
将SQL等同的转化为
这样的语句会尽可能少的扫描表记录
问题描述:
不能在查询某个表的同时对表进行更新
否则会抛出异常: ERROR 1093 (HY000): You can'tspecify target table 'ftsexchangerate' for update in FROM clause
解决办法: 转化成关联表的形式
总结一下常见查询语句的优化方式
1. COUNT的作用
· COUNT(table.filed)统计的该字段非空值的记录行数
· COUNT(*)或者是COUNT(not nullable field) 统计的是全表的行数
如果要是统计全表记录数,COUNT(*)效率会比COUNT(not nullable field)要高一点
2. MYISAM的COUNT
一般执行COUNT操作时需要扫描大量的记录,但是在MyISAM引擎的数据库中,数据库把表的记录数保存起来,所以COUN(*)会非常的快(前提是不包含where条件)
3. 当需要频繁的使用COUNT时,可以考虑使用汇总表的策略
4. 优化小例子
在MYISAM中进行范围查询时,可以减少检索行数的小技巧
原始的:select count(*) from dictionary where id>5.
优化后:select (select count(*) fromdictionary)-count(*) from dictionary where id<=5
减少查询次数
优化前:需要两条语句
Select count(*)from student where area=’SH’
Select count(*)from student where area=’BJ’
优化后:合并成一条
select count(area='SH') as shcount, count(area='BJ') as bjcount from student;
1. 确保ON或USING的字句上有索引
2. 一般情况下只需要在第二个表上创建索引
3. 尽量使 Group by/Order by的表达式中只包含一个表的字段
尽量用关联代替子查询
1. 当对关联查询执行group by操作时,使用查询表的标识列作为分组条件效率会比较高
2. 当需要查询的非group by指定的字段时,正常情况下是无法执行的,可以通过inner join 的形式来弥补
3. group by默认会对查询的结果进行排序,数据量很大的时候可能会比较耗资源,如果你不关心查询结果的顺序,可以通过order by null 避免这种不必要的浪费
在进行分页查询的时候往往是采用select * from table1 limit 100,20 的方式来提取数据,在处理的过程中会读取120条数据,然后扔掉100条的offset记录,最后返回20条记录给客户端。如果offset的值非常大,效率上可能会有影响,可以尝试
1. 可以通过覆盖索引+inner join的方式来重写sql
2. 如果可以计算出明确的开始点和结束点,可以转换成 between and 的方式,这种方式只会扫描指定的行数,效率比较高
Select * from table1 between 100 and 120.
3. 可以通过位置标签的方式,来减少需要检索的记录数
例如 从某个位置开始。 Select * from table1 whereid>100 limit 20
下图列出了三种方式的效率对比
在进行分页处理的时候往往需要知道记录的总数,然后用这些总数生成页码。获取总数往往是使用count或是伴随一次全表查询得到的,这个过程也是检索所有的记录,然后再丢掉。为了避免这种浪费可以采取两种策略
· 把页码换成“下一页”的方式,这样就只需要去取固定的条数
· 一次性读取1000条,当一千条使用完后,采用“获取更多记录”的方式再获取1000条
· 使用的时候要把每个优化手段下推到每个子集中
· Union操作会对处理后的结果执行distinct操作,这在很多时候是没有必要的。可以采用union all来避免这个问题
合理灵活的使用自定义变量往往会给程序的性能带来意想不到的效果,但往往也会带来与其他数据库系统的兼容性问题。
下面列出几个自定义变量使用的小例子
· 行号
mysql> set @rownumber:=0;
mysql> select mean, @rownumber:=@rownumber+1 from dictionary limit10;
· 避免重复查询刚刚更新的数据
在更新完一条记录后,往往需要再次执行select查询刚刚更新过的记录
通过变量可以避免这种问题
Mysql>set @updaterow:=null;
mysql> update dictionary set mean='update get variable' where id=100and @updaterow:=now();
· 统计更新和插入的数量
mysql> set @x:=0; //define avariable
mysql> insert into dictionary (id,mean) values(3,'duplicate') onduplicate key update mean=values(mean)+(0*(@x:=@x+1)); //insert a duplicaterecord
mysql> select @x; //get x value, it’s indicator duplicate times
有时候可以借助专门的查询分析工具来发现自己的问题,比如pt-query-advisor(http://www.percona.com/doc/percona-toolkit/2.1/pt-query-advisor.html)