慢 SQL 排查与优化
问题
线上出现慢 SQL 告警,如何快速定位和优化?
答案
一、排查流程
二、慢查询日志
-- 开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 超过 1 秒记录
SET GLOBAL log_queries_not_using_indexes = ON; -- 未使用索引也记录
-- 查看慢查询日志路径
SHOW VARIABLES LIKE 'slow_query_log_file';
mysqldumpslow 分析:
# 按执行次数排序 Top 10
mysqldumpslow -s c -t 10 slow.log
# 按平均耗时排序 Top 10
mysqldumpslow -s at -t 10 slow.log
三、EXPLAIN 关键字段
EXPLAIN SELECT * FROM orders WHERE user_id = 100 ORDER BY created_at DESC;
| 字段 | 关注要点 | 目标值 |
|---|---|---|
| type | 访问类型 | const > eq_ref > ref > range > index > ALL |
| key | 使用的索引 | 不为 NULL |
| rows | 预估扫描行数 | 越小越好 |
| Extra | 额外信息 | 避免 Using filesort、Using temporary |
| filtered | 过滤比例 | 越接近 100% 越好 |
必须优化的信号
type = ALL:全表扫描Extra: Using filesort:文件排序Extra: Using temporary:临时表rows非常大(> 10 万)
四、高频慢 SQL 场景与优化
场景 1:索引失效
-- ❌ 对索引列使用函数
SELECT * FROM orders WHERE DATE(created_at) = '2024-01-01';
-- ✅ 改为范围查询
SELECT * FROM orders
WHERE created_at >= '2024-01-01' AND created_at < '2024-01-02';
-- ❌ 隐式类型转换(phone 是 VARCHAR)
SELECT * FROM users WHERE phone = 13800138000;
-- ✅ 使用正确类型
SELECT * FROM users WHERE phone = '13800138000';
-- ❌ 前导通配符
SELECT * FROM products WHERE name LIKE '%手机%';
-- ✅ 使用全文索引或搜索引擎
ALTER TABLE products ADD FULLTEXT INDEX ft_name(name);
SELECT * FROM products WHERE MATCH(name) AGAINST('手机');
-- ❌ 最左前缀不匹配(索引 a, b, c)
SELECT * FROM t WHERE b = 1;
-- ✅ 查询条件包含最左列
SELECT * FROM t WHERE a = 1 AND b = 1;
场景 2:深分页
-- ❌ OFFSET 深分页(扫描 100 万行)
SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;
-- ✅ 方案 1:延迟关联(子查询只扫描索引)
SELECT o.* FROM orders o
JOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 20) t
ON o.id = t.id;
-- ✅ 方案 2:游标分页(记住上次的 ID)
SELECT * FROM orders WHERE id > 12345678 ORDER BY id LIMIT 20;
场景 3:大 IN 查询
-- ❌ IN 列表过长
SELECT * FROM products WHERE id IN (1, 2, 3, ..., 10000);
-- ✅ 分批查询
SELECT * FROM products WHERE id IN (1, 2, ..., 500);
SELECT * FROM products WHERE id IN (501, 502, ..., 1000);
-- 或使用临时表 JOIN
场景 4:COUNT 优化
-- ❌ COUNT(*) 全表扫描(InnoDB 无精确行数)
SELECT COUNT(*) FROM orders;
-- ✅ 方案 1:近似值
SELECT TABLE_ROWS FROM information_schema.TABLES
WHERE TABLE_NAME = 'orders';
-- ✅ 方案 2:单独维护计数(Redis / 计数表)
-- ✅ 方案 3:加条件限定范围
SELECT COUNT(*) FROM orders WHERE created_at > '2024-01-01';
五、索引优化工具箱
| 工具 | 用途 |
|---|---|
EXPLAIN | 查看执行计划 |
EXPLAIN ANALYZE(8.0) | 实际执行 + 耗时 |
SHOW INDEX FROM table | 查看表索引 |
SHOW STATUS LIKE 'Handler%' | 查看读行数 |
pt-query-digest | 分析慢查询日志 |
sys.schema_unused_indexes | 未使用的索引 |
常见面试问题
Q1: 索引失效的常见场景有哪些?
答案:
| 场景 | 原因 | 解决方案 |
|---|---|---|
| 函数操作 | WHERE YEAR(date) = 2024 | 改为范围查询 |
| 隐式转换 | VARCHAR 列用数字比较 | 使用正确类型 |
| 前导通配 | LIKE '%abc' | 全文索引 / ES |
| 最左前缀 | 组合索引跳列 | 调整索引或查询 |
| OR 条件 | WHERE a=1 OR b=2 | UNION 或分别建索引 |
| NOT IN / != | 无法使用索引范围扫描 | 改用 EXISTS / IN |
| 数据量小 | 优化器判断全扫更快 | 正常现象,不必优化 |
Q2: 如何判断一个 SQL 是否需要优化?
答案:
- 执行时间 > 1 秒的查询需要关注
- 扫描行数远大于返回行数(rows >> 实际结果数)
- type 为 ALL 或 index(全表/全索引扫描)
- 出现 filesort 或 temporary
- 高频执行的 SQL:即使单次 100ms,QPS=1000 时也很严重
优化优先级:高频 SQL > 慢 SQL,因为高频 SQL 的整体影响更大。
Q3: EXPLAIN 中 type 各级别的含义?
答案:
| type | 含义 | 性能 |
|---|---|---|
system | 表只有一行 | ⭐⭐⭐⭐⭐ |
const | 主键等值查询 | ⭐⭐⭐⭐⭐ |
eq_ref | JOIN 使用主键/唯一索引 | ⭐⭐⭐⭐ |
ref | 非唯一索引等值查询 | ⭐⭐⭐⭐ |
range | 索引范围扫描 | ⭐⭐⭐ |
index | 全索引扫描 | ⭐⭐ |
ALL | 全表扫描 | ⭐ |
一般要求线上查询至少达到 range 级别。