记一次千万级订单表分页查询优化:
记一次千万级订单表分页查询优化:从 28 秒到 0.01 秒
背景
电商系统,delivery_orders表存放配送订单,累计数据量 1200 万行。运营后台有一个订单列表页,支持按商家 ID 筛选、按下单时间倒序翻页。
上线初期没什么问题,随着数据量增长,运营反馈"翻到后面几页就卡死了"。
问题复现
-- 运营翻到第 500 页时实际执行的 SQLSELECT*FROMdelivery_ordersWHEREseller_id=1001ORDERBYcreated_atDESCLIMIT10000,10;执行时间:28.3 秒。
而翻第 1 页只需要 0.03 秒,第 10 页 0.2 秒,越往后越慢,到第 1000 页直接超时。
排查过程
第一步:EXPLAIN 看执行计划
EXPLAINSELECT*FROMdelivery_ordersWHEREseller_id=1001ORDERBYcreated_atDESCLIMIT10000,10;结果:
type: ref key: idx_seller_id rows: 487623 Extra: Using filesort索引是走了,但rows = 487623,扫了将近 50 万行。更关键的是Using filesort,说明排序没走索引。
第二步:理解 LIMIT offset 的本质
很多人以为LIMIT 10000, 10是"跳过前 10000 条,取 10 条",实际上 MySQL 的执行过程是:
- 扫描满足
WHERE seller_id = 1001的所有行 - 按
created_at DESC排序 - 取出前 10010 行
- 丢掉前 10000 行,返回最后 10 行
也就是说,offset 越大,做的无用功越多。翻到第 1000 页就要取出 100010 行再丢掉 100000 行。
第三步:确认索引设计问题
SHOWINDEXFROMdelivery_orders;现有索引:
idx_seller_id(seller_id)idx_created_at(created_at)
两个单列索引,MySQL 只能用其中一个,排序和筛选没法同时走索引,所以出现了Using filesort。
解决方案
方案一:建联合索引(治本)
ALTERTABLEdelivery_ordersADDINDEXidx_seller_created(seller_id,created_at);联合索引让筛选和排序都走同一个索引,消除 filesort。
但 LIMIT offset 大的问题还没解决,继续优化。
方案二:子查询定位 ID,再回表取数据
-- 优化后的 SQLSELECT*FROMdelivery_ordersWHEREidIN(SELECTidFROMdelivery_ordersWHEREseller_id=1001ORDERBYcreated_atDESCLIMIT10000,10);子查询只查id,走覆盖索引不需要回表,速度极快;外层再用id IN精确回表取完整数据,只回表 10 次。
执行时间:0.8 秒。有改善但还不够。
方案三:游标分页(彻底解决)
游标分页的思路是:不用 offset,而是记住上一页最后一条记录的游标值,下一页从游标位置开始取。
-- 第一页SELECTid,seller_id,created_at,statusFROMdelivery_ordersWHEREseller_id=1001ORDERBYcreated_atDESCLIMIT10;-- 假设第一页最后一条的 created_at = '2024-03-01 10:00:00',id = 98765-- 第二页:用游标替代 offsetSELECTid,seller_id,created_at,statusFROMdelivery_ordersWHEREseller_id=1001AND(created_at<'2024-03-01 10:00:00'OR(created_at='2024-03-01 10:00:00'ANDid<98765))ORDERBYcreated_atDESCLIMIT10;这样每次查询都只扫描真正需要的行,不管翻到第几页,执行时间都是固定的。
配合idx_seller_created联合索引,执行时间:0.01 秒。
最终方案落地
-- 1. 建联合索引ALTERTABLEdelivery_ordersADDINDEXidx_seller_created(seller_id,created_at);-- 2. 后端接口改为游标分页,接收参数:-- seller_id, last_created_at, last_id, page_size-- 3. 对外仍然支持"跳页"需求的场景,限制最大页数-- 超过 100 页引导用户缩小筛选条件前端改动:将"页码"改为"加载更多"或"下一页"交互,产品上线后运营反馈正常,列表页无论翻多少页响应都在 50ms 以内。
总结
| 方案 | 第500页耗时 | 适用场景 |
|---|---|---|
| 原始 LIMIT offset | 28.3s | 不适用 |
| 子查询优化 | 0.8s | 数据量不太大时可用 |
| 游标分页 | 0.01s | 推荐,大数据量必选 |
分页慢的根本原因不是索引,是 LIMIT offset 的机制问题。大 offset 场景下,无论索引多完善都会慢,只有换掉分页方式才能真正解决。
