MySQL查询优化之order by的优化

原文地址:https://dev.mysql.com/doc/refman/5.7/en/order-by-optimization.html

译文:

8.2.1.14 ORDER BY 优化

本节描述MySQL何时可以使用索引来满足order by子句,不能使用索引时使用的文件排序操作,以及优化器提供的有关order by的执行计划信息

有和没有limit的order by子句可能会返回不同排序的行,可以参考Section 8.2.1.17, “LIMIT Query Optimization”

使用索引来满足ORDER BY

在一些情况下,MySQL可能会使用索引来满足一个order by子句,而避免涉及到执行文件排序操作的额外排序。

只要索引中所有未使用的部分和所有额外的ordre by列都是where子句中的常量,即使order by与索引不完全匹配,也可以使用索引。如果索引不包含查询访问的所有列,则仅当索引访问比其他访问方法便宜时才使用该索引。

假如在(key_part1,key_part2)上有一个索引,下面的查询可能会使用索引来解析order by部分。优化器是否会这样做取决于如果不在索引中的列也必须读取的话,读取索引是否比表扫描更有效。

    1)在这个查询中,(key_part1,key_part2)上的索引使得优化器避免了排序:

  • SELECT * FROM t1 ORDER BY key_part1, key_part2;

    但是,这个查询使用了select * ,这可能会查询出不只key_part1和key_part2这两列。在这种情况下,扫描整个索引并查找表行以找出不在索引中的列可能会比扫描表并对结果进行排序更昂贵。如果是这样的话,优化器可能不会使用索引。如果select *只查询索引列,那么将使用索引并避免排序。

    如果t1是一个InnoDB表,表的主键是索引的隐式部分,该索引可用于解析该查询的order by:

  • SELECT pk, key_part1, key_part2 FROM t1 ORDER BY key_part1, key_part2;

    2)在下面的查询中,key_part1是常量,所以通过索引访问的所有行都是key_part2顺序,如果where子句具有足够的选择性,使得索引范围扫描比表扫描更便宜,则(key_part1, key_part2)上的索引会避免排序:

  • SELECT * FROM t1 WHERE key_part1 = constant ORDER BY key_part2;

    3)在接下来的两个查询中,是否使用索引类似于先前没有desc的相同查询:

  • SELECT * FROM t1 ORDER BY key_part1 DESC, key_part2 DESC;
    
    SELECT * FROM t1 WHERE key_part1 = constant ORDER BY key_part2 DESC;

    4)在接下来的两个查询中,key_part1将与一个常量进行比较。如果where子句具有足够的选择性,使得索引范围扫描比表扫描更便宜,则会使用索引:

  • SELECT * FROM t1 WHERE key_part1 > constant ORDER BY key_part1 ASC;
    
    SELECT * FROM t1 WHERE key_part1 < constant ORDER BY key_part1 DESC;

    5)在下一个查询中,order by没有命名key_part1,但是所有选中的行都有一个常量key_part1值,所以索引仍然可以使用:

  • SELECT * FROM t1 WHERE key_part1 = constant1 AND key_part2 > constant2
    ORDER BY key_part2;

在某些情况下,MySQL不能使用索引来解析order by,尽管它仍然可以使用索引来查找与where子句匹配的行。例子:

    1)查询在不同的索引上使用order by:

  • SELECT * FROM t1 ORDER BY key1, key2;

    2)查询在索引的非连续部分上使用order by:

  • SELECT * FROM t1 WHERE key2=constant ORDER BY key1_part1, key1_part3;

    3)查询混合使用ASC和DESC:

  • SELECT * FROM t1 ORDER BY key_part1 DESC, key_part2 ASC;

    4)用于获取行的索引与在order by中使用的索引不同:

  • SELECT * FROM t1 WHERE key2=constant ORDER BY key1;

    5)查询使用带有表达式的order by,该表达式包含索引列名以外的其他术语:

  • SELECT * FROM t1 ORDER BY ABS(key);
    SELECT * FROM t1 ORDER BY -key;

    6)查询连接许多表,而且order by子句中的列不全是来自用于检索行的第一个非常量表(即explain输出结果中连接类型不是cost的第一个表);

    7)查询中有不同的order by和group by表达式;

    8)只有在order by子句中指定的列的前缀上才有索引。在这种情况下,索引不能用于完全解析排序顺序。例如,如果只索引声明为CHAR(20)列的前10个字节,则索引无法区分超过第10字节的值,此时就需要文件排序;

    9)索引没有按顺序存储行。例如,内存表中的哈希索引就是这样的;

排序索引的可用性可能会受到列别名的使用的影响。假设列t1.a上建有索引。在这个语句中,select列表中的列的名称是a,它指的是t1.a,和在order by引用的a一样,所以t1.a上的索引可以使用:

  • SELECT a FROM t1 ORDER BY a;

在这个语句中,select列表中的列名仍然是a,但是它是一个别名。它指的是ABS(a),和在order by中引用的a一样,所以在t1.a上的索引可以使用:

  • SELECT ABS(a) AS a FROM t1 ORDER BY a;

在下面的语句中,order by引用了一个在select的列名列表中没有的名字。但是在t1表中有名字为a的列,所以order by引用t1.a的时候,t1.a上的索引可以被使用(当然,结果的排序顺序可能与根据ABS(a)排序的顺序完全不同):

  • SELECT ABS(a) AS b FROM t1 ORDER BY a;

默认情况下,如果查询中同时包含order by col1,col2,···,MySQL会对group bycol1,col2···分类。如果查询中包含一个显式order by子句,该子句包含相同的列列表,MySQL会在不造成任何速度损失的情况下把其优化掉,尽管排序仍然会发生。

如果查询中包含group by,但我们希望避免对结果排序的开销,则可以通过指定order by null来抑制排序。

示例:

  • INSERT INTO foo
    SELECT a, COUNT(*) FROM bar GROUP BY a ORDER BY NULL;

优化器仍然可以选择使用排序来实现分组操作。order by null会抑制结果的排序,而不是事先通过分组操作来确定结果的排序。

    Note

  • 默认情况下,group by进行隐式排序(即,在没有asc或desc标志符的情况下,对group by后的列进行排序)。但是,不建议使用隐式排序(即在没有asc或desc标志符的情况下进行排序)或显式排序(即在group by列中使用显式asc或desc指示符)对group by后的列进行排序。想要生成给定的排序顺序,最好使用order by子句。

使用文件排序来满足ORDER BY

如果不能使用索引来满足order by子句,MySQL会执行读取表行并对结果进行排序的文件排序操作。文件排序构成查询执行中的一个额外排序阶段。

为了获得用于文件排序操作的内存,优化器预先给变量sort_buffer_size分配了一定数量的字节。单个会话可以根据需要更改该变量的会话值,以避免过度使用内存,或者根据需要分配更多内存。

如果结果集太大而无法装入内存,文件排序操作将根据需要使用临时磁盘文件。某些类型的查询特别适合完全在内存中的文件排序操作。例如,优化器可以以如下形式使用文件排序来有效地处理内存中的查询(和子查询)中的order by操作,而不需要临时文件:

  • SELECT ... FROM single_table ... ORDER BY non_index_column [DESC] LIMIT [M,]N;

这种查询在web应用程序中很常见,这些应用程序只显示来自较大结果集的一些行。示例:

  • SELECT col1, ... FROM t1 ... ORDER BY name LIMIT 10;
    SELECT col1, ... FROM t1 ... ORDER BY RAND() LIMIT 15;

ORDER BY优化的影响因素

对于不使用文件排序的慢排序查询,可以尝试将系统变量max_length_for_sort_data降低到一个适合触发文件排序的值(将该变量的值设置得过高的一个症状是高磁盘活动和低CPU活动的组合)。

为了增加order by的速度,检查是否可以让MySQL使用索引,而不是额外的排序阶段。如果这是不可能的,尝试以下策略:

    1)增大sort_buffer_size变量的值。理想情况下,该值应该足够大,可以容纳整个结果集(以避免写到磁盘和合并传递),但是该变量的最小值也必须足够大到可以容纳15个元组(合并最多15个临时磁盘文件,并且每个文件必须有至少一个元组的内存空间)。

要考虑到存储在排序缓冲区中的列值的大小受max_sort_length系统变量值的影响。例如,如果元组存储长字符串列的值,并且增加了max_sort_length的值,那么排序缓冲区元组的大小也会增加,sort_buffer_size可能也需要增加。对于作为字符串表达式结果计算的列值(例如调用字符串值函数的列值),文件排序算法不能告诉表达式值的最大长度,因此必须为每个元组分配max_sort_length个字节。

要监视合并传递的数量(合并临时文件),可以检查Sort_merge_passes状态变量;

    2)增加read_rnd_buffer_size变量值,以便一次读取更多行;

    3)将tmpdir系统变量更改为指向具有大量空闲空间的专用文件系统。该变量值可以列出以循环方式使用的几个路径;可以使用此特性将负载分散到多个目录中。在Unix上使用冒号字符(:)分隔路径,在Windows上使用分号字符(;)分隔路径。路径应该命名位于不同物理磁盘上的文件系统中的目录,而不是同一磁盘上的不同分区。

可获得的ORDER BY执行计划信息

使用explain(可以参考Section 8.8.1, “Optimizing Queries with EXPLAIN”),我们可以检查MySQL是否使用了索引来解析order by子句:

    1)如果explain输出结果中的Extra列不包含Using filesort,则使用索引,不执行文件排序;

    2)如果explain输出结果中的Extra列包含Using filesort,则执行文件排序,不使用索引;

此外,如果执行文件排序,优化器跟踪的输出包括一个filesort_summary块。例如:

"filesort_summary": {
  "rows": 100,
  "examined_rows": 100,
  "number_of_tmp_files": 0,
  "sort_buffer_size": 25192,
  "sort_mode": "<sort_key, packed_additional_fields>"
}

sort_mode的值提供了排序缓冲区中元组内容的信息:

    1)<sort_key, rowid>:这表明排序缓冲区元组是一对包含原始表行的排序键值和行ID的对。元组按键值排序,行ID用于从表中读取行。

    2)<sort_key, additional_fields>:这表示排序缓冲区元组包含查询引用的排序键值和列。元组按键值排序,列值直接从元组读取。

    3)<sort_key, packed_additional_fields>:与前面的变体类似,但是附加列是紧密地打包在一起的,而不是使用固定长度的编码。

explain不会区分优化器是否在内存中执行文件排序。在优化器跟踪的输出中可以看到在内存中文件排序的使用。寻找filesort_priority_queue_optimization。关于优化器跟踪的信息,可以参考MySQL Internals: Tracing the Optimizer

PS:由于水平有限,译文中难免会存在谬误,欢迎批评指正。

猜你喜欢

转载自blog.csdn.net/qq_41080850/article/details/85345260