用户反馈"订单列表越翻越慢",前两页 200ms,翻到十几页要 6 秒以上。一开始以为是数据量大了,看了慢查询日志才发现是一条 SQL 写得有问题。记录一下完整的定位和优化过程。
定位
开启慢查询日志(long_query_time = 1),抓到罪魁祸首:
SELECT * FROM orders WHERE user_id = 123
ORDER BY created_at DESC LIMIT 100000, 20;
EXPLAIN 一看:type 是 index,rows 扫了 12 万。深分页时 MySQL 要把前 100020 行都取出来再丢掉前 100000 行,越往后翻代价越大。
优化第一步:延迟关联
SELECT o.* FROM orders o
JOIN (SELECT id FROM orders WHERE user_id = 123
ORDER BY created_at DESC LIMIT 100000, 20) t
ON o.id = t.id;
子查询里只走覆盖索引(user_id + created_at + id 都在索引上,不用回表),拿到 20 个 id 再回表取整行。SQL 从 6s 降到 0.4s。
优化第二步:改产品逻辑(这才是治本)
和产品聊了一下:用户根本不会真翻到第 5000 页,这个场景是"导出全部订单"。于是接口改成游标分页(拿上一页最后一条的 id 做条件,WHERE id < 上次最大id ORDER BY id DESC LIMIT 20),每页都是常数开销;导出走异步任务。深分页问题从根上消灭。
顺带的收获
- user_id 上居然没有单独索引,只有个联合索引把 user_id 放在第二位——等于白建。索引顺序必须匹配查询
- SELECT * 改成按需取列,顺便解决了网络传输的开销
- 给慢查询日志接了告警,每天超过 10 条就发邮件,别等问题大到用户来反馈
这次最大的体会:性能问题往往不只是技术问题。深分页可以用各种 SQL 骚操作续命,但"让用户不需要翻第 5000 页"才是正解。技术手段和产品手段配合,比单方面死磕 SQL 高效得多。

Comments 4 条评论
延迟关联这个思路我一直没想到
游标分页治本,认同
联合索引字段顺序错了等于白建
慢查询日志接告警这个操作好