在实际的开发中必定会碰到根据某个字段进行排序后来显示结果的需求,可是你真的理解order by
在 Mysql 底层是如何执行的吗?算法
假设你要查询城市是苏州
的全部人名字,而且按照姓名进行排序返回前 1000 我的的姓名、年龄,这条 sql 语句应该如何写?sql
首先建立一张用户表,sql 语句以下:c#
CREATE TABLE user ( id int(11) NOT NULL, city varchar(16) NOT NULL, name varchar(16) NOT NULL, age int(11) NOT NULL, PRIMARY KEY (id), KEY city (city) ) ENGINE=InnoDB;
则上述需求的 sql 查询语句以下:性能优化
select city,name,age from user where city='苏州' order by name limit 1000;
这条 sql 查询语句相信你们都能写出来,可是你了解它在 Mysql 底层的执行流程吗?今天陈某来你们聊一聊这条 sql 语句是如何执行的以及有什么参数会影响执行的流程。ide
本篇文章分为以下几个部分进行详细的阐述:性能
全字段排序优化
rowid 排序spa
全字段排序 VS rowid 排序.net
如何避免排序线程
前面聊过索引可以避免全表扫描,所以咱们给city
这个字段上添加了索引,固然城市的字段很小,不用考虑字符串的索引问题,以前有写过一篇关于如何给字符串的加索引的文章,有不了解朋友看一下这篇文章:Mysql 性能优化:如何给字符串加索引?
此时用Explain
来分析一下的这条查询语句的执行状况,结果以下图:
Extra
这个字段中的Using filesort
表示的就是须要排序,MySQL 会给每一个线程分配一块内存用于排序,称为sort_buffer
。
既然使用了索引进行查询,咱们来简单的画一下city
这棵索引树的结构,以下图:
从上图能够看出,知足city='苏州'
是从ID3
到IDX
这些记录。
一般状况下,此条 sql 语句执行流程以下:
初始化 sort_buffer,肯定放入 name、city、age 这三个字段。
从索引 city 找到第一个知足city='苏州'
条件的主键id
,也就是图中的ID3
。
到主键id索引
取出整行,取name
、city
、age
三个字段的值,存入sort_buffer
中。
从索引city
取下一个记录的主键 id。
重复步骤 三、4 直到 city 的值不知足查询条件为止,对应的主键 id 也就是图中的IDX
。
对sort_buffer
中的数据按照字段name
作快速排序。
按照排序结果取前 1000 行返回给客户端。
咱们称这个排序过程为全字段排序
,执行的流程图以下:
图中按name排序
这个动做,可能在内存中完成,也可能须要使用外部排序,这取决于排序所需的内存和参数sort_buffer_size
。
sort_buffer_size
:就是 MySQL 为排序开辟的内存(sort_buffer)的大小。若是要排序的数据量小于 sort_buffer_size,排序就在内存中完成。但若是排序数据量太大,内存放不下,则不得不利用磁盘临时文件
辅助排序。
在上面这个算法过程里面,只对原表的数据读了一遍,剩下的操做都是在sort_buffer
和临时文件
中执行的。但这个算法有一个问题,就是若是查询要返回的字段不少的话,那么sort_buffer
里面要放的字段数太多,这样内存里可以同时放下的行数不多,要分红不少个临时文件,排序的性能会不好。
因此若是单行很大,这个方法效率不够好。
咱们能够修改一个max_length_for_sort_data
这个参数使其使用另一种算法。max_length_for_sort_data,是 MySQL 中专门控制用于排序的行数据的长度的一个参数。它的意思是,若是单行的长度超过这个值,MySQL 就认为单行太大,要换一个算法。
city
、name
、age
这三个字段的定义总长度是36
,我把max_length_for_sort_data
设置为 16,咱们再来看看计算过程有什么改变。设置的 sql 语句以下:
SET max_length_for_sort_data = 16;
新的算法放入 sort_buffer 的字段,只有要排序的列(即 name 字段)和主键 id。
但这时,排序的结果就由于少了 city 和 age 字段的值,不能直接返回了,整个执行流程就变成以下所示的样子:
初始化sort_buffer
,肯定放入两个字段,即name
和id
。
从索引 city 找到第一个知足city='苏州'
条件的主键id
,也就是图中的ID3
。
到主键id索引
取出整行,取 name、id 这两个字段,存入 sort_buffer 中。
从索引city
取下一个记录的主键 id。
重复步骤 三、4 直到 city 的值不知足查询条件为止,对应的主键 id 也就是图中的IDX
。
对sort_buffer
中的数据按照字段name
作快速排序。
遍历排序结果,取前 1000 行,并按照 id 的值回到原表中取出 city、name 和 age 三个字段返回给客户端。
这个执行流程的示意图以下,我把它称为rowid排序
。
对比全字段排序
,rowid排序
多了一次回表查询
,便是多了第7步
的查询主键索引树。
若是 MySQL 实在是担忧排序内存过小,会影响排序效率,才会采用 rowid 排序算法,这样排序过程当中一次能够排序更多行,可是须要再回到原表去取数据。
若是 MySQL 认为内存足够大,会优先选择全字段排序,把须要的字段都放到 sort_buffer 中,这样排序后就会直接从内存里面返回查询结果了,不用再回到原表去取数据。
这也就体现了 MySQL 的一个设计思想:若是内存够,就要多利用内存,尽可能减小磁盘访问。
对于 InnoDB 表来讲,rowid 排序会要求回表多形成磁盘读,所以不会被优先选择。
其实,并非全部的order by
语句,都须要排序操做的。从上面分析的执行过程,咱们能够看到,MySQL 之因此须要生成临时表,而且在临时表上作排序操做,其缘由是原来的数据都是无序的。
若是可以保证从city
这个索引上取出来的行,自然就是按照 name 递增排序的话,是否是就能够不用再排序了呢?
所以想到了联合索引,建立(city,name)
联合索引,sql 语句以下:
alter table user add index city_user(city, name);
此时的索引树以下:
在这个索引里面,咱们依然能够用树搜索的方式定位到第一个知足city='苏州'
的记录,而且额外确保了,接下来按顺序取“下一条记录”的遍历过程当中,只要 city 的值是苏州,name 的值就必定是有序的。
按照上图,整个查询的流程以下:
从索引(city,name)找到第一个知足 city='苏州'条件的主键 id。
到主键 id 索引取出整行,取 name、city、age 三个字段的值,做为结果集的一部分直接返回。
从索引(city,name)取下一个记录主键 id。
重复步骤 二、3,直到查到第 1000 条记录,或者是不知足 city='苏州'条件时循环结束。
对应的流程图以下:
能够看到,这个查询过程不须要临时表,也不须要排序。接下来,咱们用 explain 的结果来印证一下。
从图中能够看到,Extra
字段中没有Using filesort
了,也就是不须要排序了。并且因为(city,name)
这个联合索引自己有序,因此这个查询也不用把 4000 行全都读一遍,只要找到知足条件的前 1000 条记录就能够退出了。也就是说,在咱们这个例子里,只须要扫描 1000 次。
难道仅仅这样就能知足了?此条查询语句是否能再优化呢?
朋友们还记得覆盖索引吗?覆盖索引的好处就是可以避免再次回表查询,不了解的朋友们能够看一下陈某以前写的文章:Mysql 性能优化:如何使用覆盖索引?。
咱们建立(city,name,age)
联合索引,这样在执行上面的查询语句就能使用覆盖索引了,避免了回表查询了,sql 语句以下:
alter table user add index city_user_age(city, name, age);
此时执行流程图以下:
固然,覆盖索引可以提高效率,可是维护索引也是须要代价的,所以还须要权衡使用。
今天这篇文章,我和你介绍了 MySQL 里面order by
语句的几种算法流程。
在开发系统的时候,你老是不可避免地会使用到 order by 语句。内心要清楚每一个语句的排序逻辑是怎么实现的,还要可以分析出在最坏状况下,每一个语句的执行对系统资源的消耗,这样才能作到下笔若有神,不犯低级错误。
文章留言区
往期推荐
一条SQL查询语句是如何执行的?Mysql性能优化:为何要用覆盖索引?Mysql性能优化:什么是索引下推?Mysql中的三类锁,你知道吗?Mysql性能优化:如何给字符串加索引?