整理 Java 和 MySQL 笔记时,我发现自己很容易把“接口慢”和“该加索引了”连在一起。但一个请求里有业务处理、数据库访问,也可能有等待和外部调用,光知道接口耗时高,还不知道该改哪里。
这篇记一下我整理后的排查思路。下面的订单表和 SQL 是学习示例,没有连接数据库实测,也没有对应的线上优化数据。
先别急着加索引
1 | 一次请求的响应时间 |
如果 SQL 耗时明显,我会先看具体语句、参数和扫描情况。如果 SQL 单独执行很快,接口却慢,就得继续看连接获取、外部调用,或者有没有在循环里反复查数据库。
“平均耗时正常”也还不够。偶尔慢一次,可能正好发生在用户最需要结果的时候。我会把分位数、并发量和锁等待一起拿出来看,而不是只留一个平均值。
应用日志和 MySQL 慢查询日志要对照着用:前者看请求各段耗时,后者帮助找到数据库里的具体语句。慢查询日志的记录规则还取决于配置,查不到记录时也要先看配置。
拿一条查询往下看
假设要从订单表里找出某个用户最近创建的 20 条待处理订单:
1 | SELECT id, status, created_at |
先查看计划:
1 | EXPLAIN |
| 计划字段 | 我要看什么 |
|---|---|
type |
当前访问方式是否与查询规模相称 |
possible_keys / key |
候选索引与实际选择的索引 |
rows / filtered |
估计扫描量与过滤情况,不把估计当成实测 |
Extra |
排序、覆盖索引等补充信息,结合完整查询判断 |
我会把 (user_id, status, created_at) 列为一个复合索引候选:前两列用于等值筛选,后面是排序列。接下来还要看表里已经有什么索引、数据分布怎样、其他查询怎么用这张表,再决定是否创建。
1 | -- 学习环境中的候选方案,创建前先检查已有索引。 |
复合索引的列顺序,我更愿意结合实际查询和最左前缀来判断。有些笔记把顺序记成一套固定口诀,换个查询就未必适用。这里可以对照 MySQL 多列索引文档。
几句口诀,回头得补上条件
SELECT * 不一定让索引失效,但可能多返回了不需要的字段,也可能增加回表开销。对这条示例查询,我只取 id、status 和 created_at,至少能把需要什么写清楚。
OR、NOT IN 也不能一看到就判定“不走索引”。最后还是得看完整查询、数据分布和优化器给出的计划。
索引多了,写入和存储的成本也会增加。为一条查询加索引之前,我还会看看它在实际访问里占多大比例。
还有一个容易忽略的区别:MySQL 8.0.18 起支持的 EXPLAIN ANALYZE 会真正执行语句。需要实际执行信息时它很有用,但不能把它当成普通 EXPLAIN 随手运行,尤其是成本高的查询。官方说明里有两者的区别。
改完以后,怎么知道是它起了作用
| 要保持一致的条件 | 要比较的证据 |
|---|---|
| 相同数据、参数与并发设置 | 请求耗时分布、扫描量、执行计划 |
| 明确冷缓存或热缓存状态 | 避免把缓存预热当成索引收益 |
| 包含有代表性的参数 | 避免只测最容易命中的用户或时间范围 |
| 关注读取与写入 | 检查索引修改是否增加其他操作成本 |
再快的第二次查询,也可能只是缓存热起来了。做前后对比时,我会把数据、参数、并发和缓存状态一并记下来,再比较耗时和扫描量。
这些笔记以后还要拿到具体问题里检验。目前先提醒自己:遇到慢接口,第一步是找到时间花在哪儿,索引排在后面。