SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;
这条要先扫出并丢弃 100 万行才返回 20 行,越往后翻越慢。
为什么
LIMIT offset, n 的语义是「跳过 offset 行」,MySQL 必须真的把前 offset 行读出来才能跳过,哪怕最后扔掉。offset 越大浪费越多。
方案一:延迟关联(改动最小)
先用覆盖索引只查 id,再回表:
SELECT o.* FROM orders o
INNER JOIN (
SELECT id FROM orders ORDER BY id LIMIT 1000000, 20
) t ON o.id = t.id;
子查询只扫索引不回表,数据量小得多,通常能快几倍到几十倍。
方案二:游标分页(推荐)
记住上一页最后一行的排序值,下一页从它之后开始:
SELECT * FROM orders WHERE id > :last_id ORDER BY id LIMIT 20;
直接走主键索引定位,性能和页码无关,永远 O(20)。
按时间排序同理(注意用 (created_at, id) 联合索引,因为时间可能重复):
SELECT * FROM orders
WHERE (created_at, id) < (:last_time, :last_id)
ORDER BY created_at DESC, id DESC
LIMIT 20;
两种分页怎么选
| OFFSET 分页 | 游标分页 | |
|---|---|---|
| 跳页(直接去第 50 页) | 支持 | 不支持 |
| 深分页性能 | 差 | 恒定 |
| 数据变动时的稳定性 | 会漏/重 | 稳定 |
| 典型场景 | 后台管理表格 | 信息流、App 下拉加载 |
另外 OFFSET 分页在数据插入时会导致漏读或重复(整体后移一行),信息流场景必须用游标。
想显示总条数怎么办
COUNT(*) 在大表上同样是全表扫。常见做法:
- 只显示「下一页」按钮,不显示总数(信息流)
- 用近似值:
SHOW TABLE STATUS的Rows,或缓存一个定时更新的计数 - 分页到一定深度就提示「结果过多,请缩小筛选条件」
楼主 · 2026-09-28 13:21 · 浏览 2

