- 1 概述
- 2 索引扫描排序和文件排序简介
- 3 索引扫描排序执行过程分析
- 4 文件排序
- 5 补充说明
- 6 参考资料
MySQL有两种方式能够实现ORDER BY:html
围绕着这两种排序方式,咱们试着理解一下ORDER BY的执行过程以及回答一些常见的问题。(下文仅讨论InnoDB存储引擎)mysql
咱们知道InnoDB存储引擎以B+树做为索引的底层实现,B+树的叶子节点存储着全部数据页而内部节点不存放数据信息,而且全部叶子节点造成一个(双向)链表。
举个例子,假设userinfo表的userid字段上有主键索引,且userid目前的范围在1001~1006之间,则userid的索引B+树以下:(这里只是为了举例,下图忽略了InnoDB数据页默认大小16KB、双向链表,而且假设B+树度数为三、userid顺序插入)sql
如今咱们想按照userid从小到大的顺序取出全部用户信息,执行如下SQL性能
SELECT * FROM userinfo ORDER BY userid;
MySQL会直接遍历上图userid索引的叶子节点链表,不须要进行额外的排序操做。这就是用索引扫描来排序。spa
但若是userid字段上没有任何索引,图1的B+树结构不存在,MySQL就只能先扫表筛选出符合条件的数据,再将筛选结果根据userid排序。这个排序过程就是filesort。.net
下文将详细介绍这两种排序方式。设计
介绍索引扫描排序以前,先看看索引的用途
SQL语句中,WHERE子句和ORDER BY子句均可以使用索引:WHERE子句使用索引避免全表扫描,ORDER BY子句使用索引避免filesort(用“避免”可能有些欠妥,某些场景下全表扫描、filesort未必比走索引慢),以提升查询效率。
虽然索引能提升查询效率,但在一条SQL里,对于一张表的查询 一次只能使用一个索引(注:排除发生index merge的可能性),也就是说当WHERE子句与ORDER BY子句要使用的索引不一致时,MySQL只能使用其中一个索引(B+树)。code
也就是说,一个既有WHERE又有ORDER BY的SQL中,使用索引有三个可能的场景:orm
举个例子,咱们建立一张order_detail表 记录每一笔充值记录的userid(用户id)、money(充值金额)、create_time(充值时间),主键是自增id:htm
CREATE TABLE `order_detail` ( `id` int(11) NOT NULL AUTO_INCREMENT, `userid` int(11) NOT NULL, `money` float NOT NULL, `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `userid` (`userid`), KEY `create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8
写脚本插入100w行数据(InnoDB别用COUNT(*)查总行数,会扫全表,这里只是为了演示):
SELECT COUNT(*) FROM order_detail; +----------+ | COUNT(*) | +----------+ | 1000000 | +----------+ SELECT * FROM order_detail LIMIT 5; +----+--------+-------+---------------------+ | id | userid | money | create_time | +----+--------+-------+---------------------+ | 1 | 104832 | 3109 | 2013-01-01 07:40:38 | | 2 | 138455 | 6123 | 2013-01-01 07:40:42 | | 3 | 109967 | 7925 | 2013-01-01 07:40:46 | | 4 | 166686 | 4307 | 2013-01-01 07:40:55 | | 5 | 119837 | 1912 | 2013-01-01 07:40:58 | +----+--------+-------+---------------------+
如今咱们想取出userid=104832用户的全部充值记录,并按照充值时间create_time正序返回。
写出以下SQL并EXPLAIN一下:
EXPLAIN SELECT * FROM order_detail WHERE userid = 104832 ORDER BY create_time; +------+-------------+--------------+------+---------------+--------+---------+-------+------+-----------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +------+-------------+--------------+------+---------------+--------+---------+-------+------+-----------------------------+ | 1 | SIMPLE | order_detail | ref | userid | userid | 4 | const | 8 | Using where; Using filesort | +------+-------------+--------------+------+---------------+--------+---------+-------+------+-----------------------------+
key
列的值是userid,能够看出这条SQL会使用userid索引用做WHERE子句的条件过滤,而ORDER BY子句没法使用该索引,只能使用filesort来排序。这就是上文的第一个场景,整个执行流程大体以下:
因为本例中M的值能够大概参考rows
列的值8,很是小,因此整个执行过程只花费0.00 sec
接下来是上文的第二种场景,索引只用于ORDER BY子句,这便是索引扫描排序:
咱们能够继续使用上文的SQL,经过FORCE INDEX子句强制Optimizer使用ORDER BY子句的索引create_time:
EXPLAIN SELECT * FROM order_detail FORCE INDEX (create_time) WHERE userid = 104832 ORDER BY create_time; +------+-------------+--------------+-------+---------------+-------------+---------+------+--------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +------+-------------+--------------+-------+---------------+-------------+---------+------+--------+-------------+ | 1 | SIMPLE | order_detail | index | NULL | create_time | 4 | NULL | 998056 | Using where | +------+-------------+--------------+-------+---------------+-------------+---------+------+--------+-------------+
能够看到Extra字段里的Using filesort已经没了,可是扫过的rows大概有998056行(准确的值应该是1000000行,InnoDB这一列只是估值)。这是由于索引用于ORDER BY子句时,会直接遍历该索引的叶子节点链表,而不像第一种场景那样从B+树的根节点出发 往下查找。执行流程以下:
整个时间复杂度是O(M*logN),M是主键id的总数,N是聚簇索引叶子节点的个数(数据页的个数)
本例中M的值为1000000,因此整个执行过程比第一种场景花了更多时间,同一台机器上耗时1.34 sec
上述两个例子刚好说明了另外一个道理:在某些场景下使用filesort比不使用filesort 效率更高。
第三种状况发生在WHERE子句与ORDER BY子句能使用相同的索引时(如: WHERE userid > xxx ORDER BY userid),这样就能省去第二种状况的回表查询操做了。
所以,若是可能,设计索引时应该尽量地同时知足这两种任务,这样是最好的。 ----《高性能MySQL》
关于filesort上文其实已经介绍过了一些。
filesort的名字起得很费解,让人误觉得它会:将一张很是大的表放入磁盘再进行排序。其实不是这样的,filesort仅仅是排序而已,是否会放入磁盘看状况而定(filesort is not always bad and it does not mean that a file is saved on disk. If the size of the data is small, it is performed in memory.)。如下是《高性能MySQL》中对filesort的介绍:
若是须要排序的数据量小于“排序缓冲区”,MySQL使用内存进行“快速排序”操做。若是内存不够排序,那么MySQL会先将数据分块,可对每一个独立的块使用“快速排序”进行排序,再将各个块的排序结果放到磁盘上,而后将各个排好序的块进行“归并排序”,最后返回排序结果。
因此filesort是否会使用磁盘取决于它操做的数据量大小。
总结来讲就是,filesort按排序方式来划分 分为两种:
数据量大的状况下涉及到磁盘io,因此效率会低一些。
根据回表查询的次数,filesort又能够分为两种方式:
两次传输排序会进行两次回表操做:第一次回表用于在WHERE子句中筛选出知足条件的rowid以及rowid对应的ORDER BY的列值;第二次回表发生在ORDER BY子句对指定列进行排序以后,经过rowid回表查出SELECT子句须要的字段信息。
举个例子,咱们须要从充值记录表筛选出2018年8月11日到12日的全部userid>140000用户的订单的明细,并按照金额从大到小进行排序(下面只是为filesort举例,不是一种好的实现):
EXPLAIN SELECT * FROM order_detail WHERE create_time >= '2018-08-11 00:00:00' and create_time < '2018-08-12 00:00:00' and userid > 140000 order by money desc; +------+-------------+--------------+-------+--------------------+-------------+---------+------+------+-----------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +------+-------------+--------------+-------+--------------------+-------------+---------+------+------+-----------------------------+ | 1 | SIMPLE | order_detail | range | userid,create_time | create_time | 4 | NULL | 1 | Using where; Using filesort | +------+-------------+--------------+-------+--------------------+-------------+---------+------+------+-----------------------------+
咱们试着分析一下这个SQL的执行过程:
熟悉计算机系统的人能够看出,第二次回表会表比第一次回表的效率低得多,由于第一次回表几乎是顺序I/O;而因为rowid是根据money进行排序的,第二次回表会按照rowid乱序去读取行记录,这些行记录在磁盘中的存储是分散的,每读一行 磁盘均可能会产生寻址时延(磁臂移动到指定磁道)+旋转时延(磁盘旋转到指定扇区),这便是随机I/O。
因此为了避免第二次回表的随机I/O,MySQL在4.1以后作了一些改进:在第一次回表时就取出这次查询用到的全部列,供后续使用。咱们称之为单次传输排序。
仍是上面那条SQL,咱们再看看单次传输排序的执行过程:
单次传输排序的弊端在于会将全部涉及到的列都放入排序缓冲区,排序缓冲区一次能放下的tuples更少了,进行归并排序的几率增大。列数据量越大,须要的归并路数更多,增长了额外的I/O开销。因此列数据量太大时,单次传输排序的效率可能还不如两次传输排序。
固然,列数据量太大的状况不是特别常见,因此MySQL的filesort会尽量使用单次传输排序,可是为了防止上述状况发生,MySQL作了如下限制:
咱们开发者也应该尽量让filesort使用单次传输排序,不过EXPLAIN不会告诉咱们这个信息,因此咱们只能肉眼检查各列的大小看看是否会触发上面两个限制 致使两次传输排序的发生。
如第3小节所述,既然filesort的效率未必比索引扫描排序低,为何不少人会想避免filesort呢?
谷歌一下using filesort,几乎都是"如何避免filesort"相关的内容。:
这是由于一般ORDER BY子句会与LIMIT子句配合,只取出部分行。若是只是为了取出top1的行 却对全部行进行排序,这显然不是一种高效的作法。这种场景下 按顺序取的索引扫描排序可能会比filesort拥有更好性能(固然也有例外)。
Whether the optimizer actually does so depends on whether reading the index is more efficient than a table scan if columns not in the index must also be read.
官方文档告诉咱们optimizer会帮咱们选择一种高效的ORDER BY方式。
但也不能彻底依赖optimizer的判断,这时合理创建索引、引导它使用指定索引多是更好的选择。
MySQL 8.0 Reference Manual :: 8.2.1.14 ORDER BY Optimization
《高性能MySQL》
Sergey Petrunia's blog » How MySQL executes ORDER BY
MySQL filesort algorithms - Valinv
MySQL技术内幕:InnoDB存储引擎(第2版)
B+ Tree Visualization
B+ Trees(pdf)
MySQL :: MySQL 8.0 Reference Manual :: 8.8.2 EXPLAIN Output Format
What do Clustered and Non clustered index actually mean? - Stack Overflow