上周,一个Java开发人员负责订单系统从MySQL 5.7迁移到8.0。上线前,Spring Boot连接正常,核心SQL能执行,单元测试和冒烟测试也全部通过。团队原以为升级完成,没想到当天晚高峰,订单列表接口开始忽快忽慢。
监控显示,接口P50基本没变,P99却从约200ms升到480ms。业务代码没改,订单表的索引也没动。开发人员最先怀疑连接池,检查后发现等待时间没有明显异常;接着让Codex比较SQL,发现同一条查询在新旧数据库里走了不同索引。
SQL没改,为什么运行时间会变? 因为数据库执行SQL前,还要根据统计信息、索引和成本模型选择执行计划。版本升级可能改变这一选择,也可能同时改变运行环境。排查的重点,不是先认定8.0更慢,而是拿到证据,证明延迟究竟增加在哪一步。
一、先判断:P99翻倍,不代表全部查询变慢
P99是尾部延迟指标,意味着约99%的请求不超过这个响应时间。假设P50仍为40ms,但P99从200ms变为480ms,说明大多数请求可能没有显著退化,只有部分慢请求拉高了尾部。
这类现象常与特定租户、查询条件或时间段有关。例如,小租户查询正常,大租户的订单列表明显变慢;普通状态查询正常,叠加时间范围排序后突然耗时增加。平均耗时会掩盖这些差异。
第一步应该从APM里找到慢请求,记录接口参数、数据库调用耗时和连接等待时间,再把慢请求对应到具体SQL。若时间耗在数据库连接获取,应先检查连接池;若耗在SQL执行,再继续查执行计划。
P99告诉我们问题存在,不能直接告诉我们问题属于优化器。
二、同一条SQL,为什么会选出两种执行计划?
订单列表有一条典型查询:
SELECT id, order_no, amount, created_at
FROM orders
WHERE tenant_id = 1001 AND status = 'PAID'
ORDER BY created_at DESC
LIMIT 20;
假设表上同时存在(status, created_at)和(tenant_id, status, created_at)两个索引。前者可以按状态、时间扫描,但还要过滤租户;后者通常更有机会直接定位指定租户的已支付订单,并利用索引顺序取得最新记录。
为什么优化器有时不选看起来更合适的索引?它并不知道每种访问路径的真实耗时,只能结合统计信息估算扫描行数、随机读取和排序等成本,再选择预计成本较低的方案。
MySQL版本变化、统计信息重新采样、数据分布变化,都可能让估算结果不同。因此,升级前后SQL相同,并不意味着一定选择相同索引。即便索引相同,访问行数和缓存情况不同,耗时也可能不同。
这也是本次排查最重要的认知:不要拿索引名称代替执行成本,更不要把"使用了索引"当作性能已经正常的证据。
三、统计信息不准确,会怎样放大慢查询?
假设一张订单表有千万行,PAID占了大多数,但某个租户只占很小一部分。优化器如果低估了状态索引需要扫描的行数,就可能选择一条实际成本很高的路径。
MySQL使用表和索引的统计信息帮助估算。迁移、重建索引或重新采样后,统计结果可能发生变化。MySQL 8.0还支持直方图,在适合的字段上辅助估算数据分布,但不会因为升级就自动为所有字段创建直方图。
排查时应该比较新旧环境的表规模、索引定义、估算行数和真实扫描情况。若估算严重偏离实际,可在评估影响后更新统计信息,并重新观察计划;必要时再判断是否适合使用直方图。
但刷新统计信息并不是万能修复。如果查询本身需要扫描大量数据、排序成本很高,或者联合索引顺序不适合业务条件,统计信息正确也不一定快。
四、为什么EXPLAIN显示命中索引,接口仍然卡?
很多开发人员只看传统EXPLAIN的key字段。有索引名称,就认为查询已优化;出现全表扫描,就直接加索引。这种判断很容易偏离实际。
对于前面的订单查询,如果数据库沿着状态索引向后扫描,却需要跳过大量不属于目标租户的记录,最终即使只返回20条订单,也可能检查大量候选行。若返回字段不在二级索引中,还可能发生回表。
另外,索引若无法同时满足过滤和排序需求,数据库可能执行额外排序。大租户和小租户的数据分布不同,某条计划在一个参数上很快,在另一组参数上却可能明显变慢。
因此,需要看实际访问了多少行、过滤掉多少行、排序发生在哪一步,而不是只看最终返回20行。
五、用EXPLAIN ANALYZE确认慢在哪里
在支持该功能的MySQL 8.0版本中,可以用EXPLAIN ANALYZE查看查询实际执行的步骤、行数和耗时:
EXPLAIN ANALYZE
SELECT id, order_no, amount, created_at
FROM orders
WHERE tenant_id = 1001 AND status = 'PAID'
ORDER BY created_at DESC LIMIT 20;
重点对照三个位置:估算行数与实际行数是否差很多;索引扫描、过滤和排序哪一步耗时最多;某个步骤是否因为循环执行而放大总成本。
注意,EXPLAIN ANALYZE会真正执行查询。不要在生产高峰随意对重查询运行,也不要把树状输出里的所有时间简单相加。应优先在可控环境使用脱敏但具有代表性的数据进行对照。
MySQL 5.7没有同样的EXPLAIN ANALYZE能力,因此跨版本对比不能要求两边输出完全相同。可以结合旧环境的EXPLAIN、慢查询日志与实际耗时,在8.0侧进一步定位具体执行步骤。
六、别漏掉另一个根因:升级时环境也变了
有时候执行计划并没有明显问题,接口却仍然变慢。原因可能是数据库迁移改变了执行环境。
比如旧实例运行很久,InnoDB缓冲池已有大量热点数据;新实例刚切流,缓存尚未预热,更多查询需要读取磁盘。又或者迁移过程中修改了字符集、排序规则、连接驱动、实例规格和参数,增加了额外开销。
还可能遇到后台数据校验、备份、统计任务与线上请求争夺资源。此时把性能下降全部归因于优化器,会让排查走错方向。
建议检查新旧实例的CPU、磁盘IO、缓冲池状态、活跃连接和慢查询变化,再确认Java侧数据库连接池是否出现等待。只有在负载和运行条件可比时,版本前后的性能差异才有解释价值。
七、最有效的排查顺序:一次只验证一个假设
如果是我接手这次故障,会先保留升级前后的慢请求样本和监控截图,避免反复切换配置后丢失证据。
第一,确定退化集中在哪些接口、租户和参数。第二,确认耗时发生在连接获取还是SQL执行。第三,比对新旧表结构、索引、数据规模和执行计划。第四,检查估算行数与真实行数是否偏离。第五,排除缓存预热、硬件和驱动配置差异。
只有确认执行计划是主要原因后,才分别验证更新统计信息、调整联合索引、改写SQL等方案。不要一次性修改SQL、连接池和数据库参数,否则即使P99恢复,也无法说明真正有效的修复是什么。
遇到只在少数大租户复现的问题,测试要覆盖小、中、大租户及不同订单状态。能在一组参数上变快,不代表所有业务参数都变快。
八、解决方案:恢复性能不能靠强制索引碰运气
如果统计信息明显不准确,应先评估更新统计信息后的效果;如果查询路径存在大量无效扫描,就分析更合适的联合索引,或者减少不必要的返回字段与排序成本。
FORCE INDEX可以用于受控对照,判断某种索引选择是否更快,但不宜未经验证就作为长期通用修复。今天适合大租户的索引,未必适合小租户和其他筛选条件。新增索引也会增加写入和存储成本。
对于已经上线的系统,还要准备回退措施。MySQL大版本升级未必能够简单原地降级,因此回退方案应在迁移前明确数据同步、切换窗口和兼容性条件,不能等生产出问题后才临时决定。
九、让Codex参与,但不要让它只凭SQL猜答案
给Codex一条SQL,通常只能得到索引建议。要分析版本升级问题,至少应提供新旧版本、关键数据库配置、表结构与索引、典型查询参数、执行计划、慢SQL记录和接口调用链。
让Agent先回答:是否确有执行计划变化?哪个步骤成本增加?是估算偏差、数据倾斜,还是环境因素?每个候选方案需要什么证据才能确认有效?
然后再让Codex协助生成对比脚本、整理性能结果、修改必要的查询或索引,并补齐回归测试。测试数据要脱敏,避免把真实用户信息和数据库凭证交给无关的工具环境。
Agent的价值是缩短证据收集与验证时间,不是代替开发人员做没有依据的数据库变更。
十、修复后怎样证明P99真正恢复?
建议使用相同的数据规模、参数分布、并发量和统计窗口,分别比较升级前、升级后以及修复后的表现。把冷缓存阶段和预热后的阶段分开,不能拿低峰数据与促销高峰直接比较。
除接口P99,还要观察SQL实际耗时、扫描行数、数据库CPU与IO、写入延迟。SQL重写后必须验证分页顺序、过滤条件和返回结果,防止"跑得更快,却查错了数据"。
这篇只突出一个主指标:同负载P99回归比 = 升级后P99 ÷ 升级前P99。 例如200ms升到480ms,回归比就是2.4倍;修复后降到220ms,回归比为1.1倍。这只是说明计算方式的示例,并非实际测试结果。
该指标用于判断是否存在明显尾部延迟回归,不能单独说明根因。只有调用链和SQL证据对应起来,才能判断优化是否真正有效。
十一、Plus和Pro怎么判断?
如果主要让ChatGPT、Codex分析单个Spring Boot项目的慢SQL、比较执行计划和检查统计信息,Plus通常够用。
如果日常要跨多个微服务和大型数据库持续收集慢查询、分析复杂执行计划、修改SQL并开展多轮压测,Pro更适合高频、长任务的工程工作流。
套餐选择仍应结合实际使用额度和任务频率;无论使用Plus还是Pro,都不能替代真实数据库监控和压测验证。
最后
MySQL升级到8.0后,SQL没改,为什么接口P99反而翻倍?
因为数据库版本变化可能改变执行计划,也可能伴随统计信息、缓存状态和运行环境变化。SQL文本没变,不代表数据库完成这条SQL的工作量没变。
正确的工程路径是:定位慢请求、保留参数与执行证据、比较新旧执行路径、排除环境差异、逐项验证方案,最后在可比负载下确认P99和业务结果都恢复正常。
持续分享ChatGPT、Codex与Java后端工程实战。真正值得复用的不是某条强制索引技巧,而是这套从现象到证据、再到修复验证的排查方法。
持续分享 Codex、大模型开发与 AI 编程实战内容,
也整理了稳定的 Plus/Pro 会员订阅渠道,有需要可自取。