当前位置: 首页 > 科技观察

算法的艺术:巧妙运用各种排序算法的MySQL排序_0

时间:2023-03-20 17:49:20 科技观察

这篇文章,我们从更宏观的角度来看一下MySQL中关键字的原理。在本文中,我们主要探讨orderby语句的底层原理。阅读本文后,您将了解:orderby语句的排序模式有哪些,以及每种排序模式的优缺点;orderby语句会使用哪些排序算法,在哪些场景下会选择哪种排序算法;如何通过sql的优化方式(执行计划+OPTIMIZER_TRACE日志)查看和分析顺序;如何优化orderby语句的执行效率?(思路:减少行大小,尽量使用索引,最好使用覆盖索引,适当增加排序缓冲区的内存大小)这里我们从数据的维度来看数据和索引structure,也就是把它们都看做是B+树,我们需要数据的时候就从存储引擎的B+树中读取。下面是我们在本文中用作演示示例的表格。假设我们有如下表:索引如下:对应的idx_d索引结构如下(这里我们做了一些夸张的技巧,让一个页面的数据变小,为了展示在索引树中查找的过程):1.如何跟踪执行优化为了方便分析sql的执行过程,我们可以在当前session中开启optimizer_trace:SEToptimizer_trace='enabled=on';然后执行sql,执行后可以通过如下堆栈信息查看执行情况Details:SELECT*FROMinformation_schema.OPTIMIZER_TRACE\G;下面是1selecta,b,c,dfromt20forceindex(idx_abc)wherea=3orderbydlimit100,2;的执行结果,其中匹配a=3的记录有8457条,关注orderbyAttribute:"filesort_priority_queue_optimization":{//是否启用优先级队列"limit":102,//排序后需要取的行数,这里是limit100,2,也就是100+2=102"rows_estimate":24576,//预估参与排序的行数"row_size":123,//行大小"memory_available":32768,//可用内存大小,即设置的sortbuffer大小"chosen":true//是否启用优先队列},..."filesort_summary":{"rows":103,//排序过程中持有的行数"examined_rows":8457,//参与排序的行数,InnoDB层返回的行数"number_of_tmp_files":0,//外部排序时使用的临时文件个数"sort_buffer_size":13496,//内存排序使用的内存大小"sort_mode":"sort_key,additional_fields"//排序方式1.1、排序方式wheresort_mode有以下几种形式:sort_key,rowid:表示排序缓冲区元组包含排序键值和原表行的行id。排序后,需要使用rowid返回表。该算法也称为原始文件排序算法(返回表排序算法);sort_key、additional_fields:表示排序缓冲区元组包含排序键值和查询需要的列。排序后直接从buffertuple中取数据,不返回表。该算法也称为改进的文件排序算法(不返回表排序);sort_key、packed_additional_fields:与前面的形式类似,但是将附加的列(比如varchar类型)紧密地打包在一起,而不是使用定长编码。max_length_for_sort_data属性相关,该属性默认值为1024字节:如果查询列和排序列占用的大小超过该值,则改用sort_key、rowid方式;如果不是,则所有列将被放入排序缓冲区,使用sort_key,additional_fields或sort_key,packed_additional_fields模式;如果查询中的记录过多,则使用sort_key、packed_additional_fields压缩可变列。1.2.排序算法根据参与排序的数据量大小,可以选择不同的排序算法:如果排序结果很小,小于内存,则使用优先队列进行堆排序;例如,下面只取前10条记录将通过优先队列排序:selecta,b,c,dfromt20forceindex(idx_abc)wherea=3orderbydlimit10;如果排序限制n,m,n太大,也就是很晚才需要排序的数据,会使用sortbuffer进行快速排序:如下,其中a=1的数据项有3个表,但是由于记录需要限制在最后面,MySQL会比较优先队列排序和快速排序的开销,选择更合适的排序算法,这里最后放弃了优先队列,改用排序缓冲区用于快速排序:selecta,b,c,dfromt20forceindex(idx_abc)wherea=1orderbydlimit300,2;快速对内存中的排序缓冲区进行排序,将结果放入排序后的临时文件中,最后对所有排序后的临时文件进行合并排序,得到最终结果;如下,a=3的记录超出了sortbuffer,我们需要找到排序后的数据从1000行开始,sortbuffer放不下1000行数据。最终MySQL选择使用sortbuffer进行批量快速排序,对最终结果进行合并排序:selecta,b,c,dfromt20forceindex(idx_abc)wherea=3orderbydlimit1000,10;2.按索引排序,避免排序执行如下sql:selecta,b,c,dfromt20forceindex(idx_d)wheredlike't%'orderbydlimit2;我们看一下执行计划:发现Extra列为:Usingindexcondition,也就是这里只有索引没有了。执行过程如下图所示:使用idx_d索引进行range_scan查找,扫描到4条记录,然后继续用orderby去索引,顺序已经排好了,直接取前两个,然后去到聚簇索引查询完整记录,返回最终需要的字段作为查询结果。这个过程只需要索引的帮助。如何查看和修改排序缓冲区的大小?让我们看一下当前排序缓冲区的大小:可以发现排序缓冲区的默认大小是512k。我们可以设置这个属性的大小:SETGLOBALsort_buffer_size=32*1024;或SETsort_buffer_size=32*1024;下面我们统一设置sortbuffer为32kSETsort_buffer_size=32*1024;如果排序结果小且小于排序缓冲区,则使用优先队列进行堆排序;比如只取前10条记录如下:selecta,b,c,dfromt20forceindex(idx_abc)wherea=3orderbydlimit10;a=3记录总数:8520查看执行计划:发现where条件使用索引,orderbylimit使用排序。我们仔细看看执行的optimizer_trace日志:"filesort_priority_queue_optimization":{"limit":10,"rows_estimate":27033,"row_size":123,"memory_available":32768,"chosen":true//使用优先级排队排序},“filesort_execution”:[],“filesort_summary”:{“rows”:11,“examined_rows”:8520,“number_of_tmp_files”:0,“sort_buffer_size”:1448,“sort_mode”:“sort_key,additional_fields”}found这里使用了一个优先级队列来进行排序。排序方式为:sort_key,additional_fields,即先回表查询完整记录,将所有需要查找排序的字段放入排序缓冲区进行排序。所以执行过程如下图所示:通过where条件a=3扫描到8520条记录;回表查找记录;将8520条记录中的必填字段放入排序缓冲区;在排序缓冲区中进行堆排序;好的结果中,取limit10的前10个entry,写入netbuffer,准备发送给client。3.2.内部快速排序如果排序限制n,m,n太大,也就是说需要在排序结束时取数据,那么会使用排序缓冲区进行快速排序。MySQL会比较优先队列排序和归并排序的开销,选择更合适的排序算法。如何衡量是使用优先队列还是内存快速排序?一般来说,快速排序算法比堆排序效率更高,但是堆排序实现的优先级队列不需要对所有元素进行排序就可以得到按限制排序的结果。.MySQL源码中声明快速排序的速度是堆排序的三倍。在实际排序中,算法会根据待排序数的大小进行切换。如果数据量太大,则使用快速排序代替。有如下SQL:selecta,b,c,dfromt20forceindex(idx_abc)wherea=1orderbydlimit300,2;我们将排序缓冲区设置为32k:SETsort_buffer_size=32*1024;有3条记录a=1。查看执行计划:可以发现where条件使用了索引,orderbylimit使用了排序。让我们仔细看看执行的optimizer_trace日志:"filesort_priority_queue_optimization":{"limit":302,"rows_estimate":27033,"row_size":123,"memory_available":32768,"strip_additional_fields":{"row_size":57,"sort_merge_cost":33783,"priority_queue_cost":61158,"chosen":false//对比发现快排的开销比优先队列低,这里优先队列不适用}},"filesort_execution":[],"filesort_summary":{"rows":3,"examined_rows":3,"number_of_tmp_files":0,"sort_buffer_size":32720,"sort_mode":""}可以发现这里最终放弃了优先级队列,转而使用sortbuffer进行快速排序。所以执行过程如下图所示:通过where条件a=1扫描3条记录;回表查找记录;将3条记录中的必填字段放入排序缓冲区;在排序缓冲区中进行快速排序;为了得到好的结果,取limit300条、2条第300条和第301条记录,写入netbuffer,准备发送给客户端。3.3.ExternalMergeSort当排序涉及的数据过多,一次无法放入排序缓冲区时,我们将内存中的排序缓冲区逐批排序,并将结果放入排序临时文件中,最后使所有排序数据排序。将有序的临时文件进行合并排序,得到最终的结果。有如下sql:selecta,b,c,dfromt20forceindex(idx_abc)wherea=3orderbydlimit1000,10;其中a=3有8520条记录。执行计划如下:image-20200614171147989可以发现where用的是index,orderbylimit用的是排序。进一步查看执行的optimizer_trace日志:"filesort_priority_queue_optimization":{"limit":1010,"rows_estimate":27033,"row_size":123,"memory_available":32768,"strip_additional_fields":{"row_size":57,"chosen":false,"cause":"not_enough_space"//sortbuffer空间不够,无法使用优先队列进行排序}},"filesort_execution":[],"filesort_summary":{"rows":8520,"examined_rows":8520,"number_of_tmp_files":24,//使用24个外部文件进行排序"sort_buffer_size":32720,"sort_mode":""}我们可以看到由于限制为1000,我们需要返回排序后1000行为了以后的记录,很明显sortbuffer已经支持不了这么大的优先级队列了,所以改用sortbuffer内存排序,这里需要在sortbuffer中批量进行快速排序获取多个排序的外部临时文件。最后执行归并排序。(外部临时文件的位置由tmpdir参数指定)流程如下图所示:4.排序方式case4.1、sort_key,additional_fields方式sort_key,additional_fields,sortbuffertuple包含sortkeyvalue和sortbuffertuple查询需要的列(先回表获取需要的数据存入sortbuffer),排序后直接从buffertuple中获取数据,无需再次回表。上面2.3.1和2.3.2的例子都是排序方式,例子不再赘述。4.2、modesort_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模式:SETmax_length_for_sort_data=100;此时执行sql:selecta,b,c,dfromt20forceindex(idx_abc)wherea=3orderbydlimit10;然后查看sql执行的optimizer_trace日志:"filesort_priority_queue_optimization":{"limit":10,"rows_estimate":27033,"row_size":49,"memory_available":32768,"chosen":true},"filesort_execution":[],"filesort_summary":{"rows":11,"examined_rows":8520,"number_of_tmp_files":0,"sort_buffer_size":632,"sort_mode":""可以发现这次切换以sort_key、rowid方式,在该方式下,执行过程如下:whereconditiona=3扫描8520条记录;回表查找记录;找到这8520条记录的id和d字段,放入sortbuffer中进行堆排序;排序完成后,取前10条记录;取10个item的id返回表查询需要的a,b,c,d字段的值;结果依次返回给客户端。可以发现,由于行记录太大,排序缓冲区中只存储了需要排序的字段和主键id,用时间换空间。最后排序完成,再次从聚簇索引中找到所有需要的字段返回给客户端,显然多了一次读盘回表操作,整体效率略低。5.orderby优化总结基于上面的介绍,我们可以总结出如下针对orderby语句的优化方法:orderby字段尽量使用定长字段类型,因为sort字段不支持压缩;orderbyfield必要时可以使用变长,尽量控制长度,道理同上;尽量不要在查询中使用select*,避免查询太多,导致orderby时排序缓冲内存不足,导致外部排序,或者rowsize超过max_length_for_sort_data,导致sort_key,rowid排序方式,导致多盘读取并影响性能;排序字段和相关条件尽量加联合索引,最好使用覆盖索引。参考文献[1]:滴滴云.MySQL全表COUNT(*)简要说明。知乎。摘自https://zhuanlan.zhihu.com/p/54378839[2]:MySQL。8.2.1.14ORDERBY优化。取自https://dev.mysql.com/doc/refman/5.7/en/order-by-optimization.html[3]:MySQL:排序(filesort)详解。摘自https://www.jianshu.com/p/069428a6594e[4]:MYSQL实现了ORDERBYLIMIT方法和优先队列(堆排序)。转自http://blog.itpub.net/7728585/viewspace-2130920/本文转载自微信公众号《Java架构杂谈》可通过以下二维码关注。转载本文请联系Java架构杂谈公众号。博客地址:https://www.itzhai.com