一、慢查询的影响与定位

在数据库性能问题中,慢查询是最常见也最致命的性能杀手。一条执行超过 1 秒的 SQL,在高并发场景下可能导致连接池耗尽、CPU 飙高、甚至整个服务不可用。

典型的慢查询症状:

  • API 接口响应时间突然从 100ms 飙升到 5s+
  • 数据库 CPU 使用率持续 > 80%
  • 大量请求排队等待数据库连接
  • 监控告警中出现大量 Lock wait timeout
① 发现问题 监控告警 / 慢日志 ② 定位 SQL pt-query-digest 分析 ③ EXPLAIN 分析 执行计划解读 ④ 调优执行 索引 / 改写 / 架构 慢查询阈值: > 1s 检查 rows_examined Top N 慢 SQL 排名 频率 & 平均耗时 type = ALL ? key 是否命中? 添加缺失索引 优化 SQL 结构 ⑤ 验证效果 & 持续监控 对比调优前后的执行时间、扫描行数、CPU 占用 慢日志分析 执行计划 索引优化 SQL 改写 持续闭环:监控 → 分析 → 优化 → 验证

二、开启慢查询日志

-- 查看当前慢查询配置
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 log_queries_not_using_indexes = 'ON';  -- 记录未使用索引的查询
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

-- 永久生效:写入 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

三、EXPLAIN 执行计划解读

EXPLAIN 是 MySQL 优化的核心工具,以下是关键字段的解读顺序:

EXPLAIN SELECT o.*, u.name 
FROM orders o 
JOIN users u ON o.user_id = u.id 
WHERE o.status = 'pending' 
  AND o.create_time > '2025-01-01';

-- 输出解读:
+----+-------------+-------+--------+---------------+---------+---------+-------+------+-------------+
| id | select_type | table | type   | possible_keys | key     | key_len | ref   | rows | Extra       |
+----+-------------+-------+--------+---------------+---------+---------+-------+------+-------------+
|  1 | SIMPLE      | o     | ref    | idx_status_ts | idx_st  | 82      | const | 1200 | Using where |
|  1 | SIMPLE      | u     | eq_ref | PRIMARY       | PRIMARY | 4       | o.uid |    1 | NULL        |
+----+-------------+-------+--------+---------------+---------+---------+-------+------+-------------+

type 字段(从最优到最差):

type 值 含义 性能
system表只有一行记录极佳
const通过主键/唯一索引匹配一行极佳
eq_refJoin 时使用主键/唯一索引优秀
ref使用非唯一索引查找良好
range索引范围扫描(BETWEEN, >, <, IN)良好
index全索引扫描一般
ALL全表扫描需要优化

四、索引优化策略

1. 覆盖索引(Covering Index)

查询的所有字段都在索引中,无需回表查询。这是性能最高的索引策略:

-- 慢查询:需要回表
SELECT id, name, email, phone FROM users WHERE status = 1;

-- 创建覆盖索引
CREATE INDEX idx_status_cover ON users(status, id, name, email, phone);

-- 验证:Extra 列出现 "Using index" 即表示覆盖索引
EXPLAIN SELECT id, name, email, phone FROM users WHERE status = 1;

2. 联合索引与最左前缀原则

-- 联合索引
CREATE INDEX idx_a_b_c ON orders(user_id, status, create_time);

-- 以下查询能命中索引:
WHERE user_id = 1                           -- ✅ 命中 user_id
WHERE user_id = 1 AND status = 'pending'   -- ✅ 命中 user_id + status
WHERE user_id = 1 AND create_time > '2025-01-01'  -- ✅ 命中 user_id(范围后失效)

-- 以下查询不能命中索引:
WHERE status = 'pending'                    -- ❌ 跳过最左列
WHERE user_id = 1 OR status = 'pending'    -- ❌ OR 导致索引失效

3. 避免索引失效的常见场景

  • WHERE 条件中对索引列使用函数:WHERE YEAR(create_time) = 2025
  • 隐式类型转换:WHERE phone = 13800138000(phone 是 VARCHAR)
  • 使用 != / NOT IN / NOT EXISTS
  • LIKE '%keyword'(前置通配符)
  • 联合索引不遵循最左前缀原则

五、实战案例:500ms → 5ms

问题场景:订单列表页面加载超时,慢日志显示以下 SQL 平均耗时 500ms:

-- 原始 SQL(500ms)
SELECT o.id, o.order_no, o.amount, o.status, o.create_time,
       u.name AS user_name, u.phone,
       p.name AS product_name
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
LEFT JOIN products p ON o.product_id = p.id
WHERE o.status = 'pending'
  AND o.create_time > '2025-01-01'
ORDER BY o.create_time DESC
LIMIT 20;

EXPLAIN 分析:

orderstype=ALL,扫描 50 万行,无可用索引。

优化方案:

-- 1. 创建精准索引(联合索引遵循最左前缀)
CREATE INDEX idx_order_status_time ON orders(status, create_time);

-- 2. 优化后 SQL(5ms)—— 索引覆盖查询条件 + 排序字段
EXPLAIN SELECT ...;
-- type: range, key: idx_order_status_time, rows: 18

优化效果对比:

指标 优化前 优化后 提升
执行时间500ms5ms↓ 100x
扫描行数500,00018↓ 99.9%
访问类型ALLrange质的飞跃

六、pt-query-digest 工具使用

# 安装 Percona Toolkit
sudo apt install percona-toolkit

# 分析慢查询日志 — 输出 Top 10 报告
pt-query-digest /var/log/mysql/slow.log --limit=10

# 分析最近 1 小时的慢日志
pt-query-digest /var/log/mysql/slow.log --since=1h

# 输出到文件
pt-query-digest /var/log/mysql/slow.log > slow_report.txt
💡 慢查询优化黄金法则

先用 EXPLAIN 确认 type 是否为 ALL 或 index;优先创建覆盖索引减少回表;联合索引列顺序 = 等值条件在前 + 范围条件在后 + 排序字段最后;避免在索引列上使用函数或计算。定期运行 pt-query-digest 形成周报机制。