MySQL慢查询优化

核心概念

慢查询优化是通过定位慢SQL → 分析执行计划 → 制定优化方案 → 验证效果的闭环流程,系统性提升数据库查询性能。优化核心是索引优化、SQL改写、表设计调整三大手段。


一、慢查询优化流程

完整优化流程图

┌──────────────┐
│ 1. 开启慢查询 │
│    日志监控   │
└───────┬──────┘
        │
┌───────▼──────┐
│ 2. 分析慢SQL  │
│   - 日志分析  │
│   - EXPLAIN   │
└───────┬──────┘
        │
┌───────▼──────┐
│ 3. 制定方案  │
│   - 加索引    │
│   - 改SQL     │
│   - 调表结构  │
└───────┬──────┘
        │
┌───────▼──────┐
│ 4. 测试验证  │
│   - 开发环境  │
│   - 压力测试  │
└───────┬──────┘
        │
┌───────▼──────┐
│ 5. 上线观察  │
│   - 灰度发布  │
│   - 监控指标  │
└───────┬──────┘
        │
┌───────▼──────┐
│ 6. 持续监控  │
└──────────────┘

二、常见慢查询场景及优化

场景1:未建索引导致全表扫描

问题SQL

SELECT * FROM orders 
WHERE user_id = 1001 
ORDER BY create_time DESC 
LIMIT 10;

EXPLAIN分析

type: ALL
key: NULL
rows: 5000000
Extra: Using where; Using filesort
执行时间:8.5秒

问题诊断

  • ❌ 全表扫描500万行
  • ❌ 未使用索引
  • ❌ 需要额外排序

优化方案

-- 创建联合索引
CREATE INDEX idx_user_time ON orders(user_id, create_time);

-- 优化后EXPLAIN
type: ref
key: idx_user_time
rows: 120
Extra: Using index condition
执行时间:0.02

优化效果:425倍提升!


场景2:索引失效(函数操作)

问题SQL

SELECT * FROM orders 
WHERE DATE(create_time) = '2025-11-02';

EXPLAIN分析

type: ALL
key: NULL
rows: 5000000
Extra: Using where
执行时间:6.8秒

问题诊断

  • DATE()函数导致索引失效
  • ❌ 即使create_time有索引也无法使用

优化方案

-- ✅ 改写为范围查询
SELECT * FROM orders 
WHERE create_time >= '2025-11-02 00:00:00' 
  AND create_time < '2025-11-03 00:00:00';

-- 优化后EXPLAIN
type: range
key: idx_create_time
rows: 15000
Extra: Using index condition
执行时间:0.08

优化效果:85倍提升!


场景3:深度分页

问题SQL

SELECT * FROM orders 
ORDER BY id 
LIMIT 1000000, 20;

EXPLAIN分析

type: index
key: PRIMARY
rows: 1000020
Extra: Using index
执行时间:12秒

问题诊断

  • ❌ 需要扫描100万行
  • ❌ 每行都要回表
  • ❌ 前100万行全部丢弃

优化方案1:主键定位(推荐)

-- 记录上一页最大ID
SELECT * FROM orders 
WHERE id > 1000000 
ORDER BY id 
LIMIT 20;

-- 优化后EXPLAIN
type: range
key: PRIMARY
rows: 20
执行时间:0.001

优化效果:12000倍提升!


优化方案2:延迟关联

SELECT o.* FROM orders o
INNER JOIN (
    SELECT id FROM orders ORDER BY id LIMIT 1000000, 20
) t ON o.id = t.id;

-- 执行时间:2.5秒

优化效果:4.8倍提升


场景4:JOIN未使用索引

问题SQL

SELECT o.*, u.name FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE o.status = 1;

EXPLAIN分析

-- orders表
type: ALL
key: NULL
rows: 5000000

-- users表
type: eq_ref
key: PRIMARY
rows: 1

执行时间:15秒

问题诊断

  • ❌ orders表全表扫描
  • ❌ status字段未建索引

优化方案

-- 1. 为status字段建索引
CREATE INDEX idx_status ON orders(status);

-- 2. 确保关联字段有索引(user_id和id)
CREATE INDEX idx_user_id ON orders(user_id);  -- 如果没有的话

-- 优化后EXPLAIN
-- orders表
type: ref
key: idx_status
rows: 80000

-- users表
type: eq_ref
key: PRIMARY
rows: 1

执行时间:0.5

优化效果:30倍提升!


场景5:回表开销大

问题SQL

SELECT * FROM orders 
WHERE user_id = 1001;

EXPLAIN分析

type: ref
key: idx_user_id
rows: 50000
Extra: Using where
执行时间:2.8秒

问题诊断

  • ✅ 使用了索引
  • ❌ 需要回表5万次
  • ❌ 随机IO开销大

优化方案1:覆盖索引

-- 只查询必要字段
SELECT id, user_id, amount, create_time FROM orders 
WHERE user_id = 1001;

-- 创建覆盖索引
CREATE INDEX idx_user_cover ON orders(user_id, amount, create_time, id);

-- 优化后EXPLAIN
type: ref
key: idx_user_cover
rows: 50000
Extra: Using index   无需回表
执行时间:0.3

优化效果:9倍提升!


优化方案2:延迟关联

SELECT o.* FROM orders o
INNER JOIN (
    SELECT id FROM orders WHERE user_id = 1001 LIMIT 20
) t ON o.id = t.id;

-- 只回表20次
执行时间:0.05

优化效果:56倍提升!


场景6:LIKE模糊查询

问题SQL

SELECT * FROM products 
WHERE name LIKE '%iPhone%';

EXPLAIN分析

type: ALL
key: NULL
rows: 1000000
Extra: Using where
执行时间:8.2秒

问题诊断

  • ❌ 前缀通配符导致索引失效
  • ❌ 全表扫描

优化方案1:全文索引

-- 创建全文索引
CREATE FULLTEXT INDEX ft_name ON products(name) WITH PARSER ngram;

-- 使用全文搜索
SELECT * FROM products 
WHERE MATCH(name) AGAINST('iPhone' IN NATURAL LANGUAGE MODE);

-- 执行时间:0.15秒

优化效果:55倍提升!


优化方案2:Elasticsearch

// 引入Elasticsearch
@Service
public class ProductSearchService {
    
    @Autowired
    private ElasticsearchClient esClient;
    
    public List<Product> search(String keyword) {
        SearchRequest request = SearchRequest.of(s -> s
            .index("products")
            .query(q -> q.match(m -> m
                .field("name")
                .query(keyword)
            ))
        );
        
        return esClient.search(request, Product.class)
            .hits().hits().stream()
            .map(Hit::source)
            .collect(Collectors.toList());
    }
}

// 执行时间:0.01秒

优化效果:820倍提升!


场景7:ORDER BY未用索引

问题SQL

SELECT * FROM orders 
WHERE user_id = 1001 
ORDER BY amount DESC 
LIMIT 10;

EXPLAIN分析

type: ref
key: idx_user_id
rows: 50000
Extra: Using where; Using filesort
执行时间:1.5秒

问题诊断

  • ✅ WHERE使用了索引
  • ❌ ORDER BY未使用索引
  • ❌ 需要额外排序5万行

优化方案

-- 创建联合索引(WHERE + ORDER BY)
CREATE INDEX idx_user_amount ON orders(user_id, amount);

-- 优化后EXPLAIN
type: ref
key: idx_user_amount
rows: 50000
Extra: Using index condition
执行时间:0.05

优化效果:30倍提升!


场景8:GROUP BY产生临时表

问题SQL

SELECT user_id, COUNT(*) as cnt 
FROM orders 
GROUP BY user_id 
ORDER BY cnt DESC 
LIMIT 10;

EXPLAIN分析

type: ALL
key: NULL
rows: 5000000
Extra: Using temporary; Using filesort
执行时间:25秒

问题诊断

  • ❌ 全表扫描
  • ❌ 使用临时表
  • ❌ 需要排序

优化方案

-- 1. 为GROUP BY字段建索引
CREATE INDEX idx_user_id ON orders(user_id);

-- 2. 如果可能,业务层缓存热门用户
-- 或定期统计,存入汇总表

-- 优化后EXPLAIN
type: index
key: idx_user_id
rows: 5000000
Extra: Using index; Using temporary; Using filesort
执行时间:8

优化效果:3倍提升


更优方案:汇总表

-- 创建汇总表
CREATE TABLE user_order_summary (
    user_id INT PRIMARY KEY,
    order_count INT,
    update_time DATETIME,
    INDEX idx_count(order_count)
);

-- 定期更新(每小时)
INSERT INTO user_order_summary (user_id, order_count, update_time)
SELECT user_id, COUNT(*), NOW() FROM orders 
GROUP BY user_id
ON DUPLICATE KEY UPDATE 
    order_count = VALUES(order_count),
    update_time = VALUES(update_time);

-- 查询汇总表
SELECT user_id, order_count FROM user_order_summary 
ORDER BY order_count DESC 
LIMIT 10;

-- 执行时间:0.001秒

优化效果:25000倍提升!


三、优化工具

1. 慢查询日志分析

# mysqldumpslow(系统自带)
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
# -s t:按查询时间排序
# -t 10:显示前10条

# pt-query-digest(Percona工具)
pt-query-digest /var/log/mysql/slow.log > report.txt

2. EXPLAIN分析

-- 基础EXPLAIN
EXPLAIN SELECT * FROM orders WHERE user_id = 1001;

-- 详细分析(MySQL 8.0+)
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 1001;

-- 查看优化器选择过程
SET optimizer_trace="enabled=on";
SELECT * FROM orders WHERE user_id = 1001;
SELECT * FROM information_schema.OPTIMIZER_TRACE\G
SET optimizer_trace="enabled=off";

3. 性能剖析

-- 开启性能剖析
SET profiling = 1;

-- 执行查询
SELECT * FROM orders WHERE user_id = 1001;

-- 查看性能分析
SHOW PROFILES;
SHOW PROFILE FOR QUERY 1;

-- 关闭
SET profiling = 0;

四、优化最佳实践

1. 索引设计原则

-- ✅ 好的索引设计
CREATE INDEX idx_combined ON orders(
    user_id,      -- 等值查询(选择性高)
    status,       -- 等值查询
    create_time   -- 排序/范围查询
);

-- ❌ 差的索引设计
CREATE INDEX idx_time ON orders(create_time);  -- 只能用于排序
CREATE INDEX idx_status ON orders(status);      -- 区分度太低

2. SQL编写规范

-- ✅ 好的SQL
SELECT id, user_id, amount FROM orders 
WHERE user_id = 1001 AND status = 1 
ORDER BY create_time DESC 
LIMIT 20;

-- ❌ 差的SQL
SELECT * FROM orders 
WHERE YEAR(create_time) = 2025 
ORDER BY amount 
LIMIT 10000, 20;

3. 监控告警

# Prometheus告警规则
groups:
  - name: mysql_alerts
    rules:
      # 慢查询数量告警
      - alert: HighSlowQueries
        expr: rate(mysql_global_status_slow_queries[5m]) > 10
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "慢查询过多"
          
      # 连接数告警
      - alert: HighConnections
        expr: mysql_global_status_threads_connected / mysql_global_variables_max_connections > 0.8
        for: 5m
        labels:
          severity: critical

五、优化效果对比

优化手段 适用场景 难度 效果 成本
加索引 未建索引 ⭐⭐⭐⭐⭐
SQL改写 索引失效 ⭐⭐ ⭐⭐⭐⭐
覆盖索引 回表多 ⭐⭐ ⭐⭐⭐
分页优化 深度分页 ⭐⭐⭐ ⭐⭐⭐⭐⭐
读写分离 读多写少 ⭐⭐⭐⭐ ⭐⭐⭐
分库分表 海量数据 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐

六、面试答题模板

回答结构

1. 问题定位(如何发现慢查询)
   - 慢查询日志
   - 监控告警
   
2. 分析诊断(为什么慢)
   - EXPLAIN分析
   - 索引情况
   
3. 优化方案(怎么解决)
   - 加索引
   - 改SQL
   - 架构优化
   
4. 效果验证(优化效果)
   - 性能对比
   - 监控数据
   
5. 经验总结(举一反三)
   - 预防措施
   - 最佳实践

示例回答

问:如何优化慢查询?

: 我会按照以下流程优化慢查询:

1. 定位慢SQL:通过慢查询日志或监控系统发现问题SQL。

2. 分析根因:用EXPLAIN分析执行计划,重点看type、key、rows、Extra。常见问题是:

  • type=ALL(全表扫描)
  • key=NULL(未使用索引)
  • rows过大(扫描行数多)
  • Extra=Using filesort(需要排序)

3. 优化方案

  • 80%的慢查询是索引问题,优先创建合适的索引
  • SQL改写:避免函数操作、隐式转换、前缀通配符
  • 深度分页:用主键定位或延迟关联
  • 架构优化:读写分离、分库分表

4. 实战案例: 曾优化过电商订单查询,原SQL执行8秒,通过创建联合索引idx_user_time(user_id, create_time),优化后0.02秒,提升400倍。

5. 预防措施

  • 定期Review慢查询日志
  • 上线前SQL审核
  • 监控告警及时响应

总结

慢查询优化遵循“定位→分析→优化→验证”的闭环流程。80%的慢查询是索引问题,通过EXPLAIN分析执行计划,针对性创建索引或改写SQL,通常能获得数十倍甚至数百倍的性能提升。单机优化到瓶颈后,考虑读写分离、分库分表等架构方案。面试中能系统讲解优化流程,并结合实际案例说明优化效果,体现数据库优化的实战能力。