优化基础_explain详解
explain命令可以帮助我们了解mysql的执行计划,当运行SQL语句时哪种策略预计会被优化器采用。
explain 输出字段详解
1 | mysql> explain select * from servers; |
expain出来的信息有10列,分别是id、select_type、table、type、possible_keys、key、key_len、ref、rows、Extra,下面对这些字段出现的可能进行解释:
id
执行编号,标识select所属的行。如果在语句中没子查询或关联查询,只有唯一的select,每行都将显示1。否则,内层的select语句一般会顺序编号,对应于其在原始语句中的位置
select_type
显示本行是简单或复杂select。如果查询有任何复杂的子查询,则最外层标记为PRIMARY。
- simple 简单子查询,不包含子查询和union
- primary 包含union或者子查询,最外层的部分标记为primary
- subquery 一般子查询中的子查询被标记为subquery,也就是位于select列表中的查询
- derived 派生表——该临时表是从子查询派生出来的,位于form中的子查询
- union 位于union中第二个及其以后的子查询被标记为union,第一个就被标记为primary如果是union位于from中则标记为derived
- union result 用来从匿名临时表里检索结果的select被标记为union result
- dependent union 顾名思义,首先需要满足UNION的条件,及UNION中第二个以及后面的SELECT语句,同时该语句依赖外部的查询
- subquery 子查询中第一个SELECT语句
- dependent subquery 和DEPENDENT UNION相对UNION一样
table
显示这一行的数据是关于哪张表的
type
表示MySQL在表中找到所需行的方式,又称“访问类型”。
常用的类型有: ALL, index, range, ref, eq_ref, const, system, NULL(从左到右,性能从差到好)
- ALL:Full Table Scan, MySQL将遍历全表以找到匹配的行
- index: Full Index Scan,index与ALL区别为index类型只遍历索引树
- range:只检索给定范围的行,使用一个索引来选择行
- ref: 表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值
- eq_ref: 类似ref,区别就在使用的索引是唯一索引,对于每个索引键值,表中只有一条记录匹配,简单来说,就是多表连接中使用primary key或者 unique key作为关联条件
- const/system:当MySQL对查询某部分进行优化,并转换为一个常量时,使用这些类型访问。如将主键置于where列表中,MySQL就能将该查询转换为一个常量,system是const类型的特例,当查询的表只有一行的情况下,使用system
- NULL: MySQL在优化过程中分解语句,执行时甚至不用访问表或索引,例如从一个索引列里选取最小值可以通过单独索引查找完成。
possible_keys
指出MySQL能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用
Key
key列显示MySQL实际决定使用的键(索引)
如果没有选择索引,键是NULL。要想强制MySQL使用或忽视possible_keys列中的索引,在查询中使用FORCE INDEX、USE INDEX或者IGNORE INDEX。
key_len
key_len列显示MySQL决定使用的键长度。如果键是NULL,则长度为NULL。使用的索引的长度。在不损失精确性的情况下,长度越短越好 。
ref
表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值
rows
表示MySQL根据表统计信息及索引选用情况,估算的找到所需的记录所需要读取的行数。
Extra
该列包含MySQL解决查询的详细信息,有以下几种情况:
Using where:意味着全表扫描或者在查找使用索引的情况下,但是还有查询条件不在索引字段当中,表示mysql服务器将在存储引擎检索行后再进行过滤
1.查询条件中的相关列,不是索引字段, 全表扫描后,通过Using where过滤获取所需的数据。
2.(type=ref)非唯一性索引扫描,但是由于索引未覆盖所有查询条件(字段d并未包含在聚集索引PRIMARY中),在存储引擎返回记录后,仍然需要过滤数据
3. WHERE筛选条件不是索引的前导列,导致不走索引,而走全表扫描。Using index:表示直接访问索引就能够获取到所需要的数据(索引覆盖),不需要通过索引回表。
Using index condition:Extra为Using Index Condition 表示会先条件过滤索引,过滤完索引后找到所有符合索引条件的数据行,随后用 WHERE 子句中的其他条件去过滤这些数据行。(使用索引下推)
Using temporary:表示MySQL需要使用临时表来存储结果集,常见于排序和分组查询
Using filesort:MySQL中无法利用索引完成的排序操作称为“文件排序”
Using join buffer:改值强调了在获取连接条件时没有使用索引,并且需要连接缓冲区来存储中间结果。如果出现了这个值,那应该注意,根据查询的具体情况可能需要添加索引来改进能。
Impossible where:这个值强调了where语句会导致没有符合条件的行。
Select tables optimized away:这个值意味着仅通过使用索引,优化器可能仅从聚合函数结果中返回一行
参考:
https://dev.mysql.com/doc/refman/5.7/en/explain-output.html
mysql查询优化器
查询优化器的局限性
UNION的限制
如果希望UNI0N的各个子句能够根据LIMT只取部分结果集,或者希望能够先排好序再合并结果集的话,就需要在 UNION的各个子句中分别使用这些子句。
1 | select first name last name |
这条查询将会把 actor中的200条记录和 customer表中的599条记录存放在一个临时表中,然后再从临时表中取出前20条。
优化方式:可以通过在UN0N的两个子查询中分别加上一个LIMIT20来减少临时表中的数据。
索引合并优化(Index merge optimization)
MySQL在 5.0版本中引入新特性:索引合并优化(Index merge optimization),当查询中单张表可以使用多个索引时,同时扫描多个索引并将扫描结果进行合并。
该新特性可以在一些场景中大幅度提升查询性能,但受限于MySQL糟糕的统计信息,也导致很多场景查询性能极差甚至导致数据库崩溃。
并行执行
MySQL无法利用多核特性来并行执行查询。很多其他的关系型数据库能够提供这个特性,但是 MySQL做不到。
最大值最小值优化
对于MIN()和MX()査询, MySQL的优化做得并不好。这里有一个例子:
1 | mysql> SELECT MIN(actor_id)FROM sakila. actor WHERE first name ='PENELOPE'; |
因为在 first name字段上并没有索引,因此 My SQL将会进行一次全表扫描。如果MySQL能够进行主键扫描,那么理论上,当MyQL读到第一个满足条件的记录的时候,就是我们需要找的最小值了,因为主键是严格按照 actor id字段的大小顺序排列的。但是 MySQL这时只会做全表扫描。
在同一个表上查询和更新
MySQL不允许对同一张表同时进行查询和更新。下面是一个无法运行的SQL,虽然这是一个符合标准的SQL语句。
1 | mysql> UPDATE tbl as outer_tbl |
可以利用临时表绕过
1 | mysql> UPDATE tbl |
优化器提示(hint)
如果对优化器选择的执行计划不满意,可以使用优化器提供的几个提示(hint)来控制最终的执行计划。
STRAIGHT JOIN
这个提示可以放置在 SELECT语句的 SELECT关键字之后,也可以放置在任何两个关联表的名字之间。
第一个用法是让查询中所有的表按照在语句中出现的顺序进行关联。
第二个用法则是固定其前后两个表的关联顺序。
MySQL可能会花费大量时间在“ statistics”状态,加上这个提示则会大大减少优化器的搜索空间。
SQL_SMALL_RESULT 和 SQL_BIG_RESULT
这两个提示只对 SELECT语句有效。它们告诉优化器对 GROUP BY或者 DISTINCT查询如何使用临时表及排序。 SQL_SMALL_RESULT告诉优化器结果集会很小,可以将结果集放在内存中的索引临时表,以避免排序操作。
如果是 SQL_BIG_RESULT,则告诉优化器结果集可能会非常大,建议使用磁盘临时表做排序操作。
FOR UPDATE 和 LOCK IN SHARE MODE
严格来说这两个不是优化器提示
这两个提示主要控制 SELECT语句的锁机制,但只对实现了行级锁的存储引擎有效。
唯一内置的支持这两个提示的引擎就是 InnodB。另外需要记住的是,这两个提示会让某些优化无法正常使用,例如索引覆盖扫描。
USE INDEX、 IGNORE INDEX 和 FORCE INDEX
这几个提示会告诉优化器使用或者不使用哪些索引来查询记录。
在MyQL5.1和之后的版本可以通过新增选项FOR ORDER BY 和FOR GROUP BY来指定是否对排序和分组有效。
FORCE INDEX和 USE INDEX基本相同。
当发现优化器选择了错误的索引,可以使用该提示。
慢sql优化
如何发现慢sql?
- 带有慢sql统计的数据库连接池 如druid连接池
- mysql自带慢sql日志
- 中间件监控,如cat
慢sql排查(优化数据访问)
优化慢sql主要从两点出发:
- 是否请求了不需要的数据
- 是否扫描额外记录
是否请求了不需要的数据?
一个表中存在text字段,但是这个字段一般情况下不需要访问,但是业务代码中每次都请求了这个字段,造成大量冗余数据返回,产生慢sql。
是否扫描额外记录
这个主要是用explain去评估sql扫描的行数,和执行计划。然后通过重写查询的方式来实现优化。(重写查询的方式下文会详细说明)
优雅地使用索引
利用索引对mysql进行优化是最常见的优化方式。
- 利用覆盖索引避免回表
- 利用索引排序
- 利用索引下推 在索引上过滤数据
只有当索引的列顺序和ORDER BY子句的顺序完全一致,并且所有列的排序方向(倒序或正序)都一样时, MySQL才能够使用索引来对结果做排序。
如果查询需要关联多张表,则只有当ORDER BY子句引用的字段全部为第一个表时,才能使用索引做排序。
ORDER BY子句和查找型查询的限制是一样的:需要满足索引的最左前缀的要求;否则,MySQL都需要执行排序操作,而无法利用索引排序。
索引如果存在范围查询,那么索引有序性无法利用,如:WHERE a>10 ORDER BY b; 索引a_b无法排序。
使用前缀索引
MySQL是支持前缀索引的,你可以定义字符串的一部分作为索引。默认地,如果你创建索引的语句不指定前缀长度,那么索引就会包含整个字符串。
使用前缀索引就用不上覆盖索引对查询性能的优化了,这也是你在选择是否使用前缀索引时需要考虑的一个因素。
字符串本身前缀区分度不够的问题
- 倒序存储,再创建前缀索引,用于绕过字符串本身前缀的区分度不够的问题;
- 是使用hash字段。你可以在表上再创建一个整数字段,来保存身份证的校验码,同时在这个字段上创建索引。
- 直接创建完整索引,这样可能比较占用空间;
索引失效
索引失效指优化器放弃走索引的情况。
对索引字段做函数操作
1 | select count(*) from tradelog where month(t_modified)=7; |
对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能。
1 | select * from tradelog where id + 1 = 10000 |
对于不改变有序性的函数,也不会考虑使用索引。对于这个SQL语句,这个加1操作并不会改变有序性,但是MySQL优化器还是不能用id索引快速定位到9999这一行。所以,需要你在写SQL语句的时候,手动改写成 where id = 10000 -1才可以。
隐式类型转换导致索引失效
gmt_modified > ‘2020-05-01 00:00:00’ 会将字符串转为时间戳,会走索引
1 | 错误的例子:select * from test where tu_mdn=13333333333; |
如何避免隐式类型转换?
只有当清楚的知道隐式类型转换的规则,才能从根本上避免产生隐式类型转换。
隐式字符编码转换
1 | CREATE TABLE `tradelog` ( |
字符集utf8mb4是utf8的超集,所以当这两个类型的字符串在做比较的时候,MySQL内部的操作是,先把utf8字符串转成utf8mb4字符集,再做比较。
导致没有走trade_detail表的tradeid索引
其他索引失效
- mysql中使用不等于!= 或者<> 会走全表扫描
- 索引字段上使用 is null / is not null 判断时,会导致索引失效而转向全表扫描
- like “%abc”,通配符在最左侧
排序优化
无论如何排序都是一个成本很高的操作,所以从性能角度考虑,应尽可能避免排序或者尽可能避免对大量数据进行排序。
当不能使用索引生成排序结果的时候,My SQL需要自己进行排序,如果数据量小则在内存中进行,如果数据量大
则需要使用磁盘,不过 MySQL将这个过程统一称为文件排序( filesort),即使完全是内存排序不需要任何磁盘文件时也是如此。
如果需要排序的数据量小于“排序缓冲区”, My SQL使用内存进行“快速排序”操作。如果内存不够排序,那么 MySQL会先将数据分块,对每个独立的块使用“快速排序”进行排序,并将各个块的排序结果存放在磁盘上,然后将各个排好序的块进行合并( merge),最后返回排序结果。
mysql有两种排序算法:
- 全字段排序(单次传输排序)
- rowid排序(两次传输排序)
定义一个表:
1 | CREATE TABLE `t` ( |
全字段排序
全字段排序执行流程如下
- 初始化sort_buffer,确定放入name、city、age这三个字段;
- 从索引city找到第一个满足city=’杭州’条件的主键id,也就是图中的ID_X;
- 到主键id索引取出整行,取name、city、age三个字段的值,存入sort_buffer中;
- 从索引city取下一个记录的主键id;
- 重复步骤3、4直到city的值不满足查询条件为止,对应的主键id也就是图中的ID_Y;
- 对sort_buffer中的数据按照字段name做快速排序;
- 按照排序结果取前1000行返回给客户端。
先读取查询所需要的所有列,然后再根据给定列进行排序,最后直接返回排序结果。

rowid排序
如果sort_buffer里面要放的字段数太多,这样内存里能够同时放下的行数很少,要分成很多个临时文件,排序的性能会很差。
max_length_for_sort_data,是MySQL中专门控制用于排序的行数据的长度的一个参数。它的意思是,如果单行的长度超过这个值,MySQL就认为单行太大,要换一个算法。
rowid排序执行流程:
- 初始化sort_buffer,确定放入两个字段,即name和id;
- 从索引city找到第一个满足city=’杭州’条件的主键id,也就是图中的ID_X;
- 到主键id索引取出整行,取name、id这两个字段,存入sort_buffer中;
- 从索引city取下一个记录的主键id;
- 重复步骤3、4直到不满足city=’杭州’条件为止,也就是图中的ID_Y;
- 对sort_buffer中的数据按照字段name进行排序;
- 遍历排序结果,取前1000行,并按照id的值回到原表中取出city、name和age三个字段返回给客户端
读取行指针和需要排序的字段,对其进行排序,然后再根据排序结果读取所需要的数据行。
优化
mysql排序优化主要从这几个方面入手
- 是否能用索引排序
- 控制sort_buffer大小和max_length_for_sort_data大小
重构查询
切分查询
例如一条删除语句,需要删除10W条数据,为了避免一次锁住很多数据,占满事务日志导致阻塞,可以切分为多个删除语句,每次删除1000条。
分解关联查询
如果查询非常复杂,可以考虑将复杂查询拆分为多个简单查询,在应用层做关联,因为应用层可以通过加机器来线性提升性能,但是数据库的资源比较宝贵,无法直接扩展。
- 让缓存的效率更高。许多应用程序可以方便地缓存单表查询对应的结果对象。
- 将查询分解后,执行单个查询可以减少锁的竞争。
- 在应用层做关联,可以更容易对数据库进行拆分,更容易做到高性能和可扩展。
- 查询本身效率也可能会有所提升。
- 可以减少冗余记录的查询。在应用层做关联查询,意味着对于某条记录应用只需要查询一次,而在数据库中做关联查询,则可能需要重复地访问一部分数据。
- 这样做相当于在应用中实现了哈希关联,而不是使用 MySQL的嵌套循环关联。
优化特定类型的查询
优化Limit分页
可以使用延迟关联,来优化limit分页
1 |
|
有时候也可以将Limit查询转换为已知位置的查询,让 MySQL通过范围扫描获得到对应的结果。
优化UNION查询
MySQL总是通过创建并填充临时表的方式来执行UNI0N査询。因此很多优化策略在UNION查询中都没法很好地使用。经常需要手工地将 WHERE、 LIMIT、ORDER BY等子句“下
推”到UNI0N的各个子查询中,以便优化器可以充分利用这些条件进行优化
除非确实需要服务器消除重复的行,否则就一定要使用UNION ALL,这一点很重要。
如果没有ALL关键字, MySQL会给临时表加上 DISTINCT选项,这会导致对整个临时表的数据做唯一性检查。这样做的代价非常高。
优化关联查询
- 用小表做驱动表
- 被关联的表要建好索引
优化COUNT()查询
在不同的MySQL引擎中,count(*)有不同的实现方式。
MyISAM引擎把一个表的总行数存在了磁盘上,因此执行count(*)的时候会直接返回这个数,效率很高;
InnoDB是索引组织表,主键索引树的叶子节点是数据,而普通索引树的叶子节点是主键值。所以,普通索引树比主键索引树小很多。对于count(*)这样的操作,遍历哪个索引树得到的结果逻辑上都是一样的。因此,MySQL优化器会找到最小的那棵树来遍历。
不同的count用法
count()是一个聚合函数,对于返回的结果集,一行行地判断,如果count函数的参数不是NULL,累计值就加1,否则不加。最后返回累计值。
count(*)、count(主键id)和count(1) 都表示返回满足条件的结果集的总行数;而count(字段),则表示返回满足条件的数据行里面,参数“字段”不为NULL的总个数。
至于分析性能差别的时候,你可以记住这么几个原则:
- server层要什么就给什么;
- InnoDB只给必要的值;
- 现在的优化器只优化了count(*)的语义为“取行数”,其他“显而易见”的优化并没有做。
对于count(主键id)来说,InnoDB引擎会遍历整张表,把每一行的id值都取出来,返回给server层。server层拿到id后,判断是不可能为空的,就按行累加。
对于count(1)来说,InnoDB引擎遍历整张表,但不取值。server层对于返回的每一行,放一个数字“1”进去,判断是不可能为空的,按行累加。
单看这两个用法的差别的话,你能对比出来,count(1)执行得要比count(主键id)快。因为从引擎返回id会涉及到解析数据行,以及拷贝字段值的操作。
对于count(字段)来说:
- 如果这个“字段”是定义为not null的话,一行行地从记录里面读出这个字段,判断不能为null,按行累加;
- 如果这个“字段”定义允许为null,那么执行的时候,判断到有可能是null,还要把值取出来再判断一下,不是null才累加。
但是count(*)是例外,并不会把全部字段取出来,而是专门做了优化,不取值。
按照效率排序的话,count(字段)<count(主键id)<count(1)≈count(*)。
distinct 和 group by的性能比较
如果表t的字段a上没有索引,那么下面这两条语句:
1 | select a from t group by a order by null; |
这种group by的写法,并不是SQL标准的写法。标准的group by语句,是需要在select部分加一个聚合函数。
1 | select a,count(*) from t group by a order by null; |
没有了count(*)以后,也就是不再需要执行“计算总数”的逻辑时,第一条语句的逻辑就变成是:按照字段a做分组,相同的a的值只返回一行。而这就是distinct的语义,所以不需要执行聚合函数时,distinct 和group by这两条语句的语义和执行流程是相同的,因此执行性能也相同。
这两条语句的执行流程是下面这样的。
- 创建一个临时表,临时表有一个字段a,并且在这个字段a上创建一个唯一索引;
- 遍历表t,依次取数据插入临时表中:
- 如果发现唯一键冲突,就跳过;
- 否则插入成功;
- 遍历完成后,将临时表作为结果集返回给客户端
mysql配置优化
mysql配置主要交给dba来配置,一般不会出现问题,如果出现问题则需要根据实际问题,来判断和哪个mysql配置有关,并查看相关配置。主要是这么几个方面,脏页的缓存池大小,刷盘的速度,一般频繁刷盘会导致mysql抖动。