写在最前:
1、慢SQL可能会消耗了70%~90%的数据库CPU资源
2、SQL语句独立于程序设计逻辑,相对于对程序源代码的优化,对SQL语句的优化在时间成本和风险上的代价都很低
3、SQL语句可以有不同的写法
一、MySQL EXPLAIN 详解
EXPLAIN Output Columns
列名 |
说明 |
id |
执行编号,标识select所属的行。如果在语句中没子查询或关联查询,只有唯一的select,每行都将显示1。否则,内层的select语句一般会顺序编号,对应于其在原始语句中的位置 |
select_type |
显示本行是简单或复杂select。如果查询有任何复杂的子查询,则最外层标记为PRIMARY(DERIVED、UNION、UNION RESUlT) |
table |
访问引用哪个表(引用某个查询,如“derived3”) |
type |
数据访问/读取操作类型(ALL、index、range、ref、eq_ref、const/system、NULL) |
possible_keys |
揭示哪一些索引可能有利于高效的查找 |
key |
显示mysql决定采用哪个索引来优化查询 |
key_len |
显示mysql在索引里使用的字节数 |
ref |
显示了之前的表在key列记录的索引中查找值所用的列或常量 |
rows |
为了找到所需的行而需要读取的行数,估算值,不精确。通过把所有rows列值相乘,可粗略估算整个查询会检查的行数 |
Extra |
额外信息,如using index、filesort等 |
1、id
id是用来顺序标识整个查询中SELELCT 语句的,在嵌套查询中id越大的语句越先执行。该值可能为NULL,如果这一行用来说明的是其他行的联合结果。
2、select_type
表示查询的类型
类型 |
说明 |
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语句,同时该语句依赖外部的查询 |
dependent subquery |
和DEPENDENT UNION相对UNION一样 |
3、table
对应行正在访问哪一个表,表名或者别名:
- 关联优化器会为查询选择关联顺序,左侧深度优先
- 当from中有子查询的时候,表名是derivedN的形式,N指向子查询,也就是explain结果中的下一列
- 当有union result的时候,表名是union 1,2等的形式,1,2表示参与union的query id
注意:MySQL对待这些表和普通表一样,但是这些“临时表”是没有任何索引的。
4、type
type显示的是访问类型,是较为重要的一个指标,结果值从好到坏依次是:
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL ,一般来说,得保证查询至少达到range级别,最好能达到ref。
类型 |
说明 |
All |
最坏的情况,全表扫描 |
index |
和全表扫描一样。只是扫描表的时候按照索引次序进行而不是行。主要优点就是避免了排序, 但是开销仍然非常大。如在Extra列看到Using index,说明正在使用覆盖索引,只扫描索引的数据,它比按索引次序全表扫描的开销要小很多 |
range |
范围扫描,一个有限制的索引扫描。key 列显示使用了哪个索引。当使用=、 <>、>、>=、<、<=、IS NULL、<=>、BETWEEN 或者 IN 操作符,用常量比较关键字列时,可以使用 range |
ref |
一种索引访问,它返回所有匹配某个单个值的行。此类索引访问只有当使用非唯一性索引或唯一性索引非唯一性前缀时才会发生。这个类型跟eq_ref不同的是,它用在关联操作只使用了索引的最左前缀,或者索引不是UNIQUE和PRIMARY KEY。ref可以用于使用=或<=>操作符的带索引的列。 |
eq_ref |
最多只返回一条符合条件的记录。使用唯一性索引或主键查找时会发生 (高效) |
const |
当确定最多只会有一行匹配的时候,MySQL优化器会在查询前读取它而且只读取一次,因此非常快。当主键放入where子句时,mysql把这个查询转为一个常量(高效) |
system |
这是const连接类型的一种特例,表仅有一行满足条件。 |
Null |
意味说mysql能在优化阶段分解查询语句,在执行阶段甚至用不到访问表或索引(高效) |
5、possible_keys
显示查询使用了哪些索引,表示该索引可以进行高效地查找,但是列出来的索引对于后续优化过程可能是没有用的。
6、key
key列显示MySQL实际决定使用的键(索引)。如果没有选择索引,键是NULL。要想强制MySQL使用或忽视possible_keys列中的索引,在查询中使用FORCE INDEX、USE INDEX或者IGNORE INDEX。
7、key_len
key_len列显示MySQL决定使用的键长度。如果键是NULL,则长度为NULL。使用的索引的长度。在不损失精确性的情况下,长度越短越好 。
8、ref
ref列显示使用哪个列或常数与key一起从表中选择行。
9、rows
rows列显示MySQL认为它执行查询时必须检查的行数。注意这是一个预估值。
10、Extra
Extra是EXPLAIN输出中另外一个很重要的列,该列显示MySQL在查询过程中的一些详细信息,MySQL查询优化器执行查询的过程中对查询计划的重要补充信息。
类型 |
说明 |
Using filesort |
MySQL有两种方式可以生成有序的结果,通过排序操作或者使用索引,当Extra中出现了Using filesort 说明MySQL使用了后者,但注意虽然叫filesort但并不是说明就是用了文件来进行排序,只要可能排序都是在内存里完成的。大部分情况下利用索引排序更快,所以一般这时也要考虑优化查询了。使用文件完成排序操作,这是可能是ordery by,group by语句的结果,这可能是一个CPU密集型的过程,可以通过选择合适的索引来改进性能,用索引来为查询结果排序。 |
Using temporary |
用临时表保存中间结果,常用于GROUP BY 和 ORDER BY操作中,一般看到它说明查询需要优化了,就算避免不了临时表的使用也要尽量避免硬盘临时表的使用。 |
Not exists |
MYSQL优化了LEFT JOIN,一旦它找到了匹配LEFT JOIN标准的行, 就不再搜索了。 |
Using index |
说明查询是覆盖了索引的,不需要读取数据文件,从索引树(索引文件)中即可获得信息。如果同时出现using where,表明索引被用来执行索引键值的查找,没有using where,表明索引用来读取数据而非执行查找动作。这是MySQL服务层完成的,但无需再回表查询记录。 |
Using index condition |
这是MySQL 5.6出来的新特性,叫做“索引条件推送”。简单说一点就是MySQL原来在索引上是不能执行如like这样的操作的,但是现在可以了,这样减少了不必要的IO操作,但是只能用在二级索引上。 |
Using where |
使用了WHERE从句来限制哪些行将与下一张表匹配或者是返回给用户。注意:Extra列出现Using where表示MySQL服务器将存储引擎返回服务层以后再应用WHERE条件过滤。 |
Using join buffer |
使用了连接缓存:Block Nested Loop,连接算法是块嵌套循环连接;Batched Key Access,连接算法是批量索引连接 |
impossible where |
where子句的值总是false,不能用来获取任何元组 |
select tables optimized away |
在没有GROUP BY子句的情况下,基于索引优化MIN/MAX操作,或者对于MyISAM存储引擎优化COUNT(*)操作,不必等到执行阶段再进行计算,查询执行计划生成的阶段即完成优化。 |
distinct |
优化distinct操作,在找到第一匹配的元组后即停止找同样值的动作 |
二、MySQL索引
1、索引原理
MySQL索引的建立对于MySQL的高效运行是很重要的,索引可以大大提高MySQL的检索速度。
打个比方,如果合理的设计且使用索引的MySQL是一辆兰博基尼的话,那么没有设计和使用以及不合理索引的MySQL就是一个人力三轮车。
索引分单列索引和组合索引。单列索引,即一个索引只包含单个列,一个表可以有多个单列索引,但这不是组合索引。组合索引,即一个索引包含多个列。
创建索引时,你需要确保该索引是应用在 SQL 查询语句的条件(一般作为 WHERE 子句的条件)。
实际上,索引也是一张表,该表保存了主键与索引字段,并指向实体表的记录。
上面都在说使用索引的好处,但过多的使用索引将会造成滥用。因此索引也会有它的缺点:虽然索引大大提高了查询速度,同时却会降低更新表的速度,如对表进行INSERT、UPDATE和DELETE。因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件。
建立索引会占用磁盘空间的索引文件。
本质都是:通过不断地缩小想要获取数据的范围来筛选出最终想要的结果,同时把随机的事件变成顺序的事件,也就是说,有了这种索引机制,我们可以总是用同一种查找方式来锁定数据。
2、磁盘IO与预读
考虑到磁盘IO是非常高昂的操作,计算机操作系统做了一些优化,当一次IO时,不光把当前磁盘地址的数据,而是把相邻的数据也都读取到内存缓冲区内,因为局部预读性原理告诉我们,当计算机访问一个地址的数据的时候,与其相邻的数据也会很快被访问到。每一次IO读取的数据我们称之为一页(page)。具体一页有多大数据跟操作系统有关,一般为4k或8k,也就是我们读取一页内的数据时候,实际上才发生了一次IO,这个理论对于索引的数据结构设计非常有帮助。
3、Mysql (InnoDB引擎)聚集索引和辅助索引
聚集索引(clustered index)就是按照每张表的主键构造一棵B+树,树中的叶子节点存放着表中的行记录数据,因此,也将聚集索引的叶子节点称为数据页;非叶子节点中存放着仅仅是键值和指向叶子节点的偏移量。每个叶子节点(数据页)都通过一个双向链表进行连接。
由于实际的数据页只能按照一棵B+树进行排序,因此数据库中每张表只能有一个聚集索引。
注意:聚集索引并不是在物理存储上是连续的,其只是在逻辑上连续,这有两点:
一、数据页是按照主键的顺序并通过双向链表链接的,因此物理存储上可以不按主键顺序存储。
二、数据页中的记录也是通过双向链表进行维护的,物理存储上同样可以不按主键顺序存储。
聚集索引的好处:
一、对于主键的排序查找非常的快(因为其叶子节点是用双向链表链接的)
二、对于主键的范围查找非常的快(因为通过叶子节点的上层中间节点,就可以得到叶结点的范围值)
辅助索引(Secondary index)也是B+树结构,但其在叶子节点中并不包含行记录的全部数据。除了包含键的值(建立辅助索引的列中的值)外,还包含了一个书签,这个书签用来告诉InnoDB引擎从哪里可以找到与索引相对应的行数据。由于InnoDB引擎是索引组织表,因此,这个书签就是相应的行数据的聚集索引键。
因为辅助索引不会对影响数据在聚集索引中的组织,所以可以有多个。
案例01:
SQL: |
# Time: 181009 16:37:04 # User@Host: mlsreader[mlsreader] @ [10.20.0.88] Id: 15580929 # Query_time: 170.911324 Lock_time: 0.000093 Rows_sent: 51 Rows_examined: 78319371 SET timestamp=1539074224; select * from `t_im_msg_1` where `msg_id` > 9 and ((`chatfrom` = '1200884314' and `chatto` != 0 and `is_del` = 0 and `channel_id` = '2') or (`chatto` = '1200884314' and `chatfrom` != 0 and `is_del` = 0 and `channel_id` = '2')) order by `msg_id` asc limit 51 offset 0; |
1、SQL分析
mysql> explain select * from `t_im_msg_1` where `msg_id` > 9 and ((`chatfrom` = '1200884314' and `chatto` != 0 and `is_del` = 0 and `channel_id` = '2') or (`chatto` = '1200884314' and `chatfrom` != 0 and `is_del` = 0 and `channel_id` = '2')) order by `msg_id` asc limit 51 offset 0;
+----+-------------+------------+-------+-----------------------+---------+---------+------+----------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+-------+-----------------------+---------+---------+------+----------+-------------+
| 1 | SIMPLE | t_im_msg_1 | range | PRIMARY,fromto,tofrom | PRIMARY | 8 | NULL | 59885538 | Using where |
+----+-------------+------------+-------+-----------------------+---------+---------+------+----------+-------------+
1.1 select_type = SIMPLE :简单子查询,不包含子查询和union;
1.2 type = range:范围扫描,一个有限制的索引扫描。key 列显示使用了哪个索引。当使用=、 <>、>、>=、<、<=、IS NULL、<=>、BETWEEN 或者 IN 操作符,用常量比较关键字列时;
1.3 possible_keys = PRIMARY,fromto,tofrom:显示查询使用了哪些索引,表示该索引可以进行高效地查找,但是列出来的索引对于后续优化过程可能是没有用的(略);
1.4 key = PRIMARY:key列显示MySQL实际决定使用的键(索引)为主键(PRIMARY);
1.5 key_len = 8:显示MySQL决定使用的键长度。如果键是NULL,则长度为NULL。使用的索引的长度。在不损失精确性的情况下,长度越短越好 。
1.6 rows = 59885538:为了找到所需的行而需要读取的行数,估算值,不精确;
1.7 Extra = Using where:mysql服务器将在存储引擎检索行后再进行过滤。就是先读取整行数据,再按 where 条件进行检查,符合就留下,不符合就丢弃。
2、查看表索引
PRIMARY KEY (`msg_id`), KEY `fromto` (`chatfrom`,`chatto`,`is_read`,`msg_id`), KEY `fromto2` (`fromid`,`toid`,`is_read`,`msg_id`), KEY `tofrom` (`chatto`,`chatfrom`,`is_read`,`msg_id`), KEY `tofrom2` (`toid`,`fromid`,`is_read`,`msg_id`), KEY `group` (`group_id`,`msg_id`), KEY `idx_ctime` (`ctime`) |
3、初步分析原因
3.1 SQL语句只使用了PRIMARY,
3.2 为了找到所需的行而需要读取的行数过多(59890096)
3.3 msg_id在组合索引尾,违背了最左原则
3.4 可能与使用了or有关
注意:例如 or 、in | not in 、is null | is not null、!=、<>,使用时并不是完全不走索引,要考虑到:
1、全表扫描是否比索引更快,以至于优化器选择全表扫描;
2、mysql-server 的版本。。。;
4、拆解SQL
mysql> explain select * from `t_im_msg_1` where `msg_id` > 9 and (`chatfrom` = '1200884314' and `chatto` != 0 and `is_del` = 0 and `channel_id` = '2');
+----+-------------+------------+-------+-----------------------+--------+---------+------+------+------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+-------+-----------------------+--------+---------+------+------+------------------------------------+
| 1 | SIMPLE | t_im_msg_1 | range | PRIMARY,fromto,tofrom | fromto | 16 | NULL | 73 | Using index condition; Using where |
+----+-------------+------------+-------+-----------------------+--------+---------+------+------+------------------------------------+
mysql> explain select * from `t_im_msg_1` where `msg_id` > 9 and (`chatto` = '1200884314' and `chatfrom` != 0 and `is_del` = 0 and `channel_id` = '2');
+----+-------------+------------+-------+-----------------------+--------+---------+------+------+------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+-------+-----------------------+--------+---------+------+------+------------------------------------+
| 1 | SIMPLE | t_im_msg_1 | range | PRIMARY,fromto,tofrom | tofrom | 16 | NULL | 220 | Using index condition; Using where |
+----+-------------+------------+-------+-----------------------+--------+---------+------+------+------------------------------------+
5、使用union
mysql> explain
select * from `t_im_msg_1`
where `msg_id` > 9
and (`chatto` = '1200884314' and `chatfrom` != 0 and `is_del` = 0 and `channel_id` = '2')
union
select * from `t_im_msg_1`
where `msg_id` > 9
and (`chatfrom` = '1200884314' and `chatto` != 0 and `is_del` = 0 and `channel_id` = '2')
order by `msg_id` asc
limit 51 offset 0;
+----+--------------+------------+-------+-----------------------+--------+---------+------+------+------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+--------------+------------+-------+-----------------------+--------+---------+------+------+------------------------------------+
| 1 | PRIMARY | t_im_msg_1 | range | PRIMARY,fromto,tofrom | tofrom | 16 | NULL | 220 | Using index condition; Using where |
| 2 | UNION | t_im_msg_1 | range | PRIMARY,fromto,tofrom | fromto | 16 | NULL | 73 | Using index condition; Using where |
| NULL | UNION RESULT | <union1,2> | ALL | NULL | NULL | NULL | NULL | NULL | Using temporary; Using filesort |
+----+--------------+------------+-------+-----------------------+--------+---------+------+------+------------------------------------+
执行时间:0.01
案例02:
SQL: |
# Time: 181009 16:37:05 # User@Host: mlsreader[mlsreader] @ [10.20.0.88] Id: 15580846 # Query_time: 178.729686 Lock_time: 0.000048 Rows_sent: 51 Rows_examined: 78735215 SET timestamp=1539074225; select * from `t_im_msg_1` where ((`chatfrom` = '1200852555' and `chatto` != 0 and `is_del` = 0 and `channel_id` = '2') or (`chatto` = '1200852555' and `chatfrom` != 0 and `is_del` = 0 and `channel_id` = '2')) order by `msg_id` asc limit 51 offset 0; |
1、SQL分析
mysql> explain select * from `t_im_msg_1` where ((`chatfrom` = '1200852555' and `chatto` != 0 and `is_del` = 0 and `channel_id` = '2') or (`chatto` = '1200852555' and `chatfrom` != 0 and `is_del` = 0 and `channel_id` = '2')) order by `msg_id` asc limit 51 offset 0;
+----+-------------+------------+-------+---------------+---------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+-------+---------------+---------+---------+------+------+-------------+
| 1 | SIMPLE | t_im_msg_1 | index | fromto,tofrom | PRIMARY | 8 | NULL | 90 | Using where |
+----+-------------+------------+-------+---------------+---------+---------+------+------+-------------+
其它不做过多分析,只看三点:
1.1 type = index:和全表扫描一样。只是扫描表的时候按照索引次序进行而不是行。主要优点就是避免了排序, 但是开销仍然非常大;
1.2 key = PRIMARY:key列显示MySQL实际决定使用的键(索引)为主键(PRIMARY);
1.3 rows = 90:为了找到所需的行而需要读取的行数,估算值,不精确(虽然不精确,但是type = index说明和全表扫描一样,但是90这就有点说不过去了);
2、揭露真相,
mysql> explain select * from `t_im_msg_1` where ((`chatfrom` = '1200852555' and `chatto` != 0 and `is_del` = 0 and `channel_id` = '2') or (`chatto` = '1200852555' and `chatfrom` != 0 and `is_del` = 0 and `channel_id` = '2')) order by `msg_id` asc;
+----+-------------+------------+-------+---------------+---------+---------+------+-----------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+-------+---------------+---------+---------+------+-----------+-------------+
| 1 | SIMPLE | t_im_msg_1 | index | fromto,tofrom | PRIMARY | 8 | NULL | 119782451 | Using where |
+----+-------------+------------+-------+---------------+---------+---------+------+-----------+-------------+
3、使用union(与案例01类似)
mysql> explain
select * from `t_im_msg_1`
where (`chatfrom` = '1200852555' and `chatto` != 0 and `is_del` = 0 and `channel_id` = '2')
union
select * from `t_im_msg_1`
where (`chatto` = '1200852555' and `chatfrom` != 0 and `is_del` = 0 and `channel_id` = '2')
order by `msg_id` asc
limit 51 offset 0;
+----+--------------+------------+-------+---------------+--------+---------+------+------+------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+--------------+------------+-------+---------------+--------+---------+------+------+------------------------------------+
| 1 | PRIMARY | t_im_msg_1 | range | fromto,tofrom | fromto | 16 | NULL | 112 | Using index condition; Using where |
| 2 | UNION | t_im_msg_1 | range | fromto,tofrom | tofrom | 16 | NULL | 247 | Using index condition; Using where |
| NULL | UNION RESULT | <union1,2> | ALL | NULL | NULL | NULL | NULL | NULL | Using temporary; Using filesort |
+----+--------------+------------+-------+---------------+--------+---------+------+------+------------------------------------+