运营反馈后台订单列表「转圈十几秒」。打开监控一看,这条查询 P99 已经飙到 1.2 秒,扫描行数 380 万。这就是本文的主角。
定位:先看执行计划
慢 SQL 优化的第一步永远是 EXPLAIN,而不是凭感觉加索引。这条查询的问题一目了然。
1-- 原始查询:按状态 + 商家 + 时间范围筛选,按时间倒序分页2SELECT id, order_no, amount, status, created_at3FROM orders4WHERE merchant_id = 100865 AND status IN (20, 30)6 AND created_at >= '2026-07-01'7ORDER BY created_at DESC8LIMIT 20;910-- EXPLAIN 结果:type=ALL, rows=380万, Extra=Using where; Using filesort全表扫描 + filesort,而表上其实有 merchant_id 的单列索引——但选择性太差(该商家占全表 40%),优化器直接放弃了它。
联合索引的设计思路
联合索引的列顺序遵循「等值查询在前、范围查询在后、排序字段紧随等值之后」的原则。status 是等值(IN)、created_at 既是范围又是排序字段,于是得到:
1ALTER TABLE orders ADD INDEX idx_merchant_status_created (merchant_id, status, created_at);23-- 优化后 EXPLAIN:type=range, rows=2100, Extra=Using index condition4-- P99: 1.2s -> 8ms覆盖索引避免回表
查询字段恰好全部包含在索引中时,InnoDB 无需回表取主键行。如果列表页只需要这几个字段,把 amount 也纳入索引收益更明显——但要在索引宽度与查询频率间取舍,最终我们保持三列联合索引,借助 MySQL 8 的 Index Condition Pushdown 已足够。
那些容易忽略的索引失效场景
- 对索引列做函数或运算:WHERE DATE(created_at) = ... 会放弃索引,改写成范围查询。
- 隐式类型转换:手机号字段是 VARCHAR,用数字去比较会导致全表扫描。
- 前导模糊匹配:LIKE '%关键字' 无法走索引;确需支持则用全文索引或搜索引擎。
- 优化器误判:统计信息过期时可能选错索引,ANALYZE TABLE 或 FORCE INDEX 兜底。
索引不是越多越好:每个索引都是一棵要维护的 B+ 树,写放大和页分裂的代价会在高并发写入时加倍奉还。
优化之外的思考
这次优化上线后三个月,订单表又涨了一倍,查询依然稳定。但这提醒我们:索引优化是缓解不是根治,当单表超过 5000 万行时,就该认真讨论分库分表或归档冷数据了。