关于在 MySQL 排序中使用索引这件事!
@[toc] 前面跟小伙伴们分享的索引相关的内容,基本上都是在 where 子句中使用索引,实际上,索引也还有另外一个大的用处,那就是在排序中使用索引,今天我们就来聊聊这个话题。
1. 排序的两种方式
MySQL 中想给查询结果排序,我们只需要来一个 order by 即可,SQL 很简单,底层实现起来整体上来说,有两种不同的思路:
- filesort,有时候我们也将之称为文件排序,这个名字有时候会给我们一些误解,让人以为是在磁盘上进行排序的,然而实际上并不一定,数据量比较小的时候,直接在内存中进行排序就行了,只有当在内存中无法完成排序的时候,才会用到磁盘文件。
- 索引排序,由于 InnoDB 中的索引是按照 B+Tree 的形式将数据组织在一起的,B+Tree 中数据本身就是有序的,所以如果能够利用好索引,排序的事情就会事半功倍。
一共就这两种排序的方式,小伙伴们也发现了,如果我们的索引设计比较合理,最终能够按照第 2 种方式进行排序,那肯定是最好不过了。
不过这里需要注意一个细节,第二种排序方式快有一个前提,那就是不需要回表,如果查询的过程中需要回表,那么第二种方式就不一定快了。原因也简单:
- 如果不需要回表,也就是我们想要查询的数据都在索引树上,索引树上的数据本身又都是按照顺序存储的,那么查到数据直接返回即可,本身就是有序的。
- 如果查询的时候,索引树上并没有我们想要的字段,那么就需要回表,小伙伴们知道,回表基本上都是随机 IO 了,因为回表的时候,主键值并不一定连续,此时效率就会低一些。那么这个时候第二种排序方式的性能就不一定强于第一种了,当然,这并无固定结论,还是要结合具体情况分析,这里我只是告诉小伙伴们有各种可能的情况。
2. 索引排序
如果我们想用上索引排序,那么需要满足哪些条件呢?
还是以我们上篇文章的数据为例,假设我有如下表结构:
CREATE TABLE `user` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `username` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `age` int DEFAULT NULL, `address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, `gender` varchar(2) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`), KEY `user_prop_index` (`username`,`age`,`address`) ) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
这个表中有一个联合索引,联合索引的字段包含 username、age 和 address 三个。
表中的数据如下:
id(主键) | username | age | address | gender |
---|---|---|---|---|
1 | ab | 99 | 深圳 | 男 |
2 | bw | 95 | 天津 | 男 |
3 | cx | 93 | 深圳 | 男 |
4 | bc | 80 | 上海 | 女 |
5 | bg | 85 | 重庆 | 女 |
6 | ac | 98 | 广州 | 男 |
7 | bw | 99 | 海口 | 女 |
8 | ck | 90 | 深圳 | 男 |
9 | cc | 92 | 武汉 | 男 |
10 | af | 88 | 北京 | 女 |
还是假设 username、age、address 三个字段组成联合索引,B+Tree 如下:
小伙伴们就想想,怎么样查询,查出来的结果是有序的?
给大家 1 分钟总结一下。
我们来梳理下:只有当索引的顺序和 order by 子句的顺序完全一致,并且所有列的排序方向也都一致的情况下,MySQL 才能通过索引来对结果进行排序,同时,如果是联合索引,order by 子句也需要满足最左匹配原则。
我举几个例子。
2.1 案例一
先来看如下 SQL:
select address from user order by username;
这个是查询 address 字段,根据 username 进行排序。很明显,我们想要的 address 字段就存在于这个联合索引的 B+Tree 上,并且这个联合索引的 B+Tree 就是按照 username 进行升序排序的,所以这个 SQL 就可以通过索引进行排序,如下图:
type:index
就说明了 MySQL 使用了索引扫描来进行排序的。
2.2 案例二
再来看下面这条 SQL:
select address from user order by username asc,age desc\G
这个 SQL 还是查询 address 字段,是根据 username 和 age 进行排序的,其中 username 是按照升序排序,age 则是按照倒序排序,小伙伴们想想,在前面这个联合索引的 B+Tree 中,username 是升序的没问题,当 username 相同的时候,age 也是按照升序排序的,但是 SQL 中却要一个升序一个倒序,显然从索引树中拿到的数据无法满足这样的条件,所以这个查询并不会使用索引排序,如下图:
Extra 中的 Using filesort
就说明了这里需要文件排序,无法通过索引排序完成需求。
2.3 案例三
再来看如下 SQL:
select address from user order by username desc
这个 SQL 和 2.1 小节的 SQL 相比就是排序的顺序变了,第一个 SQL 没有写顺序,默认就是升序,这个里边写了是按照倒序来排列。B+Tree 中的 username 是升序,那么这个能用到索引排序吗?这个是可以使用到索引排序的,在 MySQL5.7 中,执行计划如下:
在 MySQL8.x 中,执行计划如下:
小伙伴们看到,区别在于 Extra 中多了一个 Backward index scan
。
这是啥意思呢?
在 MySQL8 之前,索引是可以被反向扫描的,但是反向扫描效率会低一些,所以小伙伴们看到,在 MySQL5.7 中用到了索引排序,而且也没说其他的,这其实就是索引反向扫描了。
从 MySQL8 开始,索引定义时候的降序关键字 DESC 将不再被忽略,索引树在存储数据的时候可以降序存储了,这样在将来查询的时候扫描索引就可以按照正向扫描了,正向扫描效率相对于反向扫描效率会高一些。
这块我来举个例子说明问题。假设我有如下创建表的 SQL:
CREATE TABLE t ( c1 INT, c2 INT, INDEX idx1 (c1 ASC, c2 ASC), INDEX idx2 (c1 ASC, c2 DESC), INDEX idx3 (c1 DESC, c2 ASC), INDEX idx4 (c1 DESC, c2 DESC) );
当我在 MySQL5.7 中执行如上 SQL 之后,再来查看表的定义,结果如下:
可以看到,虽然我在执行的时候定了索引字段的顺序,但是这个顺序实际上是被忽略了。
再来看看 MySQL8 中执行之后的结果:
可以看到,在 MySQL8 中,索引定义时字段的顺序被保留了。这印证了我们前面所说的没有问题。
最后,回到我们的问题,Backward index scan
表示优化器在查询的时候将能够使用降序索引。
2.4 案例四
再来看如下 SQL:
select gender from user where username='ab' order by age
这个 SQL 中已经给 username 指定了具体的值了,在前面的 B+Tree 中,当 username 已经确定的时候,那么接下来就是按照 age 排序的,如果 age 相同则是按照 address 排序,所以上面这个 SQL 是可以通过索引排序的:
2.5 案例五
再来看如下 SQL:
select gender from user where username='ab' order by address
这个 SQL 中 username 也是给指定了具体的值了,但是排序却是按照 address 排序的,小伙伴们知道,当 username 确定后,首先是按照 age 排序,其次才是按照 address 排序,所以,对于上面这个 SQL,从索引树中读取出来的数据,顺序并不一定是按照 address 排的,所以上面这个 SQL 无法用到索引排序:
2.6 案例六
再来看下面这个 SQL:
select gender from user where username like 'a%' order by age
这个 SQL 中的查询条件 username 是范围搜索,当 username 是范围搜索的时候,就无法保证相应的 age 是有序的了,所以这个 SQL 也无法使用索引排序:
另外需要注意的是,像查询条件中的 IN 和 BETWEEN 这样的关键字,也算是范围搜索,如果 where 子句中出现这些关键字,也是有可能导致无法使用索引排序的。
2.7 案例七
再来看下面这个 SQL:
select gender from user where username like 'a%' order by username,age
这个虽然 username 也是按照范围搜索,但是最终排序的时候却是按照 username 和 age 排序的,按照范围搜索拿出来的 username 和 age 本身就是有序的,所以这里也可以使用索引排序:
2.8 案例八
再来看下面这个 SQL:
select gender from user where username like 'a%' order by username,gender
这个 SQL 就不用多说了,排序字段中出现了索引之外的列,那肯定没法使用索引排序了:
总之,就是当我们根据 where 子句中的条件从 B+Tree 中定位到数据之后,定位到的这个数据究竟是否有序?如果有序且是 SQL 中要求的顺序,就能使用索引排序,否则就不可以。
现在我们再来回过头看一下一开始的结论,大家这个时候应该就好理解了:
只有当索引的顺序和 order by 子句的顺序完全一致,并且所有列的排序方向也都一致的情况下,MySQL 才能通过索引来对结果进行排序,同时,如果是联合索引,order by 子句也需要满足最左匹配原则。
3. 其他情况
3.1 多表联查
当我们在查询的时候是多表连接查询时,如果用到了排序,那么 order by 子句中涉及到的字段,必须全部在第一个表中,此时才会用到索引排序。
松哥举一个 TienChin 项目中的例子,TienChin 中有一个活动渠道表 tienchin_channel,还有一个活动表 tienchin_activity,活动表中引用到了渠道表的 id,我们来做如下一个多表联合查询:
select ta.name from tienchin_activity ta inner join tienchin_channel tc using(`channel_id`)
我们来看下这个 SQL 的执行计划:
可以看到,在这个查询中,优化器将 ta 表作为了第一张表,tc 表作为了第二张表,那么根据前面的结论,如果使用第一个表中的索引排序,就会用到索引排序,第二张表的则用不了,我们来验证一下。
可以看到,如果是第一张表的索引,就用到了索引排序;如果是第二张表的索引,就没有用到索引排序,如果两张表的索引都用了,也不会使用索引排序。
3.2 order by null
还有一种特殊的情况就是 order by null
,不知道有没有小伙伴见到过有人这样写?
在 MySQL8 之前,默认会按照 group by 的字段进行排序,此时加上 order by null
就是告诉 MySQL,不用帮我排序了,直接返回结果就行了,因为如果不加 order by null
,则可能会进行 filesort 排序,降低查询效率。
不过从 MySQL8 开始,默认已经不会按照 group by 字段排序了,所以这句现在其实可以不用写了。
4. 小结
好啦,关于 MySQL 中的索引排序就和小伙伴们聊这么多,希望大家都有所收获~

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
阿里云刘伟光:2 万字解读金融级云原生
作者:刘伟光,阿里云智能新金融&互联网行业总裁、中国金融四十人论坛常务理事,毕业于清华大学电子工程系 01 前言 2015年云原生理念提出的时候,彼时全球金融百年发展形成的信息化到数字化的背后,金融级的技术服务水准经过长时间的打磨已经形成行业共识的标准。8年前的云原生经典理念是聚焦在容器化、DevOps、持续开发持续集成、微服务架构这些软件开发层面的新范式。而金融级要求诸如高可用、高性能、业务连续性、系统安全稳定等等这些要求跟云原生架构的理念仿佛处在两个相距遥远的范畴。随着技术层面的不断演进,在新型的应用系统的开发方面,金融机构开始逐步引入容器化等云原生部署架构,但是始终发现聚焦在开发态层面的云原生能力是不能触达金融的系统建设的各个层面。云计算技术日新月异的变化反过来推动了云原生的发展从狭义到广义,今天的云已经变成了更为普适性的标准基础设施,更是新技术新业务创新的平台;因此诸如云原生大数据,和云原生存储以及云原生网络技术等技术让云的原生能力从软件开发走向数据平台进而延展到底层物理部署架构。今天的云计算无论是公共云还是专有云,其技术体系带来的先进性以及对开源的拥抱和支持确实在改变...
- 下一篇
故障分析 | MySQL 升级到 8.0 变慢问题分析
作者:操盛春 技术专家,任职于爱可生,专注研究 MySQL、OceanBase 源码。 本文来源:原创投稿 * 爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。 1. 背景介绍 前段时间,客户线上 MySQL 版本从 5.7.29 升级到 8.0.25。 升级完成之后,放业务请求进来,没到一分钟就开始出现慢查询,然后,慢查询越来越多,业务 SQL 出现堆积。 整个过程持续了大概一个小时,直到给某条业务 SQL 对应的表加上索引,问题才得到解决。 有一个比较奇怪的现象是:问题持续的过程中,服务器的系统负载、CPU 使用率、磁盘 IO、网络都处于低峰时期的水平,也就是说,问题很可能不是因为硬件资源不够用导致的。 那么,根本原因到底是什么?让我们一起来揭晓答案~ 2. 原因分析 客户线上环境有一个监控脚本,每分钟执行一次,这个脚本执行的 SQL 如下: select...fromsys.innodb_lock_waitswinnerjoininformation_schema.innodb_trxbonb.trx_id=w.blocking_trx_idinner...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- Docker安装Oracle12C,快速搭建Oracle学习环境
- CentOS7安装Docker,走上虚拟化容器引擎之路
- Linux系统CentOS6、CentOS7手动修改IP地址
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- CentOS关闭SELinux安全模块
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Hadoop3单机部署,实现最简伪集群
- CentOS6,7,8上安装Nginx,支持https2.0的开启