Skip to content

MySQL 带有 orderby 查询可能会变慢

这个问题非常典型,涉及到MySQL查询优化的核心原理。让我详细解释原因和优化方案。

🔍 问题根源分析

1. 执行计划变化

当只有WHERE条件时,MySQL可能使用普通索引进行快速查找。但加上ORDER BY后,执行计划可能发生以下变化:

sql
-- 优化前执行计划分析
EXPLAIN SELECT * FROM users 
WHERE status = 'active' AND category_id = 10 
ORDER BY created_at DESC;

可能的执行计划变化:

  • 索引选择改变:从使用WHERE条件的索引切换到ORDER BY字段的索引
  • Using filesort出现:表示需要在内存或磁盘上进行排序
  • 回表操作增加:需要访问主键索引获取完整数据

2. 排序操作的成本来源

场景示例

sql
-- 表结构和索引
CREATE TABLE orders (
    id INT PRIMARY KEY,
    user_id INT,
    status VARCHAR(20),
    amount DECIMAL(10,2),
    created_at DATETIME,
    INDEX idx_status_user (status, user_id),      -- 复合索引
    INDEX idx_created (created_at)                -- 单字段索引
);

-- 问题查询
SELECT * FROM orders 
WHERE status = 'completed' AND user_id = 100 
ORDER BY created_at DESC 
LIMIT 20;

执行过程分析

  1. **使用索引idx_status_user**找到所有status='completed' AND user_id=100的记录
  2. 获取主键ID:假设找到5000条记录
  3. 回表操作:用5000个主键ID回到聚簇索引获取完整数据
  4. 排序操作:对5000条完整记录按created_at排序(Using filesort
  5. 返回结果:取前20条

性能瓶颈:步骤3和4需要处理大量数据!

🚀 优化解决方案

方案1:创建覆盖索引 + 排序索引

最优方案 - 创建包含所有字段的复合索引

sql
-- 创建包含查询字段和排序字段的复合索引
CREATE INDEX idx_status_user_created_covering 
ON orders(status, user_id, created_at DESC, id);

-- 或者如果还有其他查询字段,可以包含进去
CREATE INDEX idx_status_user_created_covering_all 
ON orders(status, user_id, created_at DESC, amount, ...);

优化原理

  • (status, user_id):满足WHERE条件
  • created_at DESC:满足ORDER BY排序
  • id:避免回表(如果SELECT只查这些字段)
  • 整个查询在索引中完成,无需回表,无需filesort

方案2:延迟关联(Deferred Join)

当无法创建覆盖所有字段的索引时使用:

sql
-- 优化后的查询
SELECT o.* 
FROM orders o
INNER JOIN (
    SELECT id
    FROM orders 
    WHERE status = 'completed' AND user_id = 100
    ORDER BY created_at DESC
    LIMIT 20
) AS tmp ON o.id = tmp.id
ORDER BY o.created_at DESC;

优化原理

  1. 子查询使用小型索引快速定位主键ID
  2. 只对少量主键ID进行回表操作
  3. 大幅减少排序和数据访问量

方案3:调整索引顺序策略

索引设计原则

sql
-- ❌ 不好的索引设计
CREATE INDEX idx_created_status ON orders(created_at, status);

-- ✅ 好的索引设计(针对等值查询+排序)
CREATE INDEX idx_status_created ON orders(status, created_at DESC);

-- ✅ 更好的设计(包含更多查询字段)
CREATE INDEX idx_status_user_created 
ON orders(status, user_id, created_at DESC);

索引选择策略

  • 等值查询字段放在最左边(status, user_id
  • 范围查询/排序字段放在后面(created_at
  • 遵循最左前缀原则

方案4:使用强制索引提示

当MySQL优化器选择错误的索引时:

sql
-- 强制使用最优索引
SELECT * FROM orders 
FORCE INDEX (idx_status_user_created)
WHERE status = 'completed' AND user_id = 100 
ORDER BY created_at DESC 
LIMIT 20;

📊 实战案例分析

案例1:电商订单查询优化

原始查询

sql
SELECT order_id, user_id, total_amount, created_time
FROM orders 
WHERE user_id = 12345 AND status = 'paid'
ORDER BY created_time DESC 
LIMIT 10;

优化方案

sql
-- 创建复合索引
CREATE INDEX idx_user_status_created 
ON orders(user_id, status, created_time DESC);

-- 优化后查询(使用覆盖索引)
SELECT order_id, user_id, total_amount, created_time
FROM orders 
WHERE user_id = 12345 AND status = 'paid'
ORDER BY created_time DESC 
LIMIT 10;

案例2:分页查询优化

原始查询(深度分页问题):

sql
SELECT * FROM products 
WHERE category_id = 5 AND price > 100
ORDER BY sales_count DESC 
LIMIT 10000, 20;

优化方案

sql
-- 使用延迟关联优化深度分页
SELECT p.*
FROM products p
INNER JOIN (
    SELECT product_id
    FROM products
    WHERE category_id = 5 AND price > 100
    ORDER BY sales_count DESC
    LIMIT 10000, 20
) AS tmp ON p.product_id = tmp.product_id
ORDER BY p.sales_count DESC;

🛠️ 诊断工具和技巧

1. 使用EXPLAIN分析

sql
EXPLAIN FORMAT=JSON 
SELECT * FROM orders 
WHERE status = 'completed' 
ORDER BY created_at DESC;

-- 关注关键字段:
-- • type: 访问类型(index/range/ref最好)
-- • key: 使用的索引
-- • rows: 预估扫描行数
-- • Extra: 
--   - Using index: 覆盖索引,最佳
--   - Using filesort: 需要排序,需优化
--   - Using temporary: 使用临时表,需优化

2. 索引使用检查

sql
-- 检查索引选择性
SELECT 
    COUNT(DISTINCT status) / COUNT(*) as status_selectivity,
    COUNT(DISTINCT created_at) / COUNT(*) as created_at_selectivity
FROM orders;

💡 最佳实践总结

  1. 索引设计优先级

    • 等值查询字段 → 排序字段 → 其他查询字段
    • 高选择性字段放在前面
  2. 避免filesort的策略

    • 创建包含ORDER BY字段的复合索引
    • 确保索引排序方向与ORDER BY一致
    • 使用覆盖索引避免回表
  3. 查询编写技巧

    • 避免SELECT *,只查询需要的字段
    • 对深度分页使用延迟关联
    • 合理使用FORCE INDEX提示
  4. 监控和调优

    • 定期使用EXPLAIN分析慢查询
    • 监控filesort和temporary表的使用
    • 使用Percona Toolkit等工具进行索引分析

通过合理的索引设计和查询优化,可以彻底解决"WHERE快但ORDER BY慢"的问题,让排序操作在索引层面就完成,避免昂贵的filesort操作。

chatgpt给的大致的order by执行流程,详细看这里MySQL orderby底层执行讲解

ORDER BY 是 MySQL 中执行代价较高、但又非常常见的操作之一。它的底层执行流程取决于多种因素,比如是否能利用索引、内存大小、排序字段类型等。 下面我详细讲一下 MySQL ORDER BY底层执行流程、优化机制和常见性能问题

🧠 一、执行流程概览

当执行一个带 ORDER BY 的 SQL 时,例如:

sql
SELECT * FROM user ORDER BY age DESC;

MySQL 的执行流程大致如下:

  1. 解析 SQL → 生成执行计划

    • 优化器分析是否可以利用索引排序。
    • 决定是否需要额外的排序操作(filesort)。
  2. 判断是否能用索引排序

    • 如果排序字段与索引顺序一致,并且所有查询字段都能从该索引中取出(即“覆盖索引”),则可直接返回有序结果。
    • 否则,进入 filesort 排序阶段。
  3. 执行排序(filesort)

    • 将需要排序的数据(或部分列)放入内存(sort buffer)。
    • 如果数据太大放不下,则会写入临时文件并进行外部排序(多路归并)。
  4. 返回结果

    • 按排序结果的顺序输出。

⚙️ 二、两种排序模式(filesort算法)

MySQL 的 filesort 并不是字面意义上的“文件排序”,而是 MySQL 内部的一种排序算法。主要有两种模式:

1️⃣ 单路排序(Single-Pass)

又称 “改进后的排序”(MySQL 4.1+) 流程:

  • 从表中读出所有要返回的列(SELECT 中的字段 + ORDER BY 字段)。
  • 直接在内存中排序。
  • 返回结果。

📌 优点: 少一次回表操作(一次性读出数据)。

📌 缺点: 占用更多内存,因为每行数据都要加载到 sort buffer。

适用条件: sort_buffer_size 足够大。

2️⃣ 双路排序(Two-Pass)

流程:

  • 第一步:只读出排序字段 + 主键(或行指针)放入 sort buffer。
  • 排序后,再根据主键(或指针)回表取完整数据。

📌 优点: 占用内存较少。

📌 缺点: 需要两次读表(一次读排序键,一次回表)。

MySQL 会根据 排序字段总大小sort_buffer_size 比较,自动选择单路或双路。

🧩 三、能否利用索引排序的条件

可以完全避免 filesort 的情况(也就是索引顺序输出):

  1. ORDER BY 的字段顺序与索引顺序完全一致。

    sql
    CREATE INDEX idx_user_age_name ON user(age, name);
    
    SELECT * FROM user ORDER BY age, name;  ✅ 走索引排序
  2. 排序方向一致。

    • 不能一个 ASC 一个 DESC,否则索引无法利用。
    sql
    ORDER BY age ASC, name DESC; ❌ 无法利用索引顺序
  3. WHERE 条件与索引前缀匹配。

    sql
    WHERE age > 20 ORDER BY name; ❌ 若 age 不是索引的首列,则不行
  4. 不包含混合表的排序(例如 join 后 order by 非驱动表列)。

🧮 四、临时表 + 外部排序过程(filesort 细节)

当数据量太大,sort_buffer_size 放不下时,MySQL 会:

  1. 将部分排序数据写入磁盘临时文件;
  2. 对每个块分别排序;
  3. 使用 归并排序 合并结果(多路归并)。

参数:

  • sort_buffer_size:单线程排序缓冲区大小。
  • max_sort_length:字符串字段用于排序的最大字节数。
  • read_rnd_buffer_size:filesort 读取排序结果时的缓冲区大小。

⚠️ 注意:每个排序线程都会分配一个独立的 sort buffer,多个并发排序可能导致内存暴涨。

🧾 五、Explain 中怎么看是否使用了 filesort?

使用:

sql
EXPLAIN SELECT * FROM user ORDER BY age;

Extra 列:

Extra 信息含义
Using filesort发生了排序,未利用索引
Using index利用了索引覆盖
Using temporary有临时表参与(例如 group by + order by)

🚀 六、优化方向总结

问题优化建议
Using filesort调整索引顺序以支持 ORDER BY
排序慢增大 sort_buffer_size 或分页(limit)
内存占用高降低 sort_buffer_size、分页优化
join + order by 慢先排序子查询再 join 或使用临时表
大字段排序慢只选取必要字段,减少传入 sort buffer 的数据

🧩 七、一个综合示例

假设有表:

sql
CREATE TABLE user (
  id BIGINT PRIMARY KEY,
  city VARCHAR(20),
  age INT,
  name VARCHAR(20),
  INDEX idx_city_age (city, age)
);

执行:

sql
SELECT * FROM user WHERE city='Beijing' ORDER BY age LIMIT 10;

执行过程:

  1. WHERE city='Beijing' → 走索引前缀 city
  2. ORDER BY age → 与索引顺序一致
  3. ✅ 直接索引顺序扫描,无 filesort
  4. 取前 10 条后停止扫描 → “index range scan + early termination”

学而不思则罔,思而不学则殆。