flowchart TD
A[客户端发送SQL] --> B[连接器<br/>权限验证]
B --> C{查询缓存<br/>MySQL 8.0已移除}
C -->|命中| D[直接返回结果]
C -->|未命中| E[解析器<br/>词法分析 + 语法分析]
E --> F[预处理器<br/>语义检查 + 权限验证]
F --> G[优化器<br/>生成执行计划]
G --> H[执行器<br/>调用存储引擎接口]
H --> I[存储引擎<br/>InnoDB]
I --> J[返回结果]
优化器的工作
MySQL优化器负责选择最优的执行计划,其决策包括:
选择使用哪个索引(或不使用索引)
多表JOIN时的连接顺序
子查询的执行策略(物化、转换为JOIN等)
是否使用临时表和排序
1 2 3 4 5
-- 查看优化器的选择过程(MySQL 8.0+) SET optimizer_trace ='enabled=on'; SELECT*FROM orders WHERE user_id =1AND status ='paid'; SELECT*FROM information_schema.optimizer_trace\G SET optimizer_trace ='enabled=off';
EXPLAIN 详细解读
EXPLAIN 各列完整说明
1 2 3 4 5 6
EXPLAIN SELECT o.*, u.name FROM orders o JOIN users u ON o.user_id = u.id WHERE o.status ='paid'AND o.created_at >'2025-01-01' ORDERBY o.created_at DESC LIMIT 20;
id 列
id表示查询中SELECT的序号,用来标识查询的执行顺序。
1 2 3 4 5 6 7
-- id相同: 从上到下执行 EXPLAIN SELECT*FROM orders o JOIN users u ON o.user_id = u.id; -- 两行id都是1,先执行上面的表(驱动表)
-- id不同: 数字大的先执行 EXPLAIN SELECT*FROM orders WHERE user_id IN (SELECT id FROM users WHERE vip_level =3); -- 子查询id=2先执行,外层id=1后执行
select_type 列
值
含义
SIMPLE
简单查询(无子查询或UNION)
PRIMARY
最外层查询
SUBQUERY
SELECT子句中的子查询
DERIVED
FROM子句中的子查询(派生表)
UNION
UNION中的第二个及后续查询
MATERIALIZED
被物化的子查询
type 列(访问类型)
这是EXPLAIN中最重要的字段,从优到劣排列:
graph LR
A[system] --> B[const] --> C[eq_ref] --> D[ref] --> E[range] --> F[index] --> G[ALL]
style A fill:#4caf50,color:#fff
style B fill:#4caf50,color:#fff
style C fill:#8bc34a,color:#fff
style D fill:#8bc34a,color:#fff
style E fill:#ffeb3b,color:#333
style F fill:#ff9800,color:#fff
style G fill:#f44336,color:#fff
-- 强制使用某个索引 SELECT*FROM orders FORCE INDEX (idx_user_status) WHERE user_id =1AND status ='paid';
-- 忽略某个索引 SELECT*FROM orders IGNORE INDEX (idx_status) WHERE status ='paid';
-- MySQL 8.0 Optimizer Hints(更细粒度) SELECT/*+ INDEX(o idx_user_status) */ o.* FROM orders o WHERE o.user_id =1AND o.status ='paid';
-- 控制JOIN顺序 SELECT/*+ JOIN_ORDER(u, o) */ o.*, u.name FROM orders o JOIN users u ON o.user_id = u.id;
-- 禁用合并派生表 SELECT/*+ NO_MERGE(derived) */* FROM (SELECT user_id, COUNT(*) AS cnt FROM orders GROUPBY user_id) derived WHERE cnt >10;
-- 设置查询超时(毫秒) SELECT/*+ MAX_EXECUTION_TIME(5000) */*FROM orders WHERE status ='paid';
SQL 重写优化技巧
JOIN 优化
flowchart TD
A[JOIN优化] --> B[小表驱动大表]
A --> C[被驱动表JOIN列建索引]
A --> D[避免过多表JOIN]
A --> E[选择合适的JOIN类型]
B --> B1["驱动表: 过滤后结果集小的表"]
C --> C1["确保 ON 条件列有索引"]
D --> D1["单次查询不超过3-4张表"]
E --> E1["INNER JOIN 让优化器选择驱动表<br/>LEFT JOIN 左表为驱动表"]
1 2 3 4 5 6 7 8 9 10 11 12 13 14
-- 不良写法: 大表驱动小表 SELECT o.*FROM orders o LEFTJOIN (SELECT id FROM users WHERE vip_level =3) u ON o.user_id = u.id WHERE u.id ISNOTNULL;
-- 优化写法: 小表驱动大表 SELECT o.*FROM orders o INNERJOIN users u ON o.user_id = u.id WHERE u.vip_level =3; -- 优化器会自动选择 users(小结果集)作为驱动表
-- 进一步优化: 如果只需要检查存在性 SELECT o.*FROM orders o WHEREEXISTS (SELECT1FROM users u WHERE u.id = o.user_id AND u.vip_level =3);
Nested Loop Join 原理
MySQL使用Nested Loop Join算法执行JOIN:
1 2 3 4 5
for (row in 驱动表) { for (row in 被驱动表 where 匹配条件) { 输出结果 } }
-- 低效: 相关子查询(每行都执行一次子查询) SELECT* FROM orders o WHERE o.amount > (SELECTAVG(amount) FROM orders WHERE user_id = o.user_id);
-- 优化: 转为JOIN SELECT o.* FROM orders o INNERJOIN ( SELECT user_id, AVG(amount) AS avg_amount FROM orders GROUPBY user_id ) t ON o.user_id = t.user_id AND o.amount > t.avg_amount;
-- IN子查询优化 -- MySQL优化器通常会自动将 IN 子查询转为 semi-join SELECT*FROM orders WHERE user_id IN (SELECT id FROM users WHERE vip_level =3); -- 等价于 SELECT o.*FROM orders o SEMI JOIN users u ON o.user_id = u.id WHERE u.vip_level =3;
-- NOT IN 的陷阱: 如果子查询结果包含NULL,整个NOT IN返回空 SELECT*FROM orders WHERE user_id NOTIN (SELECT manager_id FROM departments); -- 如果 manager_id 有NULL值,结果为空! -- 改用 NOT EXISTS SELECT*FROM orders o WHERENOTEXISTS (SELECT1FROM departments d WHERE d.manager_id = o.user_id);
-- Using filesort 优化 -- 确保 GROUP BY / ORDER BY 使用的列与索引顺序一致
-- 索引: idx_user_status(user_id, status) -- 可以利用索引排序 SELECT user_id, status, COUNT(*) FROM orders WHERE user_id IN (1, 2, 3) GROUPBY user_id, status;
-- 不能利用索引排序(顺序不匹配) SELECT user_id, status, COUNT(*) FROM orders WHERE user_id IN (1, 2, 3) GROUPBY status, user_id;
-- Using temporary 优化 -- GROUP BY 和 ORDER BY 的列不同时,会产生临时表 SELECT user_id, COUNT(*) AS cnt FROM orders GROUPBY user_id ORDERBY cnt DESC LIMIT 10; -- 优化: 使用派生表 SELECT*FROM ( SELECT user_id, COUNT(*) AS cnt FROM orders GROUPBY user_id ) t ORDERBY cnt DESC LIMIT 10;
OR 条件优化
1 2 3 4 5 6 7 8 9 10 11 12
-- OR 可能导致索引失效 SELECT*FROM orders WHERE user_id =1OR status ='paid'; -- 如果 user_id 和 status 分别有索引,MySQL可能用 Index Merge -- 但效率不如联合索引
-- 优化方案1: UNION ALL 替代 OR SELECT*FROM orders WHERE user_id =1 UNIONALL SELECT*FROM orders WHERE status ='paid'AND user_id !=1;
-- 优化方案2: 设计合理的联合索引 ALTERTABLE orders ADD INDEX idx_user_status(user_id, status);
实战:慢查询分析流程
flowchart TD
A[发现慢查询] --> B[慢查询日志/监控告警]
B --> C[EXPLAIN 分析执行计划]
C --> D{type 是 ALL?}
D -->|是| E[添加合适的索引]
D -->|否| F{Extra 有 filesort/temporary?}
F -->|是| G[优化 ORDER BY/GROUP BY]
F -->|否| H{rows 行数过大?}
H -->|是| I[检查索引选择性<br/>考虑SQL重写]
H -->|否| J{key_len 合理?}
J -->|否| K[检查联合索引使用情况]
J -->|是| L[检查是否回表过多]
L --> M{覆盖索引可行?}
M -->|是| N[调整索引覆盖查询列]
M -->|否| O[考虑业务层优化<br/>缓存/异步/分页]
E --> P[验证优化效果]
G --> P
I --> P
K --> P
N --> P
O --> P
-- 原始慢查询(执行时间 3.2s) SELECT o.id, o.order_no, o.amount, o.created_at, u.name, u.phone FROM orders o LEFTJOIN users u ON o.user_id = u.id WHERE o.created_at >='2025-01-01' AND o.status IN ('paid', 'shipped') ORDERBY o.amount DESC LIMIT 20;
-- 第一步: EXPLAIN分析 -- type=ALL, Using where; Using temporary; Using filesort -- 问题: 无合适索引,全表扫描 + 临时表 + 文件排序
-- 第二步: 创建索引 ALTERTABLE orders ADD INDEX idx_status_created(status, created_at);
-- 第三步: 重新EXPLAIN -- type=range, key=idx_status_created -- 但仍有 Using filesort(ORDER BY amount 不在索引中)
-- 第四步: 进一步优化 -- 方案A: 如果 status 的值固定,可以拆分查询 SELECT o.id, o.order_no, o.amount, o.created_at, u.name, u.phone FROM orders o JOIN users u ON o.user_id = u.id WHERE o.status ='paid'AND o.created_at >='2025-01-01' UNIONALL SELECT o.id, o.order_no, o.amount, o.created_at, u.name, u.phone FROM orders o JOIN users u ON o.user_id = u.id WHERE o.status ='shipped'AND o.created_at >='2025-01-01' ORDERBY amount DESC LIMIT 20;
-- 方案B: 使用延迟关联减少回表和排序数据量 SELECT o.id, o.order_no, o.amount, o.created_at, u.name, u.phone FROM orders o JOIN users u ON o.user_id = u.id JOIN ( SELECT id FROM orders WHERE status IN ('paid', 'shipped') AND created_at >='2025-01-01' ORDERBY amount DESC LIMIT 20 ) t ON o.id = t.id; -- 子查询在索引内完成过滤,只对20行做JOIN和回表