MySQL 慢查询如何优化?

2025年 阅读约 15 分钟 面试指南 · MySQL

深入解析MySQL慢查询优化全流程:慢查询日志配置与分析、EXPLAIN执行计划解读、索引优化策略、SQL改写技巧、profiling性能分析、深分页优化、JOIN优化,附实战案例和面试模拟问答。

一句话总结

慢查询优化步骤:1)开启慢查询日志定位慢 SQL → 2)EXPLAIN 分析执行计划(type/rows/Extra)→ 3)加索引优化(覆盖索引+最左前缀)→ 4)SQL 改写(避免 SELECT *、深分页用延迟关联)→ 5)表结构优化(字段类型、范式/反范式)。核心口诀:先看 type(const > ref > range > ALL),再看 Extra(Using filesort/Using temporary 需处理)

初级理解

开启慢查询日志

# 查看慢查询配置 SHOW VARIABLES LIKE 'slow_query%'; SHOW VARIABLES LIKE 'long_query_time'; # 开启慢查询日志 SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1; # 超过 1 秒记录 SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log'; # 记录未使用索引的查询 SET GLOBAL log_queries_not_using_indexes = ON; # 永久配置(my.cnf) [mysqld] slow_query_log = 1 long_query_time = 1 slow_query_log_file = /var/log/mysql/slow.log log_queries_not_using_indexes = 1

mysqldumpslow 分析慢日志

# 常用分析命令 mysqldumpslow -s t -t 10 slow.log # 按时间排序,显示前 10 条 mysqldumpslow -s c -t 10 slow.log # 按出现次数排序 mysqldumpslow -s r -t 10 slow.log # 按扫描行数排序 mysqldumpslow -g 'LEFT JOIN' slow.log # 过滤包含 LEFT JOIN 的 SQL # 输出示例 # Count: 100 Time=2.50s (250s) Lock=0.00s Rows=50000.0 (5000000) # SELECT * FROM orders WHERE status='N' ORDER BY create_time DESC; # 解读:执行100次,平均2.5秒,平均扫描5万行
一句话总结:先开慢查询日志找到问题 SQL,再用 EXPLAIN 分析为什么慢。

中级深入

EXPLAIN 详解

EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND status = 'paid'; # 输出解读(按重要性排序): # id: 查询序号(复杂查询可能有多个) # select_type: SIMPLE(简单)/ PRIMARY(主查询)/ SUBQUERY(子查询) # type: 访问类型(最重要!) # system > const > eq_ref > ref > range > index > ALL # const: 主键等值,1行(最优) # eq_ref: 联表时唯一索引匹配,1行 # ref: 非唯一索引等值匹配 # range: 索引范围扫描(>,<,BETWEEN,IN) # index: 全索引扫描(比ALL好一点) # ALL: 全表扫描(最差,必须优化) # possible_keys: 可能使用的索引 # key: 实际使用的索引 # key_len: 使用的索引长度 # ref: 与索引比较的列 # rows: 预估扫描行数 # filtered: 过滤后占比 # Extra: 额外信息(重点看!) # Using index: 覆盖索引 ✅ # Using index condition: ICP 生效 ✅ # Using where: Server层过滤 # Using filesort: 文件排序 ❌(需优化) # Using temporary: 临时表 ❌(需优化)

常见 SQL 优化技巧

# 1. SELECT * 改 SELECT 具体列 # ❌ SELECT * FROM users WHERE id = 1; # ✅ SELECT id, name, email FROM users WHERE id = 1; # 好处:减少网络传输 + 可能走覆盖索引 # 2. 分页优化(深分页) # ❌ SELECT * FROM orders LIMIT 100000, 20; # 扫描10万+20行 # ✅ 延迟关联 SELECT o.* FROM orders o INNER JOIN ( SELECT id FROM orders ORDER BY id LIMIT 100000, 20 ) tmp ON o.id = tmp.id; # ✅ 游标分页 SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 20; # 3. IN 代替 OR # ❌ SELECT * FROM users WHERE id = 1 OR id = 2 OR id = 3; # ✅ SELECT * FROM users WHERE id IN (1, 2, 3); # 4. UNION ALL 代替 UNION(不去重) # ❌ SELECT name FROM users UNION SELECT name FROM admins; # ✅ SELECT name FROM users UNION ALL SELECT name FROM admins; # 5. 避免在 WHERE 子句中对字段进行函数操作 # ❌ SELECT * FROM users WHERE YEAR(create_time) = 2025; # ✅ SELECT * FROM users WHERE create_time >= '2025-01-01' AND create_time < '2026-01-01'; # 6. JOIN 代替子查询 # ❌ SELECT * FROM orders WHERE user_id IN (SELECT id FROM users WHERE status=1); # ✅ SELECT o.* FROM orders o INNER JOIN users u ON o.user_id=u.id WHERE u.status=1;
中级要点:EXPLAIN 先看 type(至少 range 以上),再看 Extra(消除 Using filesort/temporary),最后优化 rows。

高级拓展

JOIN 优化 — 小表驱动大表

# NLJ(Nested Loop Join):驱动表每行去被驱动表匹配 # 原则:小表做驱动表,大表走索引 # 查看 JOIN 顺序 EXPLAIN SELECT * FROM users u JOIN orders o ON u.id = o.user_id WHERE u.status = 1; # id 相同的行,从上到下执行 # 第一行是驱动表,第二行是被驱动表 # 被驱动表的关联字段必须有索引! # BKA(Batched Key Access):批量读取,减少 IO # MySQL 8.0 默认开启 SET optimizer_switch='batched_key_access=on';

profiling 分析查询耗时

# 开启 profiling SET profiling = 1; # 执行查询 SELECT * FROM orders WHERE user_id = 100; # 查看所有查询的耗时 SHOW PROFILES; # 查看具体查询的详细耗时 SHOW PROFILE FOR QUERY 1; # 输出示例: # Status Duration # starting 0.000042 # checking permissions 0.000005 # Opening tables 0.000012 # init 0.000015 # System lock 0.000006 # optimizing 0.000008 # statistics 0.000012 # preparing 0.000010 # executing 0.000005 # Sending data 2.500000 ← 瓶颈在这里! # end 0.000005 # query end 0.000003

optimizer_trace — 查看优化器决策

# 开启优化器跟踪 SET optimizer_trace='enabled=on'; # 执行查询 SELECT * FROM orders WHERE user_id = 100 AND status = 'paid'; # 查看优化器决策过程 SELECT * FROM information_schema.optimizer_trace\G # 关键信息: # "rows_estimation": [...] # 索引选择评估 # "considered_execution_plans": [...] # 执行计划对比 # "attached_conditions_summary": [...] # 条件附加 SET optimizer_trace='enabled=off';

实战场景

场景:优化一个 5 秒慢查询

# 原始 SQL(5.2秒) SELECT * FROM orders WHERE status = 'paid' AND create_time >= '2025-01-01' ORDER BY create_time DESC LIMIT 20; # Step 1: EXPLAIN 分析 EXPLAIN SELECT * FROM orders WHERE status = 'paid' AND create_time >= '2025-01-01' ORDER BY create_time DESC LIMIT 20; # type: ALL, rows: 5000000, Extra: Using where; Using filesort # Step 2: 问题诊断 # - type=ALL 全表扫描,500万行 # - Using filesort 文件排序,额外开销 # Step 3: 创建联合索引 ALTER TABLE orders ADD INDEX idx_status_time(status, create_time); # Step 4: 验证效果 EXPLAIN SELECT * FROM orders WHERE status = 'paid' AND create_time >= '2025-01-01' ORDER BY create_time DESC LIMIT 20; # type: range, rows: 50000, Extra: Using index condition # 查询时间:5.2s → 0.03s # Step 5: 如果仍慢,考虑覆盖索引 # 只查需要的列 SELECT id, user_id, amount, create_time FROM orders WHERE status = 'paid' AND create_time >= '2025-01-01' ORDER BY create_time DESC LIMIT 20;

面试模拟

面试官:线上一个查询突然变慢,你怎么排查?

你:1)先看慢查询日志,确认是哪个 SQL 变慢;2)EXPLAIN 看执行计划,对比之前的执行计划是否变化(索引是否失效、是否走了不同的索引);3)检查数据量是否暴增、统计信息是否过期(ANALYZE TABLE 更新);4)检查是否有锁等待(SHOW PROCESSLIST 看锁状态);5)检查服务器资源(CPU/IO/内存)。

面试官:深分页为什么慢?怎么优化?

你:LIMIT 100000,20 需要扫描前 100020 行再丢弃前 100000 行,数据量越大越慢。优化方案:1)延迟关联——子查询只取 ID 走覆盖索引,外层回表只查 20 行;2)游标分页——记录上一页最后一条的 ID,用 WHERE id > last_id LIMIT 20。

面试官:EXPLAIN 中 type 有哪些?从好到差排序。

你:system > const > eq_ref > ref > range > index > ALL。const 是主键等值查询最优;eq_ref 是联表唯一索引;ref 是非唯一索引;range 是索引范围;index 是全索引扫描;ALL 是全表扫描最差。至少要优化到 range 级别。