概述

MySQL有两种方式可以实现ORDER BY:
1、使用文件排序(filesort)
2、通过索引扫描生成有序的结果

InnoDB存储引擎以B+树作为索引的底层实现,B+树的叶子节点存储着所有数据页,中间节点不存放数据信息,并且所有叶子节点形成一个(双向)链表。

如果MySQL可以直接遍历索引的叶子节点链表,不需要进行额外的排序操作。这就是用索引扫描来排序。

如果相关orderby的字段上没有任何索引,MySQL就只能先扫表筛选出符合条件的数据,再将筛选结果根据排序。这个排序过程就是filesort。

下面将逐一进行介绍。

文件排序

举例,表结构如下:

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;
select city,name,age from t where city='杭州' order by name limit 1000  ;


在执行如上语句explain后,Extra 字段中显示“Using filesort”,表示使用文件排序。

ORDER BY涉及排序,用户语句中的排序,mysql 会给每个线程分配一块内存用于排序,称为 sort_buffer。

sort_buffer_size,表示用于排序的内存大小。
1、如果排序的数据量小于sort_buffer_size,排序将会在内存中完成
2、如果排序数据量很大,内存中无法存下这么多数据,则会使用磁盘临时文件来辅助排序,也称外部排序
3、在使用外部排序时,MySQL会分成好几份单独的临时文件用来存放排序后的数据,然后在将这些文件合并成一个大文件。一般使用归并方法。

number_of_tmp_files表示排序过程中是否用到了临时文件。
1、为0,表示排序可以直接在内存中完成。
2、非0,表示排序过程中具体的临时文件数量。

通常,如果放在临时文件中排序。sort_buffer_size 越小,需要分成的份数越多,number_of_tmp_files 的值就越大。

注意:sort buffer是在server层。

所以文件排序filesort是否会使用磁盘取决于它操作的数据量大小。

filesort按排序方式来划分分为两种:
1、数据量小时,在内存中快排
2、数据量大时,在内存中分块快排,再在磁盘上将各个块做归并
数据量大的情况下涉及到磁盘io,所以效率会低一些。

根据回表查询的次数,filesort又可以分为两种方式:
1、回表读取两次数据(two-pass):两次传输排序,也就是rowid排序。
3、回表读取一次数据(single-pass):单次传输排序,也就是全字段排序。

下面将分别介绍这两种排序。

全字段排序

执行流程示例

还是上面这个例子,通常情况下,这个语句执行流程如下所示 :

1、初始化 sort_buffer,确定放入 name、city、age 这三个字段;

2、从索引 city 找到第一个满足 city='杭州’条件的主键 id;

3、到主键 id 索引取出整行,取 name、city、age 三个字段的值,存入 sort_buffer 中;

4、从索引 city 取下一个记录的主键 id;

5、重复步骤 3、4 直到 city 的值不满足查询条件为止

6、对 sort_buffer 中的数据按照字段 name 做快速排序;

7、按照排序结果取前 1000 行返回给客户端。


图中“按 name 排序”这个动作,可能在内存中完成,也可能需要使用外部排序,这取决于排序所需的内存和参数 sort_buffer_size。文章开头已经提过,这里不在赘述。

以下是explain的信息:

sort_mode 里面的 packed_additional_fields 的意思是,排序过程对字符串做了“紧凑”处理。即使 name 字段的定义是 varchar(16),在排序过程中还是要按照实际长度来分配空间的。
number_of_tmp_files 表示的是,排序过程中使用的临时文件数。

优缺点

全字段排序只对原表的数据读了一遍,剩下的操作都是在 sort_buffer 和临时文件中执行的。

问题:如果查询要返回的字段很多的话,那么 sort_buffer 里面要放的字段数太多,这样内存里能够同时放下的行数很少,要分成很多个临时文件,排序的性能会很差。

rowid排序

何时用到rowid排序

如果单行很大,全字段排序方法效率不够好。

MySQL中如何界定单行太大?
使用max_length_for_sort_data,MySQL 中专门控制用于排序的行数据的长度的一个参数。如果单行的长度超过这个值,MySQL 就认为单行太大,要换一个算法。

SET max_length_for_sort_data = 16;

上述示例的SQL语句中,city、name、age 这三个字段的定义总长度是 36,现在把 max_length_for_sort_data 设置为 16,计算过程出现变化,新的算法放入 sort_buffer 的字段,只有要排序的列(即 name 字段)和主键 id。相应的执行流程也有了变化,会形成二次回表。

执行流程示例

1、初始化 sort_buffer,确定放入两个字段,即 name 和 id;

2、从索引 city 找到第一个满足 city='杭州’条件的主键 id;

3、到主键 id 索引取出整行,取 name、id 这两个字段,存入 sort_buffer 中;

4、从索引 city 取下一个记录的主键 id;

5、重复步骤 3、4 直到不满足 city='杭州’条件为止;

6、对 sort_buffer 中的数据按照字段 name 进行排序;

7、遍历排序结果,取前 1000 行,并按照 id 的值回到原表中取出 city、name 和 age 三个字段返回给客户端。


以下是explain的信息:

从 OPTIMIZER_TRACE 的结果中:
1、sort_mode 变成了 ,表示参与排序的只有 name 和 id 这两个字段。
2、number_of_tmp_files 变成 10 了,是因为这时候参与排序的行数虽然仍然是 4000 行,但是每一行都变小了,因此需要排序的总数据量就变小了,需要的临时文件也相应地变少了。

全字段排序 VS rowid 排序

如果 MySQL 实在是担心排序内存太小,会影响排序效率,才会采用 rowid 排序算法,这样排序过程中一次可以排序更多行,但是需要再回到原表去取数据。

如果 MySQL 认为内存足够大,会优先选择全字段排序,把需要的字段都放到 sort_buffer 中,这样排序后就会直接从内存里面返回查询结果了,不用再回到原表去取数据。

以上体现了 MySQL 的一个设计思想:如果内存够,就要多利用内存,尽量减少磁盘访问。

对于 InnoDB 表来说,rowid 排序会要求回表多造成磁盘读,因此不会被优先选择。

全字段排序:
缺点:
1.造成sort_buffer中存放不下很多数据,因为除了排序字段还存放其他字段,对sort_buffer的利用效率不高
2.当所需排序数据量很大时,会有很多的临时文件,排序性能也会很差
优点:MySQL认为内存足够大时会优先选择全字段排序,因为这种方式比rowid 排序避免了一次回表操作

rowid排序
优点:更好的利用内存的sort_buffer进行排序操作,尽量减少对磁盘的访问
缺点:回表的操作是随机IO,会造成大量的随机读,不一定就比全字段排序减少对磁盘的访问

通过索引生成有序扫描结果

从全文排序可以看到,MySQL 做排序是一个成本比较高的操作。MySQL 之所以需要生成临时表,并且在临时表上做排序操作,其原因是原来的数据都是无序的。如果想不通过排序得到天然有序的结果,那就需要借助索引。

比如在上述的市民表上创建一个 city 和 name 的联合索引,对应的 SQL 语句:

alter table t add index city_user(city, name);

这样会生成一个(city, name)联合索引树,我们依然可以用树搜索的方式定位到第一个满足 city='杭州’的记录,并且额外确保了,接下来按顺序取“下一条记录”的遍历过程中,只要 city 的值是杭州,name 的值就一定是有序的。

执行流程示例

1、从索引 (city,name) 找到第一个满足 city='杭州’条件的主键 id;

2、到主键 id 索引取出整行,取 name、city、age 三个字段的值,作为结果集的一部分直接返回;

3、从索引 (city,name) 取下一个记录主键 id;重复步骤 2、3,直到查到第 1000 条记录,或者是不满足 city='杭州’条件时循环结束。

以下是explain的信息:

Extra 字段中没有 Using filesort ,也就是不需要文件排序。
注:
Using index condition: 要回表
Using where; Using index,直接在二级索引上获取全部数据(覆盖索引)
Using filesort 需要文件排序
Using temporary 需要临时表。

问题

mysql通过遍历索引将满足条件的数据读取到sort_buffer,并且按照排序字段进行快速排序。

如果查询的字段不包含在辅助索引中,需要按照辅助索引记录的主键返回聚集索引取出所需字段

该方式会造成随机IO,在MySQL5.6提供了MRR的机制,会将辅助索引匹配记录的主键取出来在内存中进行排序,然后在回表

按照情况建立联合索引来避免排序所带来的性能损耗,允许的情况下也可以建立覆盖索引来避免回表。

注:上述SQL可以进一步优化,使用覆盖索引,这样就是省去了回表查询操作,但是索引还是有维护代价的。这是一个需要权衡的决定。

扩展: order by rand()

order by rand() 使用了内存临时表,内存临时表排序的时候使用了 rowid 排序方法。

mysql> select word from words order by rand() limit 3;

这个语句是随机排序取前 3 个。虽然这个 SQL 语句写法很简单,但执行流程却有点复杂的。

Extra 字段显示 Using temporary,表示的是需要使用临时表;Using filesort,表示的是需要执行排序操作。

这条语句的执行流程是这样的:
1、创建一个临时表。这个临时表使用的是 memory 引擎,表里有两个字段,第一个字段是 double 类型,为了后面描述方便,记为字段 R,第二个字段是 varchar(64) 类型,记为字段 W。并且,这个表没有建索引。

2、从 words 表中,按主键顺序取出所有的 word 值。对于每一个 word 值,调用 rand() 函数生成一个大于 0 小于 1 的随机小数,并把这个随机小数和 word 分别存入临时表的 R 和 W 字段中,到此,扫描行数是 10000。

3、现在临时表有 10000 行数据了,接下来你要在这个没有索引的内存临时表上,按照字段 R 排序。

4、初始化 sort_buffer。sort_buffer 中有两个字段,一个是 double 类型,另一个是整型。

5、从内存临时表中一行一行地取出 R 值和位置信息(每个引擎用来唯一标识数据行的信息。对于有主键的 InnoDB 表来说,这个 rowid 就是主键 ID;对于没有主键的 InnoDB 表来说,这个 rowid 就是由系统生成的;MEMORY 引擎不是索引组织表。在这个例子里面,你可以认为它就是一个数组。因此,这个 rowid 其实就是数组的下标),分别存入 sort_buffer 中的两个字段里。这个过程要对内存临时表做全表扫描,此时扫描行数增加 10000,变成了 20000。

6、在 sort_buffer 中根据 R 的值进行排序。注意,这个过程没有涉及到表操作,所以不会增加扫描行数。

7、排序完成后,取出前三个结果的位置信息,依次到内存临时表中取出 word 值,返回给客户端。这个过程中,访问了表的三行数据,总扫描行数变成了 20003。

由此可见order by rand() 这种写法都会让计算过程非常复杂,需要大量的扫描行数,因此排序过程的资源消耗也会很大。

随机排序方法

如果只随机选择 1 个 word 值,可以怎么做呢?

方法一:
max(id) 和 min(id) 都是不需要扫描索引的,而第三步的 select 也可以用索引快速定位,可以认为就只扫描了 3 行。但实际上,这个算法本身并不严格满足题目的随机要求,因为 ID 中间可能有空洞,因此选择不同行的概率不一样,不是真正的随机。

mysql> select max(id),min(id) into @M,@N from t ;
set @X= floor((@M-@N+1)*rand() + @N);
select * from t where id >= @X limit 1;

方法二:
解决了方法一里面明显的概率不均匀问题。MySQL 处理 limit Y,1 的做法就是按顺序一个一个地读出来,丢掉前 Y 个,然后把下一个记录作为返回结果,因此这一步需要扫描 Y+1 行。再加上,第一步扫描的 C 行,总共需要扫描 C+Y+1 行,执行代价比随机算法 1 的代价要高。

mysql> select count(*) into @C from t;
set @Y = floor(@C * rand());
set @sql = concat("select * from t limit ", @Y, ",1");
prepare stmt from @sql;
execute stmt;
DEALLOCATE prepare stmt;

要随机取 3 个 word 值呢?可以怎么做?

mysql> select count(*) into @C from t;
set @Y1 = floor(@C * rand());
set @Y2 = floor(@C * rand());
set @Y3 = floor(@C * rand());
select * from t limit @Y1,1; //在应用代码里面取Y1、Y2、Y3值,拼出SQL后执行
select * from t limit @Y2,1;
select * from t limit @Y3,1;
假设Y1,Y2,Y3是由小到大的三个数,则可以优化成这样,这样扫描行数为Y3
id1 = select * from t limit @Y1,1;
id2= select * from t where id > id1 limit @Y2-@Y1,1;
select * from t where id > id2 limit @Y3 - @Y2,1;

扩展:优先级队列排序算法

MySQL 5.6版本开始针对Order by limit M,N语句,在空间层面做了优化,加入了新的排序算法,即:优先队列排序算法。类似数据结构中的堆排序。

mysql> select word from words order by rand() limit 3;

比如上述 SQL 语句,只需要取 R 值最小的 3 个 rowid。如果使用归并排序算法的话,虽然最终也能得到前 3 个值,但是这个算法结束后,已经将 10000 行数据都排好序了。也就是说,后面的 9997 行也是有序的了。但我们的查询并不需要这些数据是有序的。所以这浪费了非常多的计算量。

优先队列算法,就可以精确地只得到三个最小值,执行流程如下:
1、对于这 10000 个准备排序的 (R,rowid),先取前三行,构造成一个堆;

(对数据结构印象模糊的同学,可以先设想成这是一个由三个元素组成的数组)

2、取下一个行 (R’,rowid’),跟当前堆里面最大的 R 比较,如果 R’小于 R,把这个 (R,rowid) 从堆中去掉,换成 (R’,rowid’);

3、重复第 2 步,直到第 10000 个 (R’,rowid’) 完成比较。

select city,name,age from t where city='杭州' order by name limit 1000  ;

如上这个SQL也用到了 limit,但是没用优先队列排序算法。原因是,这条 SQL 语句是 limit 1000,如果使用优先队列算法的话,需要维护的堆的大小就是 1000 行的 (name,rowid),超过了设置的 sort_buffer_size 大小,所以只能使用归并排序算法。

参考:
https://time.geekbang.org/column/article/73479
https://time.geekbang.org/column/article/73795

数据库之order by相关推荐

  1. oracle12c order by,oracle 数据库中order by 的一些高级用法

    oracle数据库中order by用法 oracle数据库中order by的一些高级用法 现有一个表,表内容如下: 以下的操作都是对该表进行的操作 1.按照名称排序(默认为升序) 实现代码: se ...

  2. oracle 数据库中order by 的一些高级用法

    oracle数据库中order by用法 oracle数据库中order by的一些高级用法 现有一个表,表内容如下: 以下的操作都是对该表进行的操作 1.按照名称排序(默认为升序) 实现代码: se ...

  3. mysql order 关键字_PHP数据库MySQL Order By 关键词 - PHP教程

    PHP MySQL Order By 关键词 你可以对 MySQL 数据库中的记录集进行排序,具体请阅读本节内容. ORDER BY 关键词用于对记录集中的数据进行排序. ORDER BY 关键词 O ...

  4. mysql数据库建order,group表时的错误

    这是一个数据库设计问题,不建议这样做,最好楼主用_order做列名或者另起名字,一定要这样做的话,可以这样, create table tt(id int(3),`order` varchar(12) ...

  5. oracle排序desc和,Oracle数据库排序ORDER BY子句的使用总结篇

    在Oracle数据库中,当我们执行查询时,往往要对查询的结果进行排序处理.排序处理是通过ORDER BY子句来实现的.本文我们主要对Oracle数据库查询时的各种排序进行了总结,接下来我们就开始介绍这 ...

  6. MySQL数据库排序order by(asc、desc)

    1. 排序查询语法 排序查询语法: select * from 表名 order by 列1 asc|desc [,列2 asc|desc,...] 语法说明: 先按照列1进行排序,如果列1的值相同时 ...

  7. 数据库 sqlite order by对结果集进行排序

    一.order by语句概述 order by语句用于根据指定的列对结果集进行排序. order by 语句默认按照升序对记录进行排序. 如果您希望按照降序对记录进行排序,可以使用 desc 关键字 ...

  8. oracle数据库访问order by不起作用分析

    `SELECT * FROM student ROWNUM <= 1 ORDER BY id ASC` 执行结果,返回结果没有排序.使用驱动"System.Data.OracleCli ...

  9. 数据库排序order by

    语法 order by 后面写上要排序的字段,排序的字段可以有多个,多个采用逗号间隔: order by 默认采用升序(asc),排序,可以手动设置为降序(desc) 如果存在where子句,那么or ...

最新文章

  1. chromium android分析,Chromium Android工程迁移编译过程
  2. JAVA使用正则表达式给字符串添加分隔符
  3. 观察者设计模式 php,PHP设计模式 - 观察者模式
  4. Oracle表连接深入浅出
  5. 一个C++加密工具EncryptDecrypt.dll
  6. python写小猪佩奇_python之小猪佩奇
  7. 明日之后怎么跳过实名认证_明日之后宝箱达人活动怎么玩 明日之后宝箱达人可以开箱多少次...
  8. php soap 下载文件,允许下载SOAP API响应(PHP)中的PDF文件get(作为附件)
  9. vue多页面开发_使用VUE进行移动端H5页面开发前准备
  10. Linux开发_判断程序是否以管理员权限运行(root/sudo)
  11. pd17虚拟机启动器生成方法
  12. Linux 内存管理(一)——地址空间
  13. java读取串口设备信息_Java--串口之间的通信及扫描枪的读取
  14. 高中能学计算机吗,不读高中能把计算机这个行业学好吗
  15. 利用 MATLAB 编程实现乘子法求解约束最优化问题。
  16. 中央电大 c语言程序设计a 试题,中央电大2008年秋C语言程序设计A试题1
  17. 30行Python代码实现蚂蚁森林自动偷能量
  18. 联想y7000笔记如何安装matlab,联想Y7000P笔记本怎样安装win7系统 安装win7系统操作分享...
  19. 【HTML基础-1】HTML标签简介及常用标签
  20. window10 安装语言包出现“很抱歉,我们无法安装此功能。你可以稍后重试。错误代码: 0x80070422”

热门文章

  1. Java发送附件到邮箱
  2. QPython 3C 操作 压缩包
  3. 原创轻量VIO算法、简单易上手——XRSLAM帮你快速搭建移动平台AR应用
  4. html转pdf之使用Paged.js加页眉页脚
  5. xUtils图片本地缓存使用
  6. C++ windy数
  7. CSSCSS3基础教程
  8. 美国计算机基础课课程教学,美国大学计算机基础课程设置
  9. MATLAB将数据存在TXT文件中
  10. linux自制硬件防火墙,自制linux系统——打造属于自己的linux系统