在 【精华】洞悉MySQL底层架构:游走在缓冲与磁盘之间 这篇文章中,我们介绍了索引树的页面怎么加载到内存中,如何淘汰,等底层细节。这篇文章我们从比较宏观的角度来看MySQL中关键字的原理。本文,我们主要探索order by语句的底层原理。阅读完本文,您将了解到:

  • order by语句有哪些排序模式,以及每种排序模式的优缺点;

  • order by语句会用到哪些排序算法,在什么场景下会选择哪种排序算法;

  • 如何查看和分析sql的order by优化手段(执行计划 + OPTIMIZER_TRACE日志);

  • 如何优化order by语句的执行效率?(思想:减小行大小,尽量走索引,能够走覆盖索引最佳,可适当增加sort buffer内存大小)

这里我们从数据结构的维度来看数据和索引,也就是都当成B+树的的,我们需要数据的时候再从存储引擎的B+树中读取。

以下是我们本文作为演示例子的表,假设我们有如下表:


索引如下:


对应的idx_d索引结构如下(这里我们做了一些夸张的手法,让一个页数据变小,为了展现在索引树中的查找流程):


1、如何跟踪执行优化

为了方便分析sql的执行流程,我们可以在当前session中开启 optimizer_trace:

SET optimizer_trace='enabled=on';

然后执行sql,执行完之后,就可以通过以下堆栈信息查看执行详情了:

SELECT * FROM information_schema.OPTIMIZER_TRACE\G;

以下是

1select a, b, c, d from t20 force index(idx_abc)  where a=3 order by d limit 100,2;

的执行结果,其中符合a=3的有8457条记录,针对order by重点关注以下属性

 1"filesort_priority_queue_optimization": {  // 是否启用优先级队列 2  "limit": 102,           // 排序后需要取的行数,这里为 limit 100,2,也就是100+2=102 3  "rows_estimate": 24576, // 估计参与排序的行数 4  "row_size": 123,        // 行大小 5  "memory_available": 32768,    // 可用内存大小,即设置的sort buffer大小 6  "chosen": true          // 是否启用优先级队列 7}, 8... 9"filesort_summary": {10  "rows": 103,                // 排序过程中会持有的行数11  "examined_rows": 8457,      // 参与排序的行数,InnoDB层返回的行数12  "number_of_tmp_files": 0,   // 外部排序时,使用的临时文件数量13  "sort_buffer_size": 13496,  // 内存排序使用的内存大小14  "sort_mode": "sort_key, additional_fields"  // 排序模式15}

1.1、排序模式

其中 sort_mode有如下几种形式:

  • sort_key, rowid:表明排序缓冲区元组包含排序键值和原始表行的行id,排序后需要使用行id进行回表,这种算法也称为original filesort algorithm(回表排序算法);

  • sort_key, additional_fields:表明排序缓冲区元组包含排序键值和查询所需要的列,排序后直接从缓冲区元组取数据,无需回表,这种算法也称为modified filesort algorithm(不回表排序);

  • sort_key, packed_additional_fields:类似上一种形式,但是附加的列(如varchar类型)紧密地打包在一起,而不是使用固定长度的编码。

如何选择排序模式

选择哪种排序模式,与max_length_for_sort_data这个属性有关,这个属性默认值大小为1024字节:

  • 如果查询列和排序列占用的大小超过这个值,那么会转而使用sort_key, rowid模式;

  • 如果不超过,那么所有列都会放入sort buffer中,使用sort_key, additional_fields或者sort_key, packed_additional_fields模式;

  • 如果查询的记录太多,那么会使用sort_key, packed_additional_fields对可变列进行压缩。

1.2、排序算法

基于参与排序的数据量的不同,可以选择不同的排序算法:

  • 如果排序取的结果很小,小于内存,那么会使用优先级队列进行堆排序;

  • 例如,以下只取了前面10条记录,会通过优先级队列进行排序:

  • 1select a, b, c, d from t20 force index(idx_abc)  where a=3 order by d limit 10;
  • 如果排序limit n, m,n太大了,也就是说需要取排序很后面的数据,那么会使用sort buffer进行快速排序

  • 如下,表中a=1的数据有三条,但是由于需要limit到很后面的记录,MySQL会对比优先级队列排序和快速排序的开销,选择一个比较合适的排序算法,这里最终放弃了优先级队列,转而使用sort buffer进行快速排序:

  • 1select a, b, c, d from t20 force index(idx_abc)  where a=1 order by d limit 300,2;
  • 如果参与排序的数据sort buffer装不下了,那么我们会一批一批的给sort buffer进行内存快速排序,结果放入排序临时文件,最终使对所有排好序的临时文件进行归并排序,得到最终的结果;

  • 如下,a=3的记录超过了sort buffer,我们要查找的数据是排序后1000行起,sort buffer装不下1000行数据了,最终MySQL选择使用sort buffer进行分批快排,把最终结果进行归并排序:

  • 1select a, b, c, d from t20 force index(idx_abc)  where a=3 order by d limit 1000,10;

2、order by走索引避免排序

执行如下sql:

1select a, b, c, d from t20 force index(idx_d) where d like 't%' order by d limit 2;

我们看一下执行计划:


发现Extra列为:Using index condition,也就是这里只走了索引。

执行流程如下图所示:

通过idx_d索引进行range_scan查找,扫描到4条记录,然后order by继续走索引,已经排好序,直接取前面两条,然后去聚集索引查询完整记录,返回最终需要的字段作为查询结果。这个过程只需要借助索引。


如何查看和修改sort buffer大小?

我们看一下当前的sort buffer大小:


可以发现,这里默认配置了sort buffer大小为512k。

我们可以设置这个属性的大小:

SET GLOBAL sort_buffer_size = 32*1024;

或者

SET sort_buffer_size = 32*1024;

下面我们统一把sort buffer设置为32k

1SET sort_buffer_size = 32*1024; 

3、排序算法案例

3.1、使用优先级队列进行堆排序

如果排序取的结果很小,并且小于sort buffer,那么会使用优先级队列进行堆排序;

例如,以下只取了前面10条记录:

1select a, b, c, d from t20 force index(idx_abc) where a=3 order by d limit 10;

a=3的总记录数:8520。查看执行计划:


发现这里where条件用到了索引,order by limit用到了排序。我们进一步看看执行的optimizer_trace日志:

 1"filesort_priority_queue_optimization": { 2  "limit": 10, 3  "rows_estimate": 27033, 4  "row_size": 123, 5  "memory_available": 32768, 6  "chosen": true  // 使用优先级队列进行排序 7}, 8"filesort_execution": [ 9],10"filesort_summary": {11  "rows": 11,12  "examined_rows": 8520,13  "number_of_tmp_files": 0,14  "sort_buffer_size": 1448,15  "sort_mode": "sort_key, additional_fields"16}

发现这里是用到了优先级队列进行排序。排序模式是:sort_key, additional_fields,即先回表查询完整记录,把排序需要查找的所有字段都放入sort buffer进行排序。

所以这个执行流程如下图所示:

  1. 通过where条件a=3扫描到8520条记录;

  2. 回表查找记录;

  3. 把8520条记录中需要的字段放入sort buffer中;

  4. 在sort buffer中进行堆排序;

  5. 在排序好的结果中取limit 10前10条,写入net buffer,准备发送给客户端。


3.2、内部快速排序

如果排序limit n, m,n太大了,也就是说需要取排序很后面的数据,那么会使用sort buffer进行快速排序。MySQL会对比优先级队列排序和归并排序的开销,选择一个比较合适的排序算法。

如何衡量究竟是使用优先级队列还是内存快速排序?
一般来说,快速排序算法效率高于堆排序,但是堆排序实现的优先级队列,无需排序完所有的元素,就可以得到order by limit的结果。
MySQL源码中声明了快速排序速度是堆排序的3倍,在实际排序的时候,会根据待排序数量大小进行切换算法。如果数据量太大的时候,会转而使用快速排序。

有如下SQL:

1select a, b, c, d from t20 force index(idx_abc)  where a=1 order by d limit 300,2;

我们把sort buffer设置为32k:

1SET sort_buffer_size = 32*1024; 

其中a=1的记录有3条。查看执行计划:


可以发现,这里where条件用到了索引,order by limit 用到了排序。我们进一步看看执行的optimizer_trace日志:

 1"filesort_priority_queue_optimization": { 2  "limit": 302, 3  "rows_estimate": 27033, 4  "row_size": 123, 5  "memory_available": 32768, 6  "strip_additional_fields": { 7    "row_size": 57, 8    "sort_merge_cost": 33783, 9    "priority_queue_cost": 61158,10    "chosen": false  // 对比发现快速排序开销成本比优先级队列更低,这里不适用优先级队列11  }12},13"filesort_execution": [14],15"filesort_summary": {16  "rows": 3,17  "examined_rows": 3,18  "number_of_tmp_files": 0,19  "sort_buffer_size": 32720,20  "sort_mode": ""21}

可以发现这里最终放弃了优先级队列,转而使用sort buffer进行快速排序。

所以这个执行流程如下图所示:

  1. 通过where条件a=1扫描到3条记录;

  2. 回表查找记录;

  3. 把3条记录中需要的字段放入sort buffer中;

  4. 在sort buffer中进行快速排序

  5. 在排序好的结果中取limit 300, 2第300、301条记录,写入net buffer,准备发送给客户端。


3.3、外部归并排序

当参与排序的数据太多,一次性放不进去sort buffer的时候,那么我们会一批一批的给sort buffer进行内存排序,结果放入排序临时文件,最终使对所有排好序的临时文件进行归并排序,得到最终的结果。

有如下sql:

1select a, b, c, d from t20 force index(idx_abc) where a=3 order by d limit 1000,10;

其中a=3的记录有8520条。执行计划如下:

image-20200614171147989

可以发现,这里where用到了索引,order by limit用到了排序。进一步查看执行的optimizer_trace日志:

 1"filesort_priority_queue_optimization": { 2  "limit": 1010, 3  "rows_estimate": 27033, 4  "row_size": 123, 5  "memory_available": 32768, 6  "strip_additional_fields": { 7    "row_size": 57, 8    "chosen": false, 9    "cause": "not_enough_space"  // sort buffer空间不够,无法使用优先级队列进行排序了10  }11},12"filesort_execution": [13],14"filesort_summary": {15  "rows": 8520,16  "examined_rows": 8520,17  "number_of_tmp_files": 24,  // 用到了24个外部文件进行排序18  "sort_buffer_size": 32720,19  "sort_mode": ""20}

我们可以看到,由于limit 1000,要返回排序后1000行以后的记录,显然sort buffer已经不能支撑这么大的优先级队列了,所以转而使用sort buffer内存排序,而这里需要在sort buffer中分批执行快速排序,得到多个排序好的外部临时文件,最终执行归并排序。(外部临时文件的位置由tmpdir参数指定)

其流程如下图所示:


4、排序模式案例

4.1、sort_key, additional_fields模式

sort_key, additional_fields,排序缓冲区元组包含排序键值和查询所需要的列(先回表取需要的数据,存入排序缓冲区中),排序后直接从缓冲区元组取数据,无需再次回表。

上面 2.3.1、2.3.2节的例子都是这种排序模式,就不继续举例了。

4.2、模式

sort_key, packed_additional_fields:类似上一种形式,但是附加的列(如varchar类型)紧密地打包在一起,而不是使用固定长度的编码。

上面2.3.3节的例子就是这种排序模式,由于参与排序的总记录大小太大了,因此需要对附加列进行紧密地打包操作,以节省内存。

4.3、模式

前面我们提到,选择哪种排序模式,与max_length_for_sort_data[2]这个属性有关,max_length_for_sort_data规定了排序行的最大大小,这个属性默认值大小为1024字节:


也就是说如果查询列和排序列占用的大小小于这个值,这个时候会走sort_key, additional_fields或者sort_key, packed_additional_fields算法,否则,那么会转而使用sort_key, rowid模式。

现在我们特意把这个值设置小一点,模拟sort_key, rowid模式:

1SET max_length_for_sort_data = 100;

这个时候执行sql:

1select a, b, c, d from t20 force index(idx_abc) where a=3 order by d limit 10;

这个时候再查看sql执行的optimizer_trace日志:

 1"filesort_priority_queue_optimization": { 2  "limit": 10, 3  "rows_estimate": 27033, 4  "row_size": 49, 5  "memory_available": 32768, 6  "chosen": true 7}, 8"filesort_execution": [ 9],10"filesort_summary": {11  "rows": 11,12  "examined_rows": 8520,13  "number_of_tmp_files": 0,14  "sort_buffer_size": 632,15  "sort_mode": ""16}

可以发现这个时候切换到了sort_key, rowid模式,在这个模式下,执行流程如下:

  1. where条件a=3扫描到8520条记录;

  2. 回表查找记录;

  3. 找到这8520条记录的idd字段,放入sort buffer中进行堆排序;

  4. 排序完成后,取前面10条;

  5. 取这10条的id回表查询需要的a,b,c,d字段值;

  6. 依次返回结果给到客户端。


可以发现,正因为行记录太大了,所以sort buffer中只存了需要排序的字段和主键id,以时间换取空间,最终排序完成,再次从聚集索引中查找到所有需要的字段返回给客户端,很明显,这里多了一次回表操作的磁盘读,整体效率上是稍微低一点的。

5、order by优化总结

根据以上的介绍,我们可以总结出以下的order by语句的相关优化手段:

  • order by字段尽量使用固定长度的字段类型,因为排序字段不支持压缩;

  • order by字段如果需要用可变长度,应尽量控制长度,道理同上;

  • 查询中尽量不用用select *,避免查询过多,导致order by的时候sort buffer内存不够导致外部排序,或者行大小超过了max_length_for_sort_data导致走了sort_key, rowid排序模式,使得产生了更多的磁盘读,影响性能;

  • 尝试给排序字段和相关条件加上联合索引,能够用到覆盖索引最佳。


这篇文章的内容就差不多介绍到这里了,能够阅读到这里的朋友真的是很有耐心,为你点个赞。

本文为arthinking基于相关技术资料和官方文档撰写而成,确保内容的准确性,如果你发现了有何错漏之处,烦请高抬贵手帮忙指正,万分感激。

大家可以关注我的博客:itzhai.com 获取更多文章,我将持续更新后端相关技术,涉及JVM、Java基础、架构设计、网络编程、数据结构、数据库、算法、并发编程、分布式系统等相关内容。

如果您觉得读完本文有所收获的话,可以关注我的账号,或者点个赞吧,码字不易,您的支持就是我写作的最大动力,再次感谢!

关注我的公众号,及时获取最新的文章。

更多文章

JVM系列专题:公众号发送 JVM

References

[1]: 滴滴云. MySQL 全表 COUNT(*) 简述. zhihu.com. Retrieved from https://zhuanlan.zhihu.com/p/54378839

[2]: MySQL. 8.2.1.14 ORDER BY Optimization. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/order-by-optimization.html

[3]: MySQL:排序(filesort)详细解析. Retrieved from https://www.jianshu.com/p/069428a6594e

[4]: MYSQL实现ORDER BY LIMIT的方法以及优先队列(堆排序). Retrieved from http://blog.itpub.net/7728585/viewspace-2130920/

·END·访问IT宅(itzhai.com)查看我的博客更多文章

扫码关注及时获取新内容↓↓↓

Java架构杂谈

Java后端技术架构 · 技术专题 · 经验分享

blog: itzhai.com

码字不易,如有收获,点个「赞」哦~

c++ sort 从大到小排序_算法的艺术:MySQL order by对各种排序算法的巧用相关推荐

  1. c++ sort 从大到小排序_C语言必学的12个排序算法:冒泡排序(第4篇)

    基本思想 冒泡排序(Bubble Sort),是一类"交换"类排序方法,类似水中冒泡,最大的数据会沉到水底,较小的数会浮上来.很简单,以从小到大排序为例,每一趟排序将"逆 ...

  2. arraylist从大到小排序_初学Python最简易入门之十四排序算法10对字典排序

    当用sorted()函数对字典临时排序的时候,默认是对字典键名从小到大排序,排序的结果以列表的形式输出.如图14-10-1所示实例14-10-1用sorted()对棋类字典排序.pyw,程序执行结果见 ...

  3. c++ sort 从大到小排序_C语言必学的12个排序算法:堆排序(第7篇)

    题外话堆排序比之前的简单选择.冒泡算法.快速排序算法复杂一些,因为用到了树形数据结构,但是本文使用了数组实现完全二叉树,因此也比较简单.C语言初学者,可以简单了解其思想,具体的知识掌握可以参照数据结构 ...

  4. oracle 表连接 大表小表_优化必备基础:Oracle中常见的三种表连接方式

    在Oracle SQL语句中,如果from后面有多个表时,表的连接方式是一个很重要的考量. 从Oracle 6开始,优化器就支持下面4种表连接方式: - 嵌套循环连接(Nested Loop Join ...

  5. MySQL ORDER BY IF() 条件排序

    在做sqlzoo的时候,碰到一个SQL的排序问题,他把符合条件的单独几行,可以放在查询结果的开始,或者查询结果的尾部 通过的方法就是IN语句(也可以通过IF语句) 自己做了个测试,如下,这个是表的所有 ...

  6. 单片机里如何使用冒泡法实现数据从大到小排列_单片机实验一冒泡法排序.doc...

    单片机实验一冒泡法排序 实验一:冒泡法排序实验 实验要求 实验目的:掌握控转移指令的功能,以及冒泡法排序的原理. 实验原理 循环嵌套结构.循环程序的设计方法和一样的,要分别考虑重循环的控制条件.内循环 ...

  7. 不等式大两边小中间_不等式取值范围口诀

    不等式取值范围口诀2019-09-27 11:13:46文/宋则贤 不等式就是用大于,小于,大于等于,小于等于连接而成的数学式子.不等式取值范围口诀为同大取大,同小取小.大大小小没有解,大小小大取中间 ...

  8. 不等式大两边小中间_两边-还是小于取中间,大于取两边?是大于取中间, – 手机爱问...

    2019-03-27 如何证明三角形两边之和大于第三边,两边之差小于第三边? 证明: 假设构成三角形的三条边分别为:a.b.c,且a.b.c大小任意: ①先证明:a+b>c: 因为a.b.c都为 ...

  9. c#给定二维数组按升序排序_在数组中按升序对数字进行排序| 8086微处理器

    c#给定二维数组按升序排序 Problem: Write a program in 8086 microprocessor to sort numbers in ascending order in ...

最新文章

  1. GitHub年度报告:中国开源贡献仅次美国、Python成第二热门语言
  2. Alter-有意思的小游戏
  3. VC++ HIDAPI实现USB数据读写
  4. Eclipse添加git插件上传项目到github
  5. php 数组指向下一个值,比较数组值并根据自定义值(PHP)在数组中查找下一个值 - php...
  6. 三轴加速度传感器和六轴惯性传感器_六轴传感器和三轴传感器的区别在哪
  7. 日语词频分析——mecab使用
  8. TTL电路与CMOS电路对比
  9. teamviewer常用命令
  10. MLX90614红外测温模块的使用
  11. 根据ID3算法给出游玩的决策树的实战案例
  12. Google 后 Hadoop 时代的新 “三驾马车” -- Caffeine(搜索)、Pregel(图计算)、Dremel(查询)
  13. 基于javaweb小说评价下载网站管理系统 ssm框架
  14. C语言-自动识别用户输入的字符串并便于后期处理
  15. 2017年秋季学期软件工程第一次作业(曹洪茹)
  16. Python 基于pyecharts自定义经纬度热力图可视化
  17. python爬虫(以简书为例)
  18. 支付宝、微信小程序高频知识(汇总VS对比)
  19. Python简单实现表白藏头诗
  20. JAVA 实现插入排序

热门文章

  1. 文件的上传下载(一)
  2. .NET Mass Downloader -整体下载.NET源码
  3. c/c++这么难学,那么学会了究竟有多牛X呢?
  4. 助推曲烟数字化转型升级,开展生产业务数字化
  5. 作为程序员我是如何对事物进行分析的
  6. 扎金花 游戏开发细节与部分代码
  7. 【飞秋】ASP.NET 之 常用类、方法的超级总结,并包含动态的EXCEL导入导出功能,奉上类库源码
  8. 关羽在韩国有块私家地?
  9. TreeCtrl 查找功能的最简单实现
  10. 第八节:ES6为数组做了哪些扩展?