Mysql Order By 优化
Mysql实现排序有两种方法
- 通过索引生成有效的排序
- 通过文件排序
InnoDB 索引实现
InnoDB存储引擎以B+树作为索引的底层实现,B+树的叶子节点存储着所有数据页而内部节点不存放数据信息,这棵树的叶点data 域保存了完整的数据记录。这个索引的 key 是数据表的主键,因此 InnoDB 表数据文件本身就是主索引。并且所有叶子节点形成一个(双向)链表。MySQL会直接遍历引的叶子节点链表,不需要进行额外的排序操作。这就是用索引扫描来排序。
索引扫描排序执行过程分析
对于一张表的查询 一次只能使用一个索引也就是说当WHERE子句与ORDER BY子句要使用的索引不一致时,MySQL只能使用其中一个索引(B+树)。也就是说,一个既有WHERE又有ORDER BY的SQL中,使用索引有三个可能的场景:
- 只用于WHERE子句 筛选出满足条件的数据
- 只用于ORDER BY子句 返回排序后的结果
- 既用于WHERE又用于ORDER BY,筛选出满足条件的数据并返回排序后的结果
举个例子:我们创建一张user_account表 记录每一笔充值记录的userid(用户id)、money(用户充值金额)、create_time(充值时间),主键是自增id:
CREATE TABLE `user_recharge` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`money` float NOT NULL,
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
插入100W条测试数据
SELECT COUNT(*) FROM user_recharge;
+----------+
| COUNT(*) |
+----------+
| 1000000 |
+----------+
SELECT * FROM order_detail LIMIT 5;
+----+--------+-------+---------------------+
| id | userid | money | create_time |
+----+--------+-------+---------------------+
| 1 | 1000001 | 3000 | 2019-01-01 07:40:38 |
| 2 | 1000002 | 1235 | 2019-01-01 07:40:42 |
| 3 | 1000003 | 8909 | 2019-01-01 07:40:46 |
| 4 | 1000004 | 4307 | 2019-01-01 07:40:55 |
| 5 | 1000005 | 1912 | 2019-01-01 07:40:58 |
+----+--------+-------+---------------------+
场景一 索引只用于WHERE子句
我们现在取一条用户的查询信息,例如user_id = 1000001,并以充值时间排序,分析一下SQL
EXPLAIN SELECT * FROM user_recharge WHERE user_id = 1000001 ORDER BY create_time;
+----+-------------+---------------+------+---------------+---------+---------+-------+------+-----------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------------+------+---------------+---------+---------+-------+------+-----------------------------+
| 1 | SIMPLE | user_recharge | ref | user_id | user_id | 4 | const | 1 | Using where; Using filesort |
+----+-------------+---------------+------+---------------+---------+---------+-------+------+-----------------------------+
1 row in set (0.00 sec)
整体执行流程如下
- 先通过user_id索引找到所有满足WHERE条件的主键id(注:从b+树根节点往下找叶子节点,时间复杂度为O(logN))
- 再根据这些主键id去主键索引(聚簇索引))找到这几行的数据,生成一张临时表放入排序缓冲区(时间复杂度为O(M*logN),M是临时表缓冲区里的行数)
- 对临时表缓冲区里的数据进行排序,时间复杂度为O(MlogM),M为缓冲区内的行数
场景二 索引只用于Order By
我们可以继续使用上文的SQL,通过FORCE INDEX子句强制Optimizer使用ORDER BY子句的索引create_time:
EXPLAIN SELECT * FROM user_recharge FORCE INDEX (create_time) WHERE user_id = 1000001 ORDER BY create_time;
+----+-------------+---------------+-------+---------------+-------------+---------+------+-----------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------------+-------+---------------+-------------+---------+------+-----------+-------------+
| 1 | SIMPLE | user_recharge | index | NULL | create_time | 4 | NULL | 990900 | Using where |
+----+-------------+---------------+-------+---------------+-------------+---------+------+-----------+-------------+
1 row in set (0.00 sec)
可以看到Extra字段里的Using filesort已经没了,但是扫过的rows大概有990900行(准确的值应该是1000000行,InnoDB这一列只是估值)。这是因为索引用于ORDER BY子句时,会直接遍历该索引的叶子节点链表,而不像第一种场景那样从B+树的根节点出发 往下查找。执行流程如下:
- 从create_time索引的第一个叶子节点出发,按顺序扫描所有叶子节点
- 根据每个叶子节点记录的主键id去主键索引(聚簇索引))找到真实的行数据,判断行数据是否满足WHERE子句的user_id条件,若满足,则取出并返回 整个时间复杂度是O(M*logN),M是主键id的总数,N是聚簇索引叶子节点的个数(数据页的个数)
本例中M的值为1000000,所以整个执行过程比第一种场景花了更多时间,同一台机器上耗时1.34 sec 上述两个例子恰好说明了另一个道理:在某些场景下使用filesort比不使用filesort 效率更高。
场景三 索引WHERE 又用于Order By
第三种情况发生在WHERE子句与ORDER BY子句能使用相同的索引时(如: WHERE userid > xxx ORDER BY userid),这样就能省去第二种情况的回表查询操作了。
- 原文作者:Looper
- 原文链接:http://www.cachesx.com/post/mysql-orderby/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。