什么是 using filesort?怎么避免?
What is using filesort and how to avoid it?
考察要点
这道题面试官想看你是否深入理解 MySQL 的排序原理、执行计划的解读能力以及 SQL 优化的实战能力。核心考察点包括:
- 排序机制:是否清楚 MySQL 何时使用索引排序,何时会退化到文件排序(filesort)。
- 性能影响:是否明白
filesort并非总是磁盘排序,它包含内存排序和磁盘排序两种情况,以及它们对性能的巨大差异。 - 优化能力:是否能通过设计和使用合适的索引来避免
filesort,从而提升查询性能。
核心概念
Using filesort 是 MySQL EXPLAIN 命令输出中 Extra 字段的一个值,它表示 MySQL 无法利用索引来完成排序操作,因此必须在结果集上执行一个额外的排序步骤。
这个名字有一定误导性,filesort 并不一定在磁盘文件上进行。
-
排序过程:
- 内存排序:如果待排序的数据量不大,能够在
sort_buffer_size系统变量定义的内存空间内完成,MySQL 会在内存中执行快速排序。这个过程相对较快。 - 磁盘排序(外部排序):如果待排序的数据量超过了
sort_buffer_size,MySQL 会将数据分块,对每个块进行内存排序后存入临时文件,最后再将所有排好序的临时文件进行归并排序(merge sort)。这个过程涉及大量磁盘 I/O,性能极差。
- 内存排序:如果待排序的数据量不大,能够在
-
Filesort 的两种算法:
- 单路排序(Single-pass):这是 MySQL 4.1 之后默认的优化算法。一次性读取所有需要的列(
SELECT的列 +ORDER BY的列)到sort_buffer中,排序后直接返回结果。优点是减少了 I/O,但会占用更多的sort_buffer空间。 - 双路排序(Two-pass):这是早期的算法,也称为回表排序。
- 第一次读取:只读取排序列和主键(或行指针
rowid)到sort_buffer中进行排序。 - 第二次读取:排序完成后,根据排好序的主键/
rowid回表查询所需要的其他列。 优点是sort_buffer占用小,但缺点是需要两次 I/O,如果数据量大,第二次回表是大量的随机 I/O,性能很差。当SELECT的列总大小超过max_length_for_sort_data系统变量的阈值时,MySQL 会从单路排序退化为双路排序。
- 第一次读取:只读取排序列和主键(或行指针
- 单路排序(Single-pass):这是 MySQL 4.1 之后默认的优化算法。一次性读取所有需要的列(
-
如何识别
Using filesort: 通过EXPLAIN分析 SQL 查询计划。示例: 假设有表
employees:sql1CREATE TABLE employees (2 id INT PRIMARY KEY AUTO_INCREMENT,3 name VARCHAR(50),4 age INT,5 hire_date DATE,6 INDEX idx_name (name)7);执行一个无法使用索引排序的查询:
sql1EXPLAIN SELECT id, name, age FROM employees ORDER BY age;执行计划输出:
id select_type table type possible_keys key key_len ref rows Extra 1 SIMPLE employees ALL NULL NULL NULL NULL 1000 Using filesort 这里的
type是ALL(全表扫描),Extra字段显示Using filesort,说明 MySQL 在扫描全表后,需要对结果集按age字段进行额外排序。
常见低分回答
-
“filesort 就是在磁盘上排序,非常慢。”
- 错误点:混淆了
filesort和外部排序。filesort是一个逻辑概念,它首先尝试在内存中排序,只有内存不足时才会使用磁盘。面试者应清晰说明内存和磁盘两种情况。
- 错误点:混淆了
-
“要避免 filesort,给
ORDER BY的列加上索引就行了。”- 错误点:过于简化。单独为
ORDER BY的列创建索引通常无效。优化器需要一个能够覆盖WHERE条件和ORDER BY条件的复合索引,并且列的顺序、排序方向都必须满足特定规则。例如WHERE a=1 ORDER BY b,需要(a, b)索引,单(b)索引没用。
- 错误点:过于简化。单独为
-
“只要出现了 filesort,就应该调大
sort_buffer_size。”- 错误点:本末倒置。调大
sort_buffer_size只是治标不治本的“安慰剂”,它仅能将磁盘排序变为内存排序,但排序动作本身并未消除。在高并发场景下,为每个会话分配巨大的sort_buffer会迅速耗尽服务器内存。首选方案永远是通过索引消除排序。
- 错误点:本末倒置。调大
-
“我给
WHERE列和ORDER BY列分别建了索引,但还是filesort。”- 错误点:不理解 MySQL 单次查询通常只会使用一个索引。优化器不会先用
idx_where过滤数据,再用idx_orderby排序。必须创建一个能同时满足过滤和排序的复合索引。
- 错误点:不理解 MySQL 单次查询通常只会使用一个索引。优化器不会先用
工程实践与优化
核心思想是利用索引的有序性,使得从索引中取出的数据已经满足 ORDER BY 的顺序要求,从而避免额外的排序步骤。
如何通过索引避免 filesort:
-
创建合适的复合索引 索引的列顺序必须遵循最左前缀原则,并与
WHERE、ORDER BY子句的列顺序和类型相匹配。 黄金法则:将WHERE中的等值条件列、范围条件列、ORDER BY列依次放入复合索引中。- 场景:
SELECT * FROM employees WHERE name = 'Alice' ORDER BY hire_date; - 错误索引:
INDEX(hire_date)或INDEX(name)单独存在。 - 正确索引:
CREATE INDEX idx_name_hire_date ON employees(name, hire_date); - 原理:MySQL 通过
idx_name_hire_date索引找到name = 'Alice'的所有记录。由于索引内部是按(name, hire_date)排序的,所以对于name相同的数据,其hire_date已经天然有序。MySQL 只需按顺序读取即可,无需filesort。
- 场景:
-
保证
ORDER BY的排序方向与索引一致- 场景:
... ORDER BY a ASC, b DESC; - MySQL 8.0+:支持降序索引。可以创建
CREATE INDEX idx_a_asc_b_desc ON t(a ASC, b DESC);来精确匹配,避免filesort。 - MySQL 5.7 及更早版本:索引只能按同一方向(默认
ASC)存储。此时,MySQL 可以利用索引满足a ASC,但无法满足b DESC,仍然会触发filesort。一种妥协是拆分查询或在应用层排序。
- 场景:
-
WHERE子句中的范围查询会中断索引排序效果- 场景:
SELECT * FROM employees WHERE name > 'A' ORDER BY hire_date; - 索引:
INDEX(name, hire_date) - 结果:仍然会
filesort。因为name > 'A'是一个范围查询,匹配这个范围的记录,其hire_date并不是全局有序的。优化器会使用索引(name)来过滤数据,但无法利用其排序特性来满足ORDER BY hire_date。
- 场景:
-
ORDER BY的列必须是索引的一部分且没有跳过中间列- 场景:
SELECT * FROM t WHERE a = 1 ORDER BY c; - 索引:
INDEX(a, b, c) - 结果:会
filesort。因为ORDER BY跳过了索引中的b列。MySQL 无法保证在a固定的情况下,c是有序的。 - 正确用法:
... WHERE a = 1 ORDER BY b, c;或... WHERE a = 1 AND b = 2 ORDER BY c;
- 场景:
验证优化效果:
创建 idx_name_hire_date 索引后,再次执行 EXPLAIN:
1EXPLAIN SELECT id, name, hire_date FROM employees WHERE name = 'Alice' ORDER BY hire_date;
优化后执行计划:
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
|---|---|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | employees | ref | idx_name_hire_date | idx_name_hire_date | 202 | const | 10 | Using index |
Extra 字段的 Using filesort 消失了。如果查询的列都在索引中(覆盖索引),Extra 会显示 Using index,性能最佳。
常见追问与延伸题目
-
追问:如果数据量增长 100 倍,这条 SQL 性能怎样?怎么优化?
- 回答要点 1 (有 filesort):如果存在
filesort,性能会急剧恶化。数据量增长可能导致内存排序变为磁盘排序,查询耗时可能从毫秒级增长到分钟级甚至更长,并且会大量消耗磁盘 I/O,影响整个数据库的性能。 - 回答要点 2 (无 filesort):如果通过索引避免了
filesort,性能下降会平缓得多。查询性能主要取决于索引扫描的行数。即使数据量增长 100 倍,只要WHERE条件有高选择性(如name = 'Alice'结果集很小),查询依然很快。性能瓶颈会从 CPU/IO(排序)转移到索引的 B+ 树深度和磁盘读取上。优化方向是确保WHERE条件能高效过滤,并尽可能使用覆盖索引来避免回表。
- 回答要点 1 (有 filesort):如果存在
-
追问:
ORDER BY RAND()为什么性能极差,如何优化获取随机记录的需求?- 回答要点 1 (原因):
ORDER BY RAND()会为表中的每一行计算一个随机值,然后对整个表基于这个随机值进行filesort。这是一个计算密集且 I/O 密集的操作,数据量稍大就会导致数据库崩溃。 - 回答要点 2 (优化方案):
- ID 范围法(适用于自增且连续的 ID):先获取最大和最小 ID,然后随机一个 ID 去查询。
SELECT * FROM t WHERE id >= (RAND() * (SELECT MAX(id) FROM t)) LIMIT 1;。这有数据空洞问题。 LIMIT偏移法:先COUNT(*)得到总行数N,在应用层生成一个0到N-1的随机偏移量offset,然后SELECT * FROM t LIMIT offset, 1;。缺点是LIMIT大偏移量时,MySQL 仍需扫描offset行数据,性能差。- 工程解法:如果随机推荐的场景对实时性要求不高,可以离线计算一个随机 ID 池(比如用定时任务每天生成 1000 个随机商品 ID 存入 Redis),线上直接从 Redis 中随机取一个 ID,再去数据库查询,这样性能最好。
- ID 范围法(适用于自增且连续的 ID):先获取最大和最小 ID,然后随机一个 ID 去查询。
- 回答要点 1 (原因):
-
追问:
EXPLAIN中Using temporary和Using filesort有什么区别和联系?- 回答要点 1 (区别):
Using temporary:表示 MySQL 需要创建一个内部临时表来保存中间结果。常见于GROUP BY和ORDER BY的列不一致、DISTINCT和ORDER BY的列不一致、UNION等场景。临时表可能在内存(Memory引擎)或磁盘(InnoDB/MyISAM引擎)。Using filesort:表示需要对结果集进行一次独立的排序操作。
- 回答要点 2 (联系):两者可能同时出现,是性能的“重灾区”。例如
SELECT COUNT(*) FROM t GROUP BY col1 ORDER BY col2;。MySQL 首先会创建一个临时表来执行GROUP BY(存储col1和COUNT(*)的结果),然后对这个临时表进行filesort以满足ORDER BY col2。Using temporary; Using filesort意味着查询既有创建表的开销,又有排序的开销,必须优先优化。
- 回答要点 1 (区别):