id游标分页是gorm中唯一真正解决翻页越慢问题的方案,因其利用索引定位起点后顺序读取,避免offset扫描丢弃前n行的性能瓶颈。

直接用 id 做游标分页,是目前 GORM 里唯一能真正解决“翻页越翻越慢”问题的方案。OFFSET 分页在 10 万行后基本不可用,不是代码写得不好,是 MySQL/PostgreSQL 的物理执行机制决定的——它必须扫描并丢弃前 N 行。
为什么 id 游标分页比 Offset 快得多
数据库执行 OFFSET 100000 时,并不会跳过前 10 万行,而是逐行读、逐行比较、再丢弃;而 WHERE id > ? 只需走索引定位起点,然后顺序取 20 条,IO 和 CPU 压力几乎恒定。
- 前提是
id是主键且自增(或至少单调递增),有索引支撑 - 如果表用的是 UUID 或时间戳作主键,
id > ?依然有效,但需确保该字段有索引,且值不重复 - 别指望 GORM 自动优化 OFFSET——它只负责拼 SQL,慢的根本原因在查询模型本身
id 游标分页的正确写法
核心就两条:首次查最小 id 起始,后续靠上一页最后一条的 id 继续往后推。不能混用页码逻辑,也不能漏掉排序。
- 首次请求:
db.Order("id ASC").Limit(20).Find(&users),不带Where - 后续请求:
db.Where("id > ?", lastID).Order("id ASC").Limit(20).Find(&users),lastID必须来自上一页users[len(users)-1].ID - 前端必须传
last_id(不是page),后端不做转换,直接塞进Where条件 - 若
lastID是负数、零或非数字,应返回400 Bad Request,而不是静默设为 0 或忽略
容易被忽略的三个细节
写对了语句,但漏掉这些点,照样返回空结果、重复数据,或被绕过索引。
- 排序字段必须和
WHERE字段一致:用了id > ?,就必须Order("id ASC");若用created_at DESC,条件就得改成created_at ,否则高并发下会漏数据 - 别在游标分页里用
Preload:GORM 会先查主表 ID 列表,再发第二条 SQL 批量加载关联数据;但游标分页本身不保证 ID 连续,IN子句可能跨页,导致关联数据错位 -
Count总数在游标分页里本就不适用——你无法回答“还剩多少页”,只能返回has_next: true/false,方法是查Limit(21),取前 20 条返回,第 21 条存在即说明还有下一页
游标分页不是“更高级的分页”,而是面对大数据量时不得不做的取舍:放弃总页数和随机跳页能力,换回稳定延迟和可预测性能。最容易翻车的地方,往往不在 SQL 写法,而在前端传参格式没对齐、后端没校验 last_id 类型、或者误把 created_at 当唯一游标用。











