MySQL 性能优化不是看到慢 SQL 就立刻加索引。真正有效的优化过程,应该先确认问题范围,再定位瓶颈,再选择成本最低、收益最稳定的方案。很多线上问题不是某一条 SQL 写得差,而是数据量、索引设计、查询模型、分页方式、事务范围和业务流程共同导致的。
在供应链系统里,订单、库存、采购、出入库、结算这些表增长很快。系统刚上线时几万条数据跑得很快,半年后变成几千万条,原来的查询就可能从几十毫秒变成几秒。优化 MySQL 的关键是让数据库少扫描、少排序、少回表、少锁等待。
第一步:先确认是不是数据库慢
当用户反馈订单列表慢时,不要直接打开 SQL 改写。应该先确认耗时发生在哪里。
一次请求的耗时可能来自:
- 网关排队。
- 应用线程池不足。
- 远程服务调用慢。
- Redis 缓存超时。
- MySQL 查询慢。
- 返回数据过大导致网络传输慢。
- 前端渲染慢。
可以通过接口日志、链路追踪、APM、慢查询日志来拆分耗时。如果接口总耗时 3 秒,其中 MySQL 查询只用了 80 毫秒,那优化 SQL 不是当前重点。
在 Java 应用里,建议每个核心接口记录 traceId、SQL 耗时、调用下游耗时、返回条数。没有这些数据,优化容易变成猜。
第二步:找到真实慢 SQL
确认瓶颈在数据库后,再打开 MySQL 慢查询日志或使用监控平台找慢 SQL。
慢 SQL 的判断不能只看单次耗时,还要看调用频率。一个报表 SQL 偶尔跑 5 秒不一定最急;一个订单列表 SQL 每次 300 毫秒、每分钟调用几千次,可能对系统压力更大。
常用观察指标包括:
- 平均耗时。
- P95、P99 耗时。
- 扫描行数。
- 返回行数。
- 执行次数。
- 是否频繁创建临时表。
- 是否频繁 filesort。
- 锁等待时间。
供应链系统中常见慢 SQL 包括订单多条件查询、库存流水查询、采购单明细关联、应收应付报表、仓库作业统计。
第三步:使用 EXPLAIN 分析执行计划
拿到慢 SQL 后,必须用 EXPLAIN 看执行计划。执行计划告诉我们 MySQL 准备怎么执行这条 SQL。
重点关注这些字段:
type:访问类型,至少应尽量达到range、ref,避免大表ALL。key:实际使用的索引。rows:预估扫描行数。Extra:是否出现Using temporary、Using filesort。filtered:过滤比例。
比如订单表 scm_order 有 3000 万行,页面按客户、状态、创建时间查询:
1 | SELECT id, order_no, customer_id, status, created_at, total_amount |
如果只在 customer_id 上有单列索引,MySQL 可能先找出该客户所有订单,再过滤状态和时间,并排序。客户订单量大时,这个查询就会慢。
第四步:按查询模式设计联合索引
索引不是越多越好。索引会提升查询,也会增加写入成本和存储成本。优化时应该根据高频查询模式设计联合索引。
上面的订单查询可以考虑:
1 | CREATE INDEX idx_customer_status_created |
这样 MySQL 可以先按客户过滤,再按状态过滤,再按时间范围扫描。由于排序字段也在索引里,ORDER BY created_at DESC LIMIT 20 的成本会明显降低。
联合索引设计要考虑最左前缀原则。等值条件通常放在前面,范围条件和排序字段放在后面。对于不同查询,不要为了每个页面都创建一个索引,而是合并相近查询,找最核心的访问路径。
供应链系统里的常见索引组合包括:
- 订单:
customer_id + status + created_at。 - 采购单:
supplier_id + status + created_at。 - 库存流水:
sku_id + warehouse_id + created_at。 - 出库单:
warehouse_id + status + planned_ship_time。 - 财务单据:
tenant_id + bill_type + bill_date。
第五步:避免 SELECT *
很多列表接口习惯写 SELECT *,这在数据量小时问题不明显,数据量大后会导致更多 IO、更多网络传输,也更容易触发回表。
列表页应该只查询展示需要的字段。详情页再根据主键查询完整信息。
例如订单列表只需要订单号、客户、状态、金额、创建时间,就不要把备注、扩展 JSON、收货地址、审批意见一起查出来。供应链系统里很多表有大字段,比如合同备注、物流轨迹、扩展属性,这些字段对列表查询非常不友好。
如果查询字段都在索引里,还可能形成覆盖索引,减少回表。
第六步:优化分页方式
深分页是 MySQL 性能问题的高发点。
1 | SELECT id, order_no, created_at |
这条 SQL 需要先跳过 100000 行,再返回 20 行。页码越深,成本越高。
更好的方式是基于游标分页:
1 | SELECT id, order_no, created_at |
页面保存上一页最后一条记录的创建时间和 id,下一页从这个位置继续查。对于订单流水、库存流水、操作日志这类按时间滚动查看的数据,游标分页更稳定。
如果必须支持跳页,可以用延迟关联优化:先通过索引查出主键,再回表查详情。
第七步:减少大表 JOIN 和复杂统计
供应链系统经常有多表关联:订单表、客户表、商品表、库存表、仓库表、结算表。开发时写一个大 JOIN 很方便,但数据量上来后很难优化。
优化思路包括:
- 把高频展示字段冗余到主表,例如客户名称、供应商名称、仓库名称。
- 明细表和主表分开查询,避免列表页一次 JOIN 大量明细。
- 报表类查询走宽表、汇总表或数仓,不要压在交易库上。
- 对低频复杂查询做异步导出。
例如订单列表需要展示客户名称,可以在订单表冗余 customer_name_snapshot。客户改名时不影响历史订单展示,也减少订单列表 JOIN 客户表。
第八步:控制事务范围和锁
SQL 慢不一定是扫描慢,也可能是锁等待。
供应链系统里库存扣减、采购入库、出库确认都可能更新同一批库存行。如果事务里先调用外部接口,再更新库存,就会把锁持有时间拉长,导致并发请求排队。
优化原则是:
- 事务内只做必要的数据库操作。
- 外部接口调用放在事务外。
- 固定多表更新顺序,减少死锁。
- 库存扣减使用条件更新,避免先查后改的并发漏洞。
- 对热点库存考虑分仓、分批次、分桶。
库存扣减可以写成:
1 | UPDATE scm_inventory |
然后根据影响行数判断是否锁定成功。这比先查询库存再更新更安全。
第九步:用业务模型减少查询压力
有些性能问题不能只靠 SQL 调优解决,而要调整业务模型。
例如 ERP 首页要展示今天订单数、待发货数、库存预警数、采购待审数。如果每次打开首页都实时扫订单表、库存表、采购表,数据库压力会很大。
更合理的方式是把这些指标做成统计表:
1 | scm_dashboard_metric |
业务事件发生后异步更新统计表,首页直接读统计结果。这样牺牲几秒一致性,换来稳定的查询性能。
第十步:验证优化效果
优化完成后,必须验证,而不是只看执行计划。
验证内容包括:
- 优化前后 SQL 耗时对比。
- 扫描行数是否下降。
- 是否仍然出现临时表和 filesort。
- 高并发下连接池、CPU、IO 是否改善。
- 写入性能是否被新增索引拖慢。
- 结果是否和优化前一致。
对于核心 SQL,建议把优化结论记录下来:原 SQL、问题原因、改造方案、索引变更、回滚方式、压测结果。这样后续团队维护时不会重复踩坑。
总结
MySQL 性能优化的步骤可以概括为:确认瓶颈、定位慢 SQL、分析执行计划、设计索引、改写 SQL、优化分页、减少大 JOIN、控制事务、调整业务模型、验证效果。
在供应链系统里,性能优化不能只看数据库技巧,还要理解业务访问模式。订单列表、库存流水、采购审批、财务报表的查询目标不同,优化策略也不同。真正稳定的优化方案,是让 SQL、索引和业务模型匹配起来。