SQL 慢查询调优实战:从 23 秒到 0.03 秒的完整解剖过程

问题场景

运营同事投诉:"订单日报表加载要等半分钟,能不能优化一下?"

原始 SQL:

SELECT 
    DATE(o.created_at) as date,
    u.region,
    COUNT(DISTINCT o.id) as order_count,
    SUM(o.total_amount) as revenue,
    AVG(o.total_amount) as avg_order_value
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
LEFT JOIN order_items oi ON o.id = oi.order_id
WHERE o.created_at BETWEEN '2026-06-01' AND '2026-06-30'
  AND o.status IN ('paid', 'shipped', 'completed')
GROUP BY DATE(o.created_at), u.region
ORDER BY date, revenue DESC;

数据量:orders 表 500 万行,users 表 50 万行,order_items 表 2000 万行。

Step 1:EXPLAIN 分析(找出瓶颈)

EXPLAIN SELECT ...;

结果:
+----+-------------+-------+------+---------------+------+---------+------+---------+----------+
| id | select_type | table | type | possible_keys | key   | key_len | ref  | rows    | Extra          |
+----+-------------+-------+------+---------------+------+---------+------+---------+----------+
|  1 | SIMPLE      | o     | ALL  | NULL          | NULL  | NULL    | NULL | 5000000 | Using where; Using temporary; Using filesort |
|  1 | SIMPLE      | u     | ALL  | PRIMARY       | NULL  | NULL    | NULL | 500000  | Using where; Using join buffer |
|  1 | SIMPLE      | oi    | ALL  | NULL          | NULL  | NULL    | NULL | 20000000| Using where; Using join buffer |
+----+-------------+-------+------+---------------+------+---------+------+---------+----------+

全是 ALL(全表扫描)。问题找到了。

Step 2:建索引(对症下药)

-- orders 表:覆盖 WHERE + GROUP BY
CREATE INDEX idx_orders_status_date ON orders(status, created_at);

-- order_items 表:加速 JOIN
CREATE INDEX idx_order_items_order ON order_items(order_id);

-- users 表(已有主键索引,但 JOIN 字段需要确认)
-- user_id 已经是主键,OK

优化后 EXPLAIN:orders 表用上了 idx_orders_status_date,rows 从 500 万降到 8 万。但还有 Using temporary; Using filesort。

Step 3:消灭临时表和文件排序

-- 问题在 GROUP BY DATE(o.created_at), u.region
-- DATE() 函数破坏了索引的有序性

-- 方案:把日期提取到查询条件中
SELECT 
    o.order_date,  -- 直接用一个 DATE 类型字段(不调用函数)
    COALESCE(u.region, 'Unknown') as region,
    COUNT(DISTINCT o.id) as order_count,
    SUM(o.total_amount) as revenue
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE o.order_date BETWEEN '2026-06-01' AND '2026-06-30'
  AND o.status IN ('paid', 'shipped', 'completed')
GROUP BY o.order_date, u.region
ORDER BY o.order_date;

Step 4:移除不必要的 JOIN

-- order_items 的 JOIN 完全没用!因为是用 COUNT(DISTINCT o.id) 而不是 COUNT(*)
-- 而且 AVG(o.total_amount) 也不依赖 order_items

-- 直接删掉:
-- LEFT JOIN order_items oi ON o.id = oi.order_id  ← 删掉这行

Step 5:覆盖索引(终极优化)

-- 把查询需要的字段全放进索引,避免回表
CREATE INDEX idx_orders_covering ON orders(
    status, order_date, user_id, total_amount
);
-- MySQL 可以直接从索引中获取所有需要的数据,不需要读数据页!

最终 SQL 与性能对比

SELECT 
    o.order_date,
    COALESCE(u.region, 'Unknown') as region,
    COUNT(DISTINCT o.id) as order_count,
    SUM(o.total_amount) as revenue
FROM orders o USE INDEX(idx_orders_covering)
LEFT JOIN users u ON o.user_id = u.id  
WHERE o.order_date BETWEEN '2026-06-01' AND '2026-06-30'
  AND o.status IN ('paid', 'shipped', 'completed')
GROUP BY o.order_date, u.region
ORDER BY o.order_date;
指标优化前优化后
执行时间23.6 秒0.03 秒
扫描行数25,500,0001,200
ExtraUsing where; Using temporary; Using filesort; Using join bufferUsing index

慢查询优化的四步法

  1. EXPLAIN 分析:看 type(range > ref > ALL)、rows、Extra
  2. 建索引:覆盖 WHERE、JOIN、ORDER BY、GROUP BY 的字段
  3. 改写 SQL:消除不必要的 JOIN,避免在 WHERE 中调用函数
  4. 覆盖索引:让 MySQL 从索引中直接拿数据,不需要回表

记住:不是所有的慢查询都需要在应用代码层面解决,90% 的情况是 SQL 写法和索引的问题。