文章导读
前言
在日常写代码的工作中,排序无处不在,各种编程语言都提供了排序的实现,比如java的Arrays.sort(),golang的sort.Sort(),数据库也会有数据库的排序,我们天天写的sql,经常都有按照指定字段排序的需求,所以都会使用order by语句,那么你真的了解order by的排序逻辑吗?本篇文章就是带领大家一起探究一下Mysql的order by排序是如何实现的。
准备工作
创建一个市民表,
CREATE TABLE `t` ( `id` int(11) NOT NULL, `city` varchar(16) NOT NULL, `name` varchar(16) NOT NULL, `age` int(11) NOT NULL, `addr` varchar(128) DEFAULT NULL, PRIMARY KEY (`id`), KEY `city` (`city`) ) ENGINE=InnoDB;
复制代码
如果我们要查询城市是“杭州”的所有人的名字,并且按照姓名排序返回前1000个人的姓名、年龄。
我们的sql语句是:
select city,name,age from t where city='杭州' order by name limit 1000 ;
复制代码
sort buffer
我们使用explain命令查看一下这个sql的执行计划:
其中Extra字段中的“Using filesort”就表示需要排序,mysql会为每个线程分配一块内存用于排序,称为:sort buffer;
sort_buffer_size参数:mysql为排序开辟的内存sort buffer的大小;
- 如果排序的数据小于sort_buffer_size,排序就在内存中完成;
- 如果排序的数据大于sort_buffer_size,就得利用磁盘的临时文件使用外部排序,外排一般使用归并排序算法。
再来看看city字段建立的索引示意图:
无序数据排序
全字段排序
select city,name,age from t where city='杭州' order by name limit 1000 ;
复制代码
执行流程
观察上面的sql,可以发现查询的字段并不多,此时会走全字段排序的执行流程:
- 初始化 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排序
select * from t where city='杭州' order by name limit 1000 ;
复制代码
执行流程
如果需要查询的字段非常多,此时会走rowid排序的执行流程:
- 初始化sort_buffer, 确定放入两个字段,即name和id;
- 从索引city找到第一个满足city=“杭州”条件的主键id, 也就是图中的ID_X;
- 到主键id索引取整行,取name, id 这两个字段,存入sort_buffer中;
- 从索引city取下一个记录的主键id;
- 重复3,4直到city的值不满足查询条件为止,对应的主键id也就是途中ID_Y;
- 对sort_buffer中的数据按照字段name做快速排序;
- 遍历排序结果,取前1000行,并按照id值回表中取出city、name和age三个字段返回给客户端。
ps:
当查询字段多,mysql会选择rowid的方式,这样可以避免外部排序,但是对比全字段排序,rowid的方式多了回表。
全字段排序 vs rowid排序
全字段排序 | rowid排序 |
---|---|
将要查询的字段数据全部放在sort_buffer中, 然后进行排序, 如果内存不足,使用外排; | 只将排序字段和主键id的数据放在sort_buffer中, 然后排序, 最后根据主键id将其他数据回表查询出来; |
适用于innodb表 | 适用于memory表 |
mysql的设计思想:
如果内存够,就要多利用内存,尽量减少磁盘访问,也就是尽量减少使用rowid,因为rowid需要回表;
对于innodb表来说,rowid排序要求回表,会增加磁盘io,因此不会被优先选择;
但是rowid排序也有适用的场景,比如内存表,即使回表,也是查询内存;
如何避免排序
覆盖索引
根据上面全字段和rowid的排序流程,如果查询出来的数据本身就是排序好的,那么是不是避免了排序,从而就能提高sql的查询速度,索引是有序的,如果能走覆盖索引,那么就并不需要高成本的去排序;
给city name age这三个字段加上联合索引:
alter table t add index city_user(city, name, age);
复制代码
然后在执行一下sql
select city,name,age from t where city='杭州' order by name limit 1000 ;
复制代码
查看一下执行计划:
可以看出extra字段没有using filesort,不需要排序,并且也使用到了覆盖索引,性能提升很多。
随机排序
order by rand()
接下来再来分析一下随机排序,也就是order by rand(),探究一下order by rand()的排序逻辑。
准备工作:
--创建单词表
CREATE TABLE `words` ( `id` int(11) NOT NULL AUTO_INCREMENT, `word` varchar(64) DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB;
--创建1w条数据
复制代码
执行sql:
select word from words order by rand() limit 3;
复制代码
查看执行计划:
可以看出使用了临时表和排序,临时表其实就是内存表,那么它的排序会如何考虑呢?
对于临时表,本身就存储在内存,所以即使回表,也是查询内存,不需要访问磁盘,速度很可能,mysql会选择rowid排序。
执行流程
- 一个临时表,该表使用的memory引擎,第一个字段是double类型,记为R,第二个字段是varchar类型,记为W;
- 从words表中,按主键顺序取出所有的word值,对每个word值,调用rand()函数,生成一个小数,并将这个小数存入R字段,word值存入W字段,是全表扫描;
- 临时表根据rowid方式进行排序,初始化sort_buffer,使用rowid排序,两个字段,一个R字段,一个是位置信息字段;
- 在sort_buffer中根据R字段排序,排序完成,根据前三个结果的位置信息,依次到临时表取出word值,返回。
ps:
位置信息字段是什么意思?其实也就是mysql的表是怎么定位一行数据的?
- 对于有主键的innodb表来说,这个rowid就是主键id;
- 对于没有主键的innodb表来说,这个rowid就是由系统生成的;
- memory引擎不是索引表,可以认为就是一个数组,rowid就是数组的下标;
内存临时表&磁盘临时表
刚刚提到内存临时表,那么是不是所有的临时表都是基于内存呢?其实也不是,mysql也有一个参数判断tmp_table_size,默认是16M,如果内存表超过了这个size,那么就会转成磁盘临时表。
磁盘临时表使用的就是innodb引擎,排序会优先选择全字段排序;
这个sql只需要取前三个数据,所以排序算法会优先选择堆排序,而不是归并排序,这也是mysql5.6以后的优化;
随机排序的优化
分析了order by rand()的流程,发现成本也是比较高的,所以可以换一种思路,不管我们的表的主键id是不是连续,都有办法,先找出随机的三个index;
总结
- mysql的order by是一个成本比较高的操作,学习了order by()的工作原理,这也是日常工作中sql优化的一个考虑点;
- 平时工作中大部分排序需求都是基于分页的,limit都比较小,但是select的字段比较多,然后表大部分是基于innodb引擎,innodb优先选择全字段排序,如果sort_buffer_size不够的话,可能也会有使用外部排序的可能,所以也可以适当调整一下sort_buffer_size参数大小;
- mysql根据不同的排序场景也做出了很多优化,比如排序算法并不只是归并,也有堆排序;
- mysql的设计思想:一切的优化都是为了减少磁盘io;
参考
time.geekbang.org/column/arti…