慢查询故障实战复盘

2022年双十一凌晨,某电商平台的数据库突然出现大量慢查询,平均响应时间从5ms飙升到30s。

监控告警疯狂弹出:慢查询数量超过5000条/秒,数据库连接池耗尽,所有业务请求超时。

技术团队排查后发现:一条看似简单的订单查询SQL,由于缺少联合索引,导致全表扫描50万行数据。

更可怕的是:这条SQL在白天流量低的时候执行很快,但大促零点流量高峰时并发执行,直接拖垮了整个数据库。

这次故障导致订单系统瘫痪1小时,直接损失约1000万元。

【面试官手记】

慢查询是数据库最常见的性能杀手。我面试过的候选人里,能说清楚"慢查询分析"的有50%,能说清楚"执行计划解读"的有30%,能说清楚"索引优化"的有20%。慢查询的关键词是早发现 + 快优化

一、慢查询识别标准 🔴

1.1 慢查询定义

慢查询定义:

MySQL慢查询阈值(默认):
- long_query_time = 1秒
- 超过1秒的查询视为慢查询

慢查询的影响:
- 占用数据库连接
- 锁等待时间增加
- 拖慢整个数据库
- 影响所有业务

慢查询的典型特征:
- 全表扫描
- 缺少索引
- 返回数据量过大
- 关联查询过多
- 排序没有索引

1.2 慢查询日志

-- 查看慢查询日志配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
SHOW VARIABLES LIKE 'log_queries%';

-- 设置慢查询阈值为100ms
SET GLOBAL long_query_time = 0.1;
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL slow_query_log_file = '/var/lib/mysql/slow.log';

-- 查看慢查询数量
SHOW STATUS LIKE 'Slow_queries';
-- +---------------+-------+
-- | Variable_name | Value |
-- +---------------+-------+
-- | Slow_queries  | 1234  |
-- +---------------+-------+

1.3 pt-query-digest分析

# 使用pt-query-digest分析慢查询
pt-query-digest /var/lib/mysql/slow.log

# 输出示例:
# Query 1: 0.5% of all queries (1234 queries)
#   Time range: 2024-01-01 00:00:00 to 2024-01-01 01:00:00
#   Query: SELECT * FROM orders WHERE user_id = ? AND status = ?
#   Response time: 3.5s  min: 0.5s, max: 10.2s, avg: 3.5s
#   Rows examined: 500000, Rows sent: 10
#   WARNING: query took 350x longer than it should
#
#   Query profile:
#   +--------+--------+--------+--------+--------+--------+
#   | total  | min    | max    | avg    | 95%   | threads|
#   +--------+--------+--------+--------+--------+--------+
#   | 1234   | 0.5s   | 10.2s  | 3.5s   | 8.2s  | 5.3    |
#   +--------+--------+--------+--------+--------+--------+
#
#   Query times:
#   - Scan table: 98%  # 98%的时间在扫描表
#
#   Recommendation:
#   CREATE INDEX idx_user_status ON orders(user_id, status);

二、执行计划解读 🔴

2.1 EXPLAIN详解

-- 分析SQL执行计划
EXPLAIN SELECT * FROM orders WHERE user_id = 123 AND status = 1 ORDER BY create_time DESC LIMIT 10;

-- 输出:
-- +----+-------------+--------+------------+-------+--------------------------+--------------------------+---------+--------+---------+----------+----------+-------------+
-- | id | select_type| table  | type       | key   | key_len                 | ref                      | rows    | filtered| Extra  |
-- +----+-------------+--------+------------+-------+--------------------------+--------------------------+---------+--------+---------+----------+----------+-------------+
-- |  1 | SIMPLE     | orders | ref        | idx_user_status | 8     | const,const              | 1500    | 100.00 | Using index condition; Using filesort |
-- +----+-------------+--------+------------+-------+--------------------------+--------------------------+---------+--------+---------+----------+----------+-------------+

-- 关键字段解读:

-- type(访问类型,从好到差):
-- - const:主键或唯一索引,等值查询,最多1条
-- - eq_ref:主键或唯一索引,非等值查询
-- - ref:普通索引,等值查询
-- - range:索引范围查询
-- - index:索引全扫描
-- - ALL:全表扫描(最差!)

-- key:实际使用的索引

-- rows:扫描的行数,越少越好

-- Extra(额外信息):
-- - Using filesort:需要额外排序(需要优化!)
-- - Using temporary:需要临时表(需要优化!)
-- - Using index:覆盖索引,不需要回表
-- - Using index condition:索引下推

2.2 常见问题模式

-- 问题1:全表扫描
SELECT * FROM orders WHERE status = 1;
-- type = ALL, rows = 500000
-- 解决:status字段添加索引

-- 问题2:索引失效
SELECT * FROM orders WHERE status = 1 AND LEFT(create_time, 7) = '2024-01';
-- 解决:避免函数运算,改用范围查询

-- 问题3:最左前缀匹配
CREATE INDEX idx_a_b_c ON table(a, b, c);
SELECT * FROM table WHERE b = 1;  -- 索引失效!
SELECT * FROM table WHERE a = 1;  -- 索引生效
SELECT * FROM table WHERE a = 1 AND b = 1;  -- 索引生效
SELECT * FROM table WHERE a = 1 AND c = 1;  -- 部分生效

-- 问题4:隐式类型转换
CREATE TABLE users (user_id VARCHAR(20));
SELECT * FROM users WHERE user_id = 123;
-- user_id是字符串,传入数字会全表扫描!

2.3 SQL优化实战

// 优化前:慢查询
@Select("SELECT * FROM orders WHERE user_id = #{userId} AND status = #{status} ORDER BY create_time DESC")
List<Order> selectOrders(@Param("userId") Long userId, @Param("status") Integer status);

// 优化1:添加合适索引
-- CREATE INDEX idx_user_status_time ON orders(user_id, status, create_time DESC);

// 优化后:快查询
@Select("SELECT id, user_id, amount, status, create_time FROM orders WHERE user_id = #{userId} AND status = #{status} ORDER BY create_time DESC LIMIT 100")
List<Order> selectOrders(@Param("userId") Long userId, @Param("status") Integer status);

// 优化原则:
// 1. 只查需要的字段,不用SELECT *
// 2. LIMIT限制返回数量
// 3. 避免在索引列上使用函数
// 4. 避免隐式类型转换

三、索引优化方案 🟡

3.1 索引设计原则

索引设计原则:

1. 选择性高的字段建索引
   - 唯一索引 > 等值查询
   - 性别字段不适合建索引(只有男女)
   - 状态字段要判断分布是否均匀

2. 联合索引设计
   - 最左前缀匹配原则
   - 等值查询字段放前面
   - 范围查询放最后

3. 覆盖索引
   - 索引包含所有查询字段
   - 避免回表查询

4. 索引长度
   - 字符串前缀索引
   - 平衡选择性和长度

联合索引设计示例:
查询:WHERE a = 1 AND b = 1 AND c > 100 ORDER BY d
索引:(a, b, c, d)
- a和b用于等值查询
- c用于范围查询
- d用于排序

3.2 索引优化案例

-- 场景:订单查询优化
-- 原SQL
SELECT * FROM orders
WHERE user_id = 123
  AND status IN (1, 2, 3)
  AND create_time BETWEEN '2024-01-01' AND '2024-01-31'
ORDER BY create_time DESC
LIMIT 100;

-- 分析执行计划
EXPLAIN SELECT * FROM orders
WHERE user_id = 123
  AND status IN (1, 2, 3)
  AND create_time BETWEEN '2024-01-01' AND '2024-01-31'
ORDER BY create_time DESC
LIMIT 100;

-- type = ALL(全表扫描!)需要优化

-- 优化1:添加复合索引
ALTER TABLE orders ADD INDEX idx_user_status_time
    (user_id, status, create_time DESC);

-- 优化2:改写SQL
-- IN改为多个OR
SELECT * FROM orders
WHERE user_id = 123 AND status = 1 AND create_time BETWEEN '2024-01-01' AND '2024-01-31'
UNION ALL
SELECT * FROM orders
WHERE user_id = 123 AND status = 2 AND create_time BETWEEN '2024-01-01' AND '2024-01-31'
UNION ALL
SELECT * FROM orders
WHERE user_id = 123 AND status = 3 AND create_time BETWEEN '2024-01-01' AND '2024-01-31';

-- 优化3:只查需要的字段
SELECT id, user_id, amount, status, create_time FROM orders
WHERE user_id = 123
  AND status IN (1, 2, 3)
  AND create_time BETWEEN '2024-01-01' AND '2024-01-31'
ORDER BY create_time DESC
LIMIT 100;

3.3 索引维护

-- 查看索引使用情况
SELECT index_name, cardinality, seq_in_index
FROM information_schema.statistics
WHERE table_schema = 'ecommerce' AND table_name = 'orders'
ORDER BY index_name, seq_in_index;

-- 分析表(更新索引统计)
ANALYZE TABLE orders;

-- 检查索引是否被使用
SHOW STATUS LIKE 'Handler_read%';
-- Handler_read_first: 索引第一条的读取次数
-- Handler_read_key: 按索引读取次数
-- Handler_read_last: 索引最后一条的读取次数
-- Handler_read_next: 按索引顺序读取下一条
-- Handler_read_prev: 按索引顺序读取上一条
-- Handler_read_rnd: 基于固定位置读取
-- Handler_read_rnd_next: 全表扫描读取

-- 删除冗余索引
SHOW INDEX FROM orders;
-- 查看索引列表,找出重复索引
-- DROP INDEX idx_user ON orders;

四、生产避坑 🟡

4.1 慢查询的五大坑

坑1:SELECT * 全字段查询

问题:SELECT * 返回所有字段
场景:表有20个字段,只用3个
解决方案:
- 只查需要的字段
- 使用覆盖索引

坑2:隐式类型转换

问题:字符串字段用数字查询
场景:user_id VARCHAR传Long
解决方案:
- 保证类型一致
- CAST显式转换

坑3:分页深度过大

问题:LIMIT 1000000, 10
场景:查第100万页
解决方案:
- 使用游标分页
- 或限制最大页数

坑4:索引失效

问题:在索引列上使用函数
场景:WHERE DATE(create_time) = '2024-01-01'
解决方案:
- 改写为范围查询
- WHERE create_time BETWEEN '2024-01-01 00:00:00' AND '2024-01-01 23:59:59'

坑5:没有监控

问题:慢查询出现后才发现
场景:大促零点流量峰值
解决方案:
- 配置慢查询告警
- 定期分析慢查询日志

4.2 慢查询检查清单

开发规范:
- [ ] 不使用SELECT *
- [ ] 使用LIMIT限制返回
- [ ] 避免索引列上使用函数
- [ ] 保证类型一致

索引规范:
- [ ] 高选择性字段建索引
- [ ] 联合索引遵循最左前缀
- [ ] 定期分析慢查询日志
- [ ] 删除冗余索引

监控规范:
- [ ] 配置慢查询告警
- [ ] 监控QPS和响应时间
- [ ] 监控锁等待

五、真实面试回放 🟡

面试官:慢查询怎么分析和优化?

候选人(小张):分析三步:

一是开启慢查询日志。

二是用pt-query-digest分析。

三是用EXPLAIN看执行计划,重点关注type、key、rows、Extra。

优化方案:

一是添加合适的索引。

二是避免SELECT *,只查需要的字段。

三是改写SQL,避免函数运算。

面试官:怎么判断索引是否合适?

小张:看执行计划的type列。

const/eq_ref/ref是好的,range凑合,index勉强,ALL最差。

还要看rows扫描行数,越少越好。

Extra里出现Using filesort和Using temporary需要优化。

【面试官手记】

小张这场面试的亮点:

  1. 知道慢查询分析三步法

  2. 知道执行计划关键字段含义

  3. 知道索引优化的常见方法

慢查询是P6工程师必备知识点,能完整回答的候选人,说明有数据库调优经验。

慢查询的核心是早发现 + 快优化。记住三个要点:

  1. 分析工具:慢查询日志、pt-query-digest、EXPLAIN
  2. 优化方向:添加索引、减少返回、改写SQL
  3. 预防措施:Code Review、慢查询告警、定期分析

慢查询是数据库的隐形杀手,大促前必须全面排查。