10.2.1.16 ORDER BY 优化
本节描述了MySQL何时可以使用索引来满足一个ORDER BY子句,filesort
操作在无法使用索引时用于排序,以及优化器提供的关于ORDER BY的执行计划信息。
带有和不带有LIMIT的ORDER BY可能会返回不同的顺序,如第10.2.1.19节“LIMIT查询优化”中讨论的那样。
使用索引来满足ORDER BY
在某些情况下,MySQL可以使用索引来满足一个ORDER BY子句并避免执行filesort
操作所需的额外排序。
即使ORDER BY子句不完全匹配索引,也可能使用索引,只要所有未使用的索引部分和所有额外的ORDER BY列在WHERE子句中是常量。如果索引不包含查询访问的所有列,索引将被用于索引访问,如果索引访问比其他访问方法便宜。
假设有一个名为(
的索引,以下查询可能会使用该索引来解决ORDER BY部分。优化器是否实际这样做取决于读取索引是否比表扫描更有效,如果必须同时读取不在索引中的列。key_part1
, key_part2
)
-
在这个查询中,名为
(
的索引使得优化器可以避免排序: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;
-
在这个查询中,
key_part1
是一个常量,所以通过索引访问的所有行都按key_part2
顺序排列,并且名为(
的索引避免了排序,如果WHERE子句足够选择性,使得索引范围扫描比表扫描便宜:key_part1
,key_part2
)SELECT * FROM t1 WHERE key_part1 = constant ORDER BY key_part2;
-
在接下来的两个查询中,是否使用索引与之前展示的相同查询类似,但不带有DESC:
SELECT * FROM t1 ORDER BY key_part1 DESC, key_part2 DESC; SELECT * FROM t1 WHERE key_part1 = constant ORDER BY key_part2 DESC;
-
ORDER BY子句中的两个列可以以相同方向(都为
ASC
,或都为DESC
)排序,也可以以相反方向排序(一个为ASC
,另一个为DESC
)。索引使用的条件是索引必须具有相同的同质性,但不必具有相同的实际方向。如果查询混合了
ASC
和DESC
,优化器可以使用名为(
的索引的列,如果索引也使用了相应的混合升序和降序列:key_part1
,key_part2
)SELECT * FROM t1 ORDER BY key_part1 DESC, key_part2 ASC;
优化器可以使用名为
(
的索引,如果key_part1
,key_part2
)key_part1
是降序,key_part2
是升序。它也可以使用那些列的索引(进行反向扫描)如果key_part1
是升序,key_part2
是降序。请参阅第10.3.13节“Descending Indexes”。 -
在接下来的两个查询中,
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;
-
在下一个查询中,
ORDER BY
不指定key_部分1
,但所有选出的行都有一个恒定的key_部分1
值,因此索引仍然可以使用:SELECT * FROM t1 WHERE key_part1 = constant1 AND key_part2 > constant2 ORDER BY key_part2;
在某些情况下,MySQL 不能 使用索引来解决 ORDER BY
,尽管它可能仍然使用索引来找到匹配 WHERE
子句的行。例子:
-
查询使用了不同索引的
ORDER BY
:SELECT * FROM t1 ORDER BY key1, key2;
-
查询使用了非连续部分的索引进行
ORDER BY
:SELECT * FROM t1 WHERE key2=constant ORDER BY key1_part1, key1_part3;
-
用于检索行的索引与
ORDER BY
中使用的索引不同:SELECT * FROM t1 WHERE key2=constant ORDER BY key1;
-
查询使用了包含除了索引列名之外的表达式的
ORDER BY
:SELECT * FROM t1 ORDER BY ABS(key); SELECT * FROM t1 ORDER BY -key;
-
查询连接了许多表,
ORDER BY
中的列不都是从第一个非常量表中检索行的表中来的。(这是在EXPLAIN
输出中没有const
连接类型的第一个表。) -
查询有不同的
ORDER BY
和GROUP BY
表达式。 -
只有索引列的前缀被索引。在这种情况下,索引不能完全解决排序顺序。例如,如果只对一个
CHAR(20)
列的前 10 个字节进行了索引,索引就不能区分超过第 10 个字节的值,因此需要filesort
。 -
索引不存储按顺序排列的行。例如,这是对一个
MEMORY
表中的HASH
索引的真实情况。
索引可用于排序的可用性可能受到列别名使用的影响。假设表 t1
的列 a
被索引。在这个语句中,选择列表中的列名是 a
。它引用 t1.a
,正如 ORDER BY
中对 a
的引用一样,所以可以使用索引 t1.a
:
SELECT a FROM t1 ORDER BY a;
在这个语句中,选择列表中的列名也是 a
,但它是别名名称。它引用 ABS(a)
,正如 ORDER BY
中对 a
的引用一样,所以不能使用索引 t1.a
:
SELECT ABS(a) AS a FROM t1 ORDER BY a;
在以下语句中,ORDER BY
指定的名称不是选择列表中的列名。但是,有一个名为 a
的列在表 t1
中,所以 ORDER BY
引用的是 t1.a
,并且可以使用索引 t1.a
。(排序顺序与对 ABS(a)
的排序顺序可能完全不同,当然了。)
SELECT ABS(a) AS b FROM t1 ORDER BY a;
在 MySQL 8.3 及之前版本中,GROUP BY
隐式进行排序。在 MySQL 8.4 中,这种情况不再发生,因此在必要时提供 ORDER BY NULL
来抑制隐式排序(如前所述)是不再需要的。然而,查询结果可能与之前的 MySQL 版本不同。为了产生特定的排序顺序,请提供一个 ORDER BY
子句。
如果索引不能用于满足 ORDER BY
子句,MySQL 将执行一个 filesort
操作,该操作读取表行并对它们进行排序。一个 filesort
是查询执行的额外排序阶段。
为了获得用于 filesort
操作的内存,优化器会按需分配内存缓冲区直到达到由 sort_buffer_size
系统变量指定的大小。这使得用户可以将 sort_buffer_size
设置为更大的值以加速更大排序,而无需担心对小排序的内存使用过多。(这可能不会在 Windows 上发生,因为它有一个弱的多线程 malloc
。)
一个 filesort
操作使用临时磁盘文件作为必要时,如果结果集太大以至于无法完全放入内存。在某些类型的查询中,优化器可以在没有临时文件的情况下高效地处理 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优化
为了提高ORDER BY
的速度,检查MySQL是否可以使用索引而不是额外的排序阶段。如果这不可能,请尝试以下策略:
-
增加
sort_buffer_size
变量值。理想情况下,值应该足够大,以便整个结果集都能放入排序缓冲区(避免写入磁盘和合并passes)。注意,存储在排序缓冲区中的列值大小受到
max_sort_length
系统变量值的影响。例如,如果表元组存储长字符串列值,并且您增加了max_sort_length
的值,排序缓冲区元组大小也会随之增加,这可能要求您增加sort_buffer_size
。为了监控合并passes(合并临时文件)的数量,请检查
Sort_merge_passes
状态变量。 -
增加
read_ rnd_buffer_size
变量值,以便一次读取更多行。 -
更改
tmpdir
系统变量的值,使其指向一个拥有大量免费空间的专用文件系统。该变量值可以列出几个路径,这些路径将按顺序使用;您可以利用这个特性来在多个目录之间分散负载。路径应该以Unix上使用的冒号字符(:
)或Windows上使用的分号字符(;
)分隔。路径应指向位于不同物理磁盘上的文件系统,不是同一磁盘上的不同分区。
使用EXPLAIN
(参见Section 10.8.1, “Optimizing Queries with EXPLAIN”),您可以检查MySQL是否可以使用索引来解决ORDER BY
子句:
此外,如果进行了filesort
,优化器跟踪输出将包含一个filesort_ summary
块。例如:
"filesort_summary": {
"rows": 100,
"examined_rows": 100,
"number_of_tmp_files": 0,
"peak_memory_used": 25192,
"sort_mode": "<sort_key, packed_additional_fields>"
}
peak_memory_used
指示在排序过程中使用的最大内存。这是一个不一定会达到sort_buffer_size
变量值的值。优化器按需分配排序缓冲区内存,开始时使用一个小数目,并根据需要增加到sort_buffer_size
字节。)
sort_mode
值提供了关于排序缓冲区元组内容的信息:
-
<sort_key, rowid>
:这表示排序缓冲区中的元组是包含排序键值和原表行ID的对。元组按排序键值排序,使用行ID从表中读取行。 -
<sort_key, additional_fields>
:这表示排序缓冲区中的元组包含排序键值和查询引用列的列。元组按排序键值排序,列值直接从元组中读取。 -
<sort_key, packed_additional_fields>
:与前一种变体相同,但额外列紧凑排列而不是使用固定长度编码。
注意,EXPLAIN
不区分优化器是否在内存中进行了filesort
。内存中的filesort
使用可以在优化器跟踪输出中看到。查找filesort_priority_queue_optimization
。关于优化器跟踪的信息,请参阅MySQL Internals: Tracing the Optimizer。