线上接口突然变慢,大概率是数据库查询出了问题。这篇文章梳理一下排查慢查询的完整流程。

开启慢查询日志

-- 查看当前配置
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 slow_query_log_file = '/var/log/mysql/slow.log';

-- 永久配置(写入 my.cnf)
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1  # 记录未使用索引的查询

用 pt-query-digest 分析

手动看慢查询日志很费时,pt-query-digest(Percona Toolkit)可以聚合分析:

pt-query-digest /var/log/mysql/slow.log | head -100

输出会按响应时间降序列出各类查询,告诉你哪些查询最值得优化:

# Query 1: 0.50 QPS, 2.50x concurrency, ID 0xABC123
# This item is included in the report because it matches --limit.
#              pct   total     min     max     avg     95%  stddev  median
# Count         15    1000
# Exec time     80%    200s     0.1s     5s    0.2s    0.5s   0.3s   0.15s
# Rows sent      5%    500       0      10       0       1       1       0
# Rows examine  90%    900k    100     5000    900    2000    800     600

重点关注 Rows examine(扫描行数)和 Exec time(执行时间)。

EXPLAIN 分析单条查询

找到慢查询后,用 EXPLAIN 看执行计划:

EXPLAIN SELECT u.name, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at > '2024-01-01'
GROUP BY u.id\G

关键字段:

id: 1
select_type: SIMPLE
table: u
type: range          -- range 是走了索引的范围扫描,还不错
key: idx_created_at  -- 使用了这个索引
rows: 5000           -- 预估扫描 5000 行
Extra: Using where; Using temporary; Using filesort
--           ↑ 有临时表和文件排序,这是性能瓶颈

看到 Using filesortUsing temporary,通常意味着需要加合适的索引或调整 GROUP BY/ORDER BY。

常见慢查询场景及优化

场景 1:JOIN 字段没有索引

-- 慢:orders.user_id 没有索引,每次都要全表扫描
SELECT * FROM users u JOIN orders o ON u.id = o.user_id;

-- 加索引
ALTER TABLE orders ADD INDEX idx_user_id (user_id);

场景 2:SELECT *

-- 慢:传输大量不需要的数据,且无法用覆盖索引
SELECT * FROM products WHERE category_id = 5;

-- 快:只取需要的字段
SELECT id, name, price FROM products WHERE category_id = 5;

场景 3:深分页

-- 慢:需要扫描并丢弃前 100000 条数据
SELECT * FROM logs ORDER BY id LIMIT 100000, 20;

-- 快:用游标分页(记录上一页最后一条的 id)
SELECT * FROM logs WHERE id > 100000 ORDER BY id LIMIT 20;

场景 4:IN 子查询

-- 慢:子查询可能导致全表扫描
SELECT * FROM orders WHERE user_id IN (
    SELECT id FROM users WHERE vip_level > 3
);

-- 快:改写为 JOIN
SELECT o.* FROM orders o
JOIN users u ON o.user_id = u.id
WHERE u.vip_level > 3;

监控建议

  • 配置 Prometheus + MySQL Exporter,监控 mysql_global_status_slow_queries 指标
  • 设置告警阈值,慢查询数量突增时及时收到通知
  • 每周跑一次 pt-query-digest,主动发现新出现的慢查询

性能优化是持续的过程,不是一次性的工作。