主键分页避免offset+limit的性能与数据一致性问题,采用where id > ? order by id limit n游标方式,需索引支持、查n+1条判断是否有下一页,并在复合排序时用created_at和id联合索引及元组游标。

为什么不能直接用 Offset + Limit 做主键分页
Offset + Limit 本身不依赖主键,它只是跳过前 N 行再取 M 行。当数据有并发写入(比如新插入或删除),同一页面可能重复出现某条记录,或彻底漏掉——这不是 GORM 的 bug,而是 SQL 标准行为:无 ORDER BY 时数据库不保证行序稳定,即使加了 ORDER BY,OFFSET 越大,数据库仍要扫描并丢弃前 N 行,百万级表上 Offset(100000).Limit(20) 可能从 20ms 拉到 2s+。
基于主键的游标分页怎么写(id ASC 场景)
核心是把“第 N 页”转换成 “WHERE id > ? ORDER BY id LIMIT 20”,靠上一页最后一条的 id 驱动下一页查询,完全避开 Offset。
- 首次请求:用
db.Order("id ASC").Limit(20).Find(&users),取回后检查len(users) > 0,拿到users[len(users)-1].ID作为 next_cursor - 后续请求:前端传
cursor=12345,后端执行db.Where("id > ?", cursor).Order("id ASC").Limit(20).Find(&users) - 必须确保
id字段有主键索引(InnoDB 默认满足),否则性能无保障 - 不要在游标查询里用
Preload,关联字段应分两步查:先取主表 ID 列表,再用IN批量加载
主键分页的边界情况怎么处理
游标分页没有“总页数”概念,但业务常需知道是否还有下一页。最轻量做法是查 limit + 1 条,比如要返回 20 条,就 Limit(21),如果结果长度为 21,说明还有下一页,返回前 20 条 + has_next: true;否则设 has_next: false。
- 前端不该传
page=5,而应传cursor=12345或cursor=2024-01-01T00:00:00Z,12345(时间+ID 复合游标) - 如果用户强行刷新或跳转,丢失 cursor,可 fallback 到首屏逻辑(
WHERE id > 0 ORDER BY id LIMIT 20) - 删除中间记录不会导致“断层”,因为游标只依赖上一条可见记录的 ID,不是绝对位置
复合排序场景下如何安全使用主键分页
仅靠 created_at DESC 不可靠——高并发下多条记录时间戳相同,数据库排序结果不确定。必须加二级排序字段,推荐 created_at DESC, id DESC 或 created_at ASC, id ASC,且两者都需联合索引支持。
- 游标值不再是单个
id,而是元组:last_created_at, last_id - 查询条件写成
db.Where("created_at - 联合索引必须按 ORDER BY 顺序建立,例如
INDEX idx_created_id (created_at, id),否则无法走索引 - 别用
RAND()或模糊字段(如LIKE "%xxx%")参与游标条件,会强制全表扫描











