文件排序(filesort)的优化与避免

文件排序(filesort)的优化与避免

什么是 filesort

当 MySQL 无法使用索引完成排序时,就需要自己进行排序操作,这个操作称为 filesort(文件排序)。

EXPLAIN SELECT * FROM order WHERE status = 1 ORDER BY create_time DESC;
-- Extra: Using where; Using filesort  ← 存在 filesort

注意:filesort 不一定是”文件”排序——如果排序数据量较小,在内存中完成;数据量大时才会使用磁盘文件。但无论如何,它都比索引排序慢。

为什么 filesort 比索引排序慢

索引排序

当 ORDER BY 字段是索引的”最左前缀”时,MySQL 可以直接按索引顺序读取数据:

索引排序的过程:
1. 扫描索引 B+Tree
2. 按顺序从叶子节点取出主键
3. 回表获取完整行数据
4. 直接返回结果(有序)
→ 不需要额外的排序操作,没有额外的 CPU/内存/IO

Filesort 排序

当 ORDER BY 字段不能使用索引时:

Filesort 的过程:
1. 扫描表/索引,获取排序字段值 + 主键/行指针
2. 在 sort buffer 中对这些值进行排序
3. 如果 sort buffer 放不下,使用磁盘临时文件做归并排序
4. 按排序后的顺序回表获取完整行
→ 额外的 CPU 消耗(排序)+ 可能的 IO 消耗(磁盘临时文件)

哪些情况会产生 filesort

-- 1. ORDER BY 字段不是索引列
SELECT * FROM user ORDER BY register_time;

-- 2. ORDER BY 和 GROUP BY 字段不同
SELECT category, COUNT(*) FROM product GROUP BY category ORDER BY COUNT(*);

-- 3. 跨表排序
SELECT u.* FROM user u JOIN order o ON u.id = o.user_id
ORDER BY o.amount;

-- 4. ORDER BY 字段不满足最左前缀
-- 索引:(a, b, c)
ORDER BY b;       -- 未从最左列开始
ORDER BY a, c;    -- 跳过了 b

-- 5. 混合排序方向
-- 索引:idx(a ASC, b ASC)
ORDER BY a ASC, b DESC;  -- 方向不一致

如何查看 filesort

-- 方式 1:EXPLAIN
EXPLAIN SELECT * FROM user ORDER BY register_time LIMIT 10;
-- Extra: Using filesort

-- 方式 2:状态变量
FLUSH STATUS;
SELECT * FROM user ORDER BY register_time LIMIT 10;
SHOW STATUS LIKE 'Sort%';
-- Sort_merge_passes: 归并排序的轮次(> 0 表示使用了磁盘临时文件)
-- Sort_range: 范围扫描排序次数
-- Sort_rows: 总排序行数
-- Sort_scan: 全表扫描排序次数

优化策略

策略 1:建立合适的索引(最有效)

这是最根本的解决方案。通过建立包含排序字段的联合索引,让排序直接走索引。

-- 原始查询
SELECT * FROM order WHERE status = 1 ORDER BY create_time DESC LIMIT 10;

-- 建立联合索引
CREATE INDEX idx_status_create ON order(status, create_time);

-- 优化后
SELECT * FROM order WHERE status = 1 ORDER BY create_time DESC LIMIT 10;
-- Extra: Using where  (不再有 Using filesort)
-- type: ref(走索引)

关键:索引列的顺序必须与 WHERE + ORDER BY 的条件匹配。

策略 2:使用覆盖索引

-- 需要排序字段包含在索引中
CREATE INDEX idx_status_create_amount ON order(status, create_time, amount);

-- 只查索引包含的字段
SELECT status, create_time, amount 
FROM order 
WHERE status = 1 
ORDER BY create_time DESC;
-- Extra: Using where; Using index(完全使用索引覆盖,效率最高)

策略 3:减少排序数据量

-- 先过滤再排序,减少参与排序的行数
SELECT * FROM order 
WHERE status = 1 AND create_time > '2024-01-01'  -- 先过滤
ORDER BY create_time DESC 
LIMIT 10;

-- 不加过滤条件会导致排序大量行

策略 4:增大 sort buffer

# my.cnf
# 增大排序缓冲,减少磁盘排序
sort_buffer_size = 2M  # 默认 256K,通常 1M-8M

sort buffer 是每个连接独立分配的,设置过大可能导致内存占用过高。

策略 5:优化排序方向

-- 统一排序方向可以走索引
-- 索引:idx_create(create_time ASC)
ORDER BY create_time ASC;   -- 走索引
ORDER BY create_time DESC;  -- 走索引(倒序扫描)

-- 以下情况无法走索引
ORDER BY create_time ASC, id DESC;  -- 混合方向

Filesort 的两种模式

MySQL 在 filesort 时有两种模式,可以通过 max_length_for_sort_data 控制:

模式一:单行排序(Sort Tuple)

-- 当行数据很小时
-- sort buffer 中只存排序字段和主键
-- 排序完成后,按主键回表获取完整数据

模式二:全行排序(Sort Row)

-- 当行数据很大时
-- sort buffer 中存放排序字段和完整行数据
-- 不需要回表,但占用更多 sort buffer 空间
# 控制两种模式的切换
max_length_for_sort_data = 4096  # 默认 4096 字节
# 超过此值使用 Sort Row 模式
# 适当增大可以减少回表次数

监控和诊断

-- 检查系统的 filesort 情况
SHOW GLOBAL STATUS LIKE 'Sort%';

-- 理想的系统状态
Sort_merge_passes = 0  -- 没有磁盘排序
Sort_rows << table_rows  -- 不会对全表排序

排序优化的优先级:

索引排序 > 单行 filesort(内存)> 多行 filesort(内存)> 磁盘 filesort

面试要点

  • filesort 的本质是无法利用索引排序时的”兜底方案”
  • Extra: Using filesort 就是需要优化的信号
  • 最有效的优化是创建合适的索引(排序字段包含在索引中)
  • 不是所有 filesort 都需要优化——数据量小(< 1000 行)时内存排序可以接受
  • 区分”内存 filesort”和”磁盘 filesort”,后者才是真正的性能杀手
  • 能通过 SHOW STATUS LIKE ‘Sort%’ 中的 Sort_merge_passes 判断是否产生了磁盘排序
© 版权声明
THE END
喜欢就支持一下吧
点赞15 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容