面对复杂SQL慢查询,有哪些底层分析方法?
面试场景
面试官:”给你一条慢SQL,你的分析思路是什么?”
SQL优化不是猜测,而是有方法论的系统分析。本文讲解底层分析方法,帮助你形成体系化的优化思维。
方法论框架
慢SQL
│
├── 1. 定量分析:慢在哪里?(Profile)
│
├── 2. 执行计划:怎么执行的?(EXPLAIN)
│
├── 3. 成本分析:为什么这样执行?(成本模型)
│
└── 4. 优化方案:如何改进?(索引/SQL重写)
方法一:EXPLAIN执行计划
基础用法
EXPLAIN SELECT * FROM orders WHERE user_id = 123;
关键字段解读
| 字段 | 含义 | 关注点 |
|---|---|---|
| type | 访问类型 | system > const > eq_ref > ref > range > index > ALL |
| key | 实际使用的索引 | NULL表示没用索引 |
| rows | 预估扫描行数 | 越小越好 |
| Extra | 额外信息 | Using filesort/Using temporary要警惕 |
type详解
const ─── 主键/唯一索引等值查询
eq_ref ─── 多表JOIN,被驱动表主键/唯一索引
ref ─── 普通索引等值查询
range ─── 索引范围扫描
index ─── 全索引扫描
ALL ─── 全表扫描(最差)
Extra关键信息
| Extra | 含义 | 是否需优化 |
|---|---|---|
| Using index | 覆盖索引 | ✅ 好 |
| Using where | 需要回表过滤 | ⚠️ 一般 |
| Using filesort | 额外排序 | ❌ 需优化 |
| Using temporary | 使用临时表 | ❌ 需优化 |
| Using index condition | 索引下推(ICP) | ✅ 好 |
方法二:EXPLAIN ANALYZE(MySQL 8.0.18+)
执行并获取实际指标
EXPLAIN ANALYZE
SELECT * FROM orders WHERE user_id = 123;
输出示例
-> Index lookup on orders using idx_user_id (user_id=123)
(cost=35.13 rows=50)
(actual time=0.089..0.241 rows=47 loops=1)
| 字段 | 含义 |
|---|---|
| cost | 优化器预估成本 |
| rows | 预估行数 |
| actual time | 实际执行时间 |
| actual rows | 实际返回行数 |
价值:可以看到预估与实际的差距,判断统计信息是否准确。
方法三:Optimizer Trace
查看优化器选择执行计划的完整决策过程。
开启Trace
SET optimizer_trace = 'enabled=on';
SELECT * FROM orders WHERE user_id = 123 AND status = 1;
SELECT * FROM information_schema.optimizer_trace\G
关键信息
{
"considered_execution_plans": [
{
"plan_prefix": [],
"table": "orders",
"best_access_path": {
"considered_access_paths": [
{
"access_type": "ref",
"index": "idx_user_id",
"rows": 50,
"cost": 35.13,
"chosen": true
},
{
"access_type": "ref",
"index": "idx_status",
"rows": 500000,
"cost": 3500.00,
"chosen": false // 成本高,未选择
}
]
}
}
]
}
方法四:Profile性能分析
开启Profile
SET profiling = 1;
SELECT * FROM orders WHERE user_id = 123;
SHOW PROFILE FOR QUERY 1;
输出示例
| Status | Duration |
|---|---|
| starting | 0.000012 |
| checking permissions | 0.000005 |
| Opening tables | 0.000021 |
| Sending data | 0.523456 |
| end | 0.000003 |
分析:Sending data耗时最长,说明是数据读取慢(可能涉及大量回表)。
详细分析
SHOW PROFILE ALL FOR QUERY 1;
SHOW PROFILE CPU, BLOCK IO FOR QUERY 1;
方法五:索引统计信息分析
查看索引基数
SHOW INDEX FROM orders;
| Column_name | Cardinality | 含义 |
|---|---|---|
| user_id | 100000 | 不同值的数量 |
| status | 5 | 不同值很少,区分度低 |
原则:高区分度的字段适合建索引。
查看数据分布
SELECT status, COUNT(*)
FROM orders
GROUP BY status;
如果90%的数据status=1,那么WHERE status=1的索引效果会很差。
方法六:慢查询日志
开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 超过1秒记录
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
分析工具
# mysqldumpslow汇总分析
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
# pt-query-digest更强大
pt-query-digest /var/log/mysql/slow.log
实战分析案例
案例:ORDER BY优化
原始SQL:
SELECT * FROM orders
WHERE user_id = 123
ORDER BY create_time DESC
LIMIT 10;
执行计划:
type: ref, key: idx_user_id
Extra: Using filesort -- 有额外排序!
分析:
- 用了idx_user_id定位数据
- 但还需要额外排序(filesort)
优化方案:建立联合索引
ALTER TABLE orders ADD INDEX idx_user_time (user_id, create_time);
优化后执行计划:
type: ref, key: idx_user_time
Extra: Using index condition -- 无filesort
优化决策树
慢SQL
│
├─ type=ALL? → 考虑添加索引
│
├─ Using filesort? → 考虑优化ORDER BY(联合索引)
│
├─ Using temporary? → 考虑优化GROUP BY/DISTINCT
│
├─ rows很大? → 检查WHERE条件是否命中索引
│
└─ key=NULL? → 检查索引是否存在/是否失效
面试答题框架
分析工具五件套:
1. EXPLAIN - 查看执行计划
2. EXPLAIN ANALYZE - 对比预估与实际(8.0+)
3. Optimizer Trace - 了解优化器决策过程
4. Profile - 定位具体耗时环节
5. 慢查询日志 - 发现线上问题SQL
分析思路:
- 看type:ALL/index需警惕
- 看rows:预估扫描行数是否合理
- 看Extra:filesort/temporary需优化
- 看统计信息:是否准确
总结
| 方法 | 作用 | 使用时机 |
|---|---|---|
| EXPLAIN | 查看执行计划 | 必备,每次优化都用 |
| EXPLAIN ANALYZE | 获取实际执行指标 | 需要对比预估与实际 |
| Optimizer Trace | 了解优化器决策 | 理解为什么选这个计划 |
| Profile | 定位耗时环节 | 分析时间花在哪里 |
| 慢查询日志 | 发现问题SQL | 线上监控 |