从排序原理到MYSQL中的排序方式

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 本文参考MYSQL官方文档,算法书籍,部分为自己观点可能有误,如果有误请指出共同讨论 转载请说明出处,谢谢! 一、MYSQL排序可能用到的排序算法 从MYSQL官方文档和源码的接口来看MYSQL使用BUFFER内部快速排序算法,外部多路归并排序算法,相应的接口函...
本文参考MYSQL官方文档,算法书籍,部分为自己观点可能有误,如果有误请指出共同讨论
转载请说明出处,谢谢!


一、MYSQL排序可能用到的排序算法

从MYSQL官方文档和源码的接口来看MYSQL使用BUFFER内部快速排序算法,外部多路归并排序算法,相应的接口函数为
filesort()函数,注意filesort()是一个总的接口,内部排序实际调用save_index()下的 std::stable_sort\ std:: sort、归并排序
也包含在下面接口可能为merge_many_buff(),也就像执行计划中 using   filesort的含义,他只能代表使用了排序,但是
是否使用到tempfile排序是看不出来的,但是这个可以再trace看到但是线上是不可以trace的研究是可以的,随后我会演示。
还要注意 using   temporary只是说明使用了临时表存储中间结果,这里先不讨论,只看排序。

下面简要介绍两种算法原理

1、buffer内部 快速排序算法
   快速排序是交换排序类算法,是冒泡排序的升级版,其原理是采用分而治之的思想,在于每趟交换设置一个基准点,
   将大于这个基准点的数据放到一边,大于的放到另一边,不断的进行递归完成,对于大数据量的排序快速排序一般
   效率优秀,在文章的最后是一个简单的快速排序的实现,如果对算法感兴趣的可以参考一下。但是至少还能进
   行3种优化
   --小数据优化,因为快速排序对数据量小的时候并不是最优,可以使用其他排序算法如插入排序。
   --尾递归优化,减少栈的使用
   --基准点优化,尽量取到数据中的中间值作为基准点,这样能够让快速排序更加优化。
   
2、外部磁盘多路归并排序
   将内部快速排序后有序的数据文件进行不断的比较得到有序的文件,每次归并多少个文件就是归并的路数
  图中每一个R当然代表的一个有序的磁盘文件
   下图2路归并排序(截取数据结构C语言版)
   
   下图5路归并排序( 截取数据结构C语言版)


二、MYSQL相关参数
    sort_buffer_size:
        当然也就是每次排序的buffer,用作内部快速排序用,如果buffer越大当然产生的物理文件也就越少,但是这个
    参数是会话级别的,过分加大会造成内存不足,默认256K。注意:
    On Linux, there are thresholds of 256KB and
    2MB where larger values may significantly slow down memory allocation, so you should consider staying
    below one of those values
    
    read_rnd_buffer_size:
        除了MRR用到,这里也用到了用于 二次排序的时候对排序好的数据按照primary key(ROW_ID)按照分块的方式再次排序,
    意义同样在回表取数据可以尽量顺序化
    
    max_length_for_sort_data:
        单位为字节(bytes),如果排序返回行的字段长度综合大约这个值,使用二次排序而不是一次排序,默认1024,最小
    值为4,如果加大这个值可能过多的使用一次排序造成高TEMPFILE空间使用而CPU不高,为什么如此后面解释。
    
    max_sort_length:
        单位为字节(bytes),如果排序字段的长度超过这个值,只是用这个参数设置的长度,默认1024,比如text这种字段
    经常会大于这个值,如果加大这个值明显会提高排序的准确性,但是也意味着更高的BUFFER使用和TEMPFILE使用
    
三、监控磁盘排序
    Sort_merge_passes:磁盘排序归并次数,减少sort_buffer_size大小会显著减少Sort_merge_passes值,并且临时文件也
    会变少,第五部分证明
    sys.statements_with_sorting视图
     
四、MYSQL二次访问排序(original method)和一次访问排序(modified method)

1、二次访问排序
--读取数据只包含排序键值和rowid(primary key)放到sort buffer
--在buffer中进行快速排序,如果buffer 满则把内存中的排序数据写入tempfile
--重复上面的过程直到内部快速排序完成,并且生成多个tmepfile文件
--进行多路归并排序源码接口在merge_many_buff(),其中定义了MERGEBUFF,MERGEBUFF2 2个宏
  这个在官方文档上有出现所以提出来说明一下
  /* Defines used by filesort and uniques */
#define MERGEBUFF 7
#define MERGEBUFF2 15
  如果有兴趣的可以仔细看看源码..
--最后一次归并的时候,只有rowid(priamry key)到最后的文件中
--对最后的文件根据rowid(primary key)访问表数据,这样就可以得到排序好的数据
  这里有一个类似MRR的优化,将数据进行分块读入read_rnd_buffer_size进行
  按照rowid(primary key)排序在去访问表的数据,目的在于减少随机读取造成的影响
  但是这是分块的,只能减少不能杜绝,特别是数据量特别大的情况下,因为
  read_rnd_buffer_size只有默认256K.

问题在于对表数据的二次访问,一次在读取数据的时候,后一次在通过排序好的
rowid(primary key)进行数据的访问,并且会出现大量随机访问。

2、一次访问排序
这个就简单了,二次访问排序是把排序键值和rowid(primary key)放到sort buffer,
这个就是关于需要的数据字段全部放到sort buffer比如:
select id,name1,name2 from test order by id;

这里id,name1,name2都会存放到sort buffer中。这样排序好就好了,不需要回表取
数据了,当然这样做的劣势就是更大的sort buffer占用,更大tempfile占用。所以
mysql使用max_length_for_sort_data来限制默认1024,这是指id,name1,name2字段
的bytes长度。
因为不需要回表,所以只要一次访问数据

3、5.7.3后一次访问排序算法的优化
使用一个叫做pack优化的方法,目的在于压缩NULL减少一次访问排序算法对sort buffer和tempfile的过多使用
原文:
without packing, a VARCHAR(255)
column value containing only 3 characters takes 255 characters in the sort buffer. With packing, 
the value requires only 3 characters plus a two-byte length indicator. NULL values require only 
a bitmask.
但是我在做MYSQL TRACE的时候发现这还有一个unpack的过程,并且每一行每一个字段都需要pack unpack
随后证明

关于使用了的那种排序方式在执行计划中都体现为filesort不好弄清楚,但是我们可以通过trace的方式,
在官方文档也说了,但是我使用了对MYSQLD的trace方式来做,效果一致,详细参考第五部分

五、证明观点

1、首先需要证明是使用的是二次访问排序还是一次访问排序,是否启用了pack
官方文档说明
"filesort_summary": {
"rows": 100,
"examined_rows": 100,
"number_of_tmp_files": 0,
"sort_buffer_size": 25192,
"sort_mode": ""
}
sort_mode:
: This indicates use of the original algorithm. 
: This indicates use of the modified algorithm.
: This indicates use of the modified algorithm. Sort
buffer tuples contain the sort key value and packed columns referenced by the query. 
也就是说
sort_key, rowid是二次访问排序
sort_key, additional_fields是一次访问排序
sort_key, packed_additional_fields是一次访问排序+pack方式
好了我们来证明,使用测试表
mysql> show create table testmer \G;
*************************** 1. row ***************************
       Table: testmer
Create Table: CREATE TABLE `testmer` (
  `seq` int(11) NOT NULL,
  `id1` int(11) DEFAULT NULL,
  `id2` int(11) DEFAULT NULL,
  `id3` int(11) DEFAULT NULL,
  `id4` int(11) DEFAULT NULL,
  PRIMARY KEY (`seq`),
  KEY `id1` (`id1`,`id2`),
  KEY `id3` (`id3`,`id4`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
1 row in set (0.01 sec)

mysql>  select * from testmer ;
+-----+------+------+------+------+
| seq | id1  | id2  | id3  | id4  |
+-----+------+------+------+------+
|   1 |    1 |    2 |    4 |    4 |
|   2 |    1 |    3 |    4 |    5 |
|   3 |    1 |    2 |    4 |    4 |
|   4 |    2 |    4 |    5 |    6 |
|   5 |    2 |    6 |    5 |    8 |
|   6 |    2 |   10 |    5 |    3 |
|   7 |    4 |    5 |    8 |   10 |
|   8 |    0 |    1 |    3 |    4 |
+-----+------+------+------+------+
8 rows in set (0.01 sec)

分别在max_length_for_sort_data为1024和max_length_for_sort_data为4对
select * from testmer order by id1;
生成trace文件
意义也就是使用一次访问排序和二次访问排序,因为数据量少也就在sort_buffer
排序就好了。

一次访问排序:
opt: filesort_execution: ending struct
opt: filesort_summary: starting struct
opt: rows: 8
opt: examined_rows: 8
opt: number_of_tmp_files: 0
opt: sort_buffer_size: 34440
opt: sort_mode: ""
opt: filesort_summary: ending struct

为 一次访问排序+pack方式

二次访问排序:
opt: filesort_execution: ending struct
opt: filesort_summary: starting struct
opt: rows: 8
opt: examined_rows: 8
opt: number_of_tmp_files: 0
opt: sort_buffer_size: 18480
opt: sort_mode: ""
opt: filesort_summary: ending struct

为是二次访问排序
可以看到不同,证明了max_length_for_sort_data的作用

其实这个是filesort()函数中的一个调用而已,其实gdb也可以打上断点也能看到
  Opt_trace_object(trace, "filesort_summary")
    .add("rows", num_rows)
    .add("examined_rows", param.examined_rows)
    .add("number_of_tmp_files", num_chunks)
    .add("sort_buffer_size", table_sort.sort_buffer_size())
    .add_alnum("sort_mode",
               param.using_packed_addons() ?
               "" :
               param.using_addon_fields() ?
               "" : "");

2、证明减少sort_buffer_size大小会显著减少Sort_merge_passes值,并且临时文件也
   会变少

 为了完成这个证明我建立了一个大表,降低先sort_buffer为使用如下的语句使用更多的
 tempfile进行排序
 
mysql> select count(*) from  testshared3;
+----------+
| count(*) |
+----------+
|  1048576 |
+----------+
1 row in set (28.31 sec)
 
mysql> set sort_buffer_size=50000;
Query OK, 0 rows affected (0.00 sec)

mysql> show variables like '%sort_buffer_size%';
+-------------------------+---------+
| Variable_name           | Value   |
+-------------------------+---------+
| sort_buffer_size        | 50000   |
+-------------------------+---------+

 mysql> show status like '%Sort_merge%';
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| Sort_merge_passes | 0     |
+-------------------+-------+
1 row in set (0.00 sec)

mysql> explain select * from  testshared3 order by id limit 1048570,1;
+----+-------------+-------------+------------+------+---------------+------+---------+------+---------+----------+----------------+
| id | select_type | table       | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra          |
+----+-------------+-------------+------------+------+---------------+------+---------+------+---------+----------+----------------+
|  1 | SIMPLE      | testshared3 | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 1023820 |   100.00 | Using filesort |
+----+-------------+-------------+------------+------+---------------+------+---------+------+---------+----------+----------------+
mysql> select * from  testshared3 order by id limit 1048570,1;
+------+
| id   |
+------+
|    1 |
+------+
1 row in set (5 min 4.76 sec)
完成后
mysql> show status like '%Sort_merge%';
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| Sort_merge_passes | 63    |
+-------------------+-------+
1 row in set (0.21 sec)

opt: number_of_tmp_files: 378 
临时文件数量378

然后加大sort_buffer_size

mysql>  show variables like '%sort_buffer_size%';
+-------------------------+---------+
| Variable_name           | Value   |
+-------------------------+---------+
| sort_buffer_size        | 262144  |
+-------------------------+---------+

mysql> show status like '%Sort_merge%';
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| Sort_merge_passes | 0     |
+-------------------+-------+
1 row in set (0.04 sec)

还是同样的语句

mysql> select * from  testshared3 order by id limit 1048570,1;
+------+
| id   |
+------+
|    1 |
+------+
1 row in set (5 min 4.76 sec)
mysql> show status like '%Sort_merge%';
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| Sort_merge_passes | 11    |
+-------------------+-------+
opt: number_of_tmp_files: 73
临时文件数量73

得到证明

3、证明有pack和unpack操作,并且每一行每一个字段都需要pack unpack

这个直接查看trace文件是否有接口就好了
实际上可以看到8段如下的操作
>Field::unpack      
Field::unpack
Field::unpack
Field::unpack
Field::unpack
Field::unpack     
Field::unpack"|wc -l
40                                                              
[root@testmy tmp]# cat sortys2.trace|grep ">Field::pack"|wc -l  
40             

刚好是5(字段)*8(行)               

当然我随后对一个大表只有一个字段的表进行一样的测试,既然是一个字段使用
一次访问排序的时候排序的全部字段就是这个字段而已,所以pack和unpack的次数应该
和行数差不多
mysql> select count(*) from  testshared3;
+----------+
| count(*) |
+----------+
|  1048576 |
+----------+
1 row in set (28.31 sec)
mysql> desc testshared3;
+-------+---------+------+-----+---------+-------+
| Field | Type    | Null | Key | Default | Extra |
+-------+---------+------+-----+---------+-------+
| id    | int(11) | YES  |     | NULL    |       |
+-------+---------+------+-----+---------+-------+
1 row in set (0.26 sec)

[root@testmy tmp]# cat mysqld11.trace|grep ">Field::unpack"|wc -l
1048571                    

也得到同样基本相同的结构,证明了一次访问排序中每一行每一个字段都需
要pack、unpack操作,其实在整个trace中还能看到很多类容比如列取出了
一次访问排序的全部字段,这里就不在详解了

六、源码GDB发现内部排序调用STD容器的std::stable_sort std::sort 进行排序

(gdb) n
250         if (param->sort_length < 10)
(gdb) list
245         than quicksort seems to be somewhere around 10 to 40 records.
246         So we're a bit conservative, and stay with quicksort up to 100 records.
247       */
248       if (count <= 100)
249       {
250         if (param->sort_length < 10)
251         {
252           std::sort(m_sort_keys, m_sort_keys + count,
253                     Mem_compare(param->sort_length));
254           return;

这部分mysql上的源码

点击(此处)折叠或打开

  1. /*
  2.     std::stable_sort has some extra overhead in allocating the temp buffer,
  3.     which takes some time. The cutover point where it starts to get faster
  4.     than quicksort seems to be somewhere around 10 to 40 records.
  5.     So we're a bit conservative, and stay with quicksort up to 100 records.
  6.   */
  7.   if (count <= 100)
  8.   {
  9.     if (param->sort_length < 10)
  10.     {
  11.       std::sort(m_sort_keys, m_sort_keys + count,
  12.                 Mem_compare(param->sort_length));
  13.       return;
  14.     }
  15.     std::sort(m_sort_keys, m_sort_keys + count,
  16.               Mem_compare_longkey(param->sort_length));
  17.     return;
  18.   }
  19.   // Heuristics here: avoid function overhead call for short keys.
  20.   if (param->sort_length < 10)
  21.   {
  22.     std::stable_sort(m_sort_keys, m_sort_keys + count,
  23.                      Mem_compare(param->sort_length));
  24.     return;
  25.   }
  26.   std::stable_sort(m_sort_keys, m_sort_keys + count,
  27.                    Mem_compare_longkey(param->sort_length));




最后附上快速排序的代码
带排序数据是13,3,2,9,34,5,102,90,20,2
排序完成后如下:
gaopeng@bogon:~/datas$ ./a.out 
sort result:2 2 3 5 9 13 20 34 90 102 

点击(此处)折叠或打开

  1. /*************************************************************************
  2.   > File Name: qsort.c
  3.   > Author: gaopeng QQ:22389860
  4.   > Mail: gaopp_200217@163.com
  5.   > Created Time: Fri 06 Jan 2017 03:04:08 AM CST
  6.  ************************************************************************/


  7. #include<stdio.h>
  8. #include<stdlib.h>


  9. int partition(int *k,int low,int high)
  10. {

  11.         int point;
  12.         point = k[low]; //基准点,采用数组的第一个值,这里实际可以优化
  13.         while(low<high) //等待low=high一趟交换完成
  14.         {

  15.                 while(low<high && k[high] >=point) //过滤掉尾部大于基准点的值,不需要交换
  16.                 {
  17.                         high--;
  18.                 }
  19.                 k[low] = k[high]; //基准点多次交换为无谓交换直接赋值即可
  20.                 while(low<high && k[low] <=point) //过滤掉头部小于基准点的值,不需要交换
  21.                 {

  22.                         low++;
  23.                 }
  24.                 k[high] = k[low]; //基准点多次交换为无谓交换直接赋值即可
  25.         }
  26.         k[low] = point;
  27.         return low;
  28. }

  29. int q_sort(int *k,int low,int high)
  30. {

  31.         int point;
  32.         if(low<high)
  33.         {

  34.                 point = partition(k,low,high);
  35.                 q_sort(k,low,point-1); //实现递归前半部分
  36.                 q_sort(k,point+1,high); //实现递归后半部分
  37.         }
  38.         return 0;
  39. }

  40. int main()
  41. {

  42.         int i,a[10]={13,3,2,9,34,5,102,90,20,2}; //测试数据
  43.         q_sort(a,0,9); //数组下标0 9

  44.         printf("sort result:");
  45.         for(i=0;i<10;i++)
  46.         {
  47.                 printf("%d ",a[i]);
  48.         }
  49.         printf("\n");
  50. }


相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
3天前
|
缓存 关系型数据库 MySQL
MySQL 索引优化与慢查询优化:原理与实践
通过本文的介绍,希望您能够深入理解MySQL索引优化与慢查询优化的原理和实践方法,并在实际项目中灵活运用这些技术,提升数据库的整体性能。
22 5
|
2天前
|
存储 SQL 关系型数据库
MySQL进阶突击系列(03) MySQL架构原理solo九魂17环连问 | 给大厂面试官的一封信
本文介绍了MySQL架构原理、存储引擎和索引的相关知识点,涵盖查询和更新SQL的执行过程、MySQL各组件的作用、存储引擎的类型及特性、索引的建立和使用原则,以及二叉树、平衡二叉树和B树的区别。通过这些内容,帮助读者深入了解MySQL的工作机制,提高数据库管理和优化能力。
|
15天前
|
SQL 存储 关系型数据库
MySQL进阶突击系列(01)一条简单SQL搞懂MySQL架构原理 | 含实用命令参数集
本文从MySQL的架构原理出发,详细介绍其SQL查询的全过程,涵盖客户端发起SQL查询、服务端SQL接口、解析器、优化器、存储引擎及日志数据等内容。同时提供了MySQL常用的管理命令参数集,帮助读者深入了解MySQL的技术细节和优化方法。
|
2月前
|
存储 关系型数据库 MySQL
MySQL主从复制原理和使用
本文介绍了MySQL主从复制的基本概念、原理及其实现方法,详细讲解了一主两从的架构设计,以及三种常见的复制模式(全同步、异步、半同步)的特点与适用场景。此外,文章还提供了Spring Boot环境下配置主从复制的具体代码示例,包括数据源配置、上下文切换、路由实现及切面编程等内容,帮助读者理解如何在实际项目中实现数据库的读写分离。
MySQL主从复制原理和使用
|
2月前
|
缓存 算法 关系型数据库
Mysql(3)—数据库相关概念及工作原理
数据库是一个以某种有组织的方式存储的数据集合。它通常包括一个或多个不同的主题领域或用途的数据表。
78 5
Mysql(3)—数据库相关概念及工作原理
|
1月前
|
SQL NoSQL 关系型数据库
2024Mysql And Redis基础与进阶操作系列(5)作者——LJS[含MySQL DQL基本查询:select;简单、排序、分组、聚合、分组、分页等详解步骤及常见报错问题所对应的解决方法]
MySQL DQL基本查询:select;简单、排序、分组、聚合、分组、分页、INSERT INTO SELECT / FROM查询结合精例等详解步骤及常见报错问题所对应的解决方法
|
2月前
|
存储 缓存 关系型数据库
MySQL事务日志-Redo Log工作原理分析
事务的隔离性和原子性分别通过锁和事务日志实现,而持久性则依赖于事务日志中的`Redo Log`。在MySQL中,`Redo Log`确保已提交事务的数据能持久保存,即使系统崩溃也能通过重做日志恢复数据。其工作原理是记录数据在内存中的更改,待事务提交时写入磁盘。此外,`Redo Log`采用简单的物理日志格式和高效的顺序IO,确保快速提交。通过不同的落盘策略,可在性能和安全性之间做出权衡。
1684 14
|
2月前
|
SQL 关系型数据库 MySQL
Mysql中搭建主从复制原理和配置
主从复制在数据库管理中广泛应用,主要优点包括提高性能、实现高可用性、数据备份及灾难恢复。通过读写分离、从服务器接管、实时备份和地理分布等机制,有效增强系统的稳定性和数据安全性。主从复制涉及I/O线程和SQL线程,前者负责日志传输,后者负责日志应用,确保数据同步。配置过程中需开启二进制日志、设置唯一服务器ID,并创建复制用户,通过CHANGE MASTER TO命令配置从服务器连接主服务器,实现数据同步。实验部分展示了如何在两台CentOS 7服务器上配置MySQL 5.7主从复制,包括关闭防火墙、配置静态IP、设置域名解析、配置主从服务器、启动复制及验证同步效果。
Mysql中搭建主从复制原理和配置
|
2月前
|
SQL 关系型数据库 MySQL
阿里面试:MYSQL 事务ACID,底层原理是什么? 具体是如何实现的?
尼恩,一位40岁的资深架构师,通过其丰富的经验和深厚的技術功底,为众多读者提供了宝贵的面试指导和技术分享。在他的读者交流群中,许多小伙伴获得了来自一线互联网企业的面试机会,并成功应对了诸如事务ACID特性实现、MVCC等相关面试题。尼恩特别整理了这些常见面试题的系统化解答,形成了《MVCC 学习圣经:一次穿透MYSQL MVCC》PDF文档,旨在帮助大家在面试中展示出扎实的技术功底,提高面试成功率。此外,他还编写了《尼恩Java面试宝典》等资料,涵盖了大量面试题和答案,帮助读者全面提升技术面试的表现。这些资料不仅内容详实,而且持续更新,是求职者备战技术面试的宝贵资源。
阿里面试:MYSQL 事务ACID,底层原理是什么? 具体是如何实现的?
|
2月前
|
存储 SQL 关系型数据库
mysql中主键索引和联合索引的原理与区别
本文详细介绍了MySQL中的主键索引和联合索引原理及其区别。主键索引按主键值排序,叶节点仅存储数据区,而索引页则存储索引和指向数据域的指针。联合索引由多个字段组成,遵循最左前缀原则,可提高查询效率。文章还探讨了索引扫描原理、索引失效情况及设计原则,并对比了InnoDB与MyISAM存储引擎中聚簇索引和非聚簇索引的特点。对于优化MySQL性能具有参考价值。