游标分页是高并发、大数据量、用户端无限滚动场景的事实标准;必须在offset不可靠时切换,如app/web无限滚动、写入频繁、offset超5000/10000、page无上限等。

偏移量分页(Limit+Offset)能用,但不等于该用;游标分页(WHERE id > ? ORDER BY id LIMIT)不是“高级技巧”,而是高并发、大数据量、用户端场景下的事实标准。
什么时候必须用游标分页
当出现以下任一情况时,Offset已不可靠,应立即切游标:
- 接口面向 App 或 Web 无限滚动(如朋友圈、消息流),用户可能翻到第 200 页以上
- 数据写入频繁(每秒新增 > 10 条),且要求分页结果不重复、不丢失
-
OFFSET值超过 5000(MySQL)或 10000(PostgreSQL),查询响应明显变慢或触发数据库 CPU 飙升 - 前端传的是
page参数,但后端无法控制其上限(例如开放 API)
典型错误现象:db.Offset(99999).Limit(20) 在压测中查出空数组,或同一页两次请求返回不同记录——这不是 GORM bug,是 SQL 扫描机制决定的。
游标分页怎么写才不丢数据
核心是保证“下一页起始点”严格来自上一页最后一条记录的排序字段值,且该字段有索引、单调可比。
- 首选排序字段:
id ASC(主键自增最稳),SQL 形如WHERE id > ? ORDER BY id ASC LIMIT 20 - 次选组合:
created_at DESC, id DESC(防时间重复),条件需写成WHERE (created_at, id) 或拆成两层 <code>WHERE created_at - 首次请求不能带
WHERE,直接db.Order("id ASC").Limit(20).Find(&users);后续请求必须用users[len(users)-1].ID作为last_id,不能取错索引 - 前端必须透传
last_id(不是page),后端不做转换;若last_id为负数或非数字,直接返回400 Bad Request
漏掉 ORDER BY 或用错比较方向(比如 id 却配 <code>ASC),会导致查不到数据或重复返回。
偏移量分页还能用吗?怎么用才安全
能用,但仅限于低风险场景:后台管理、数据静态、页码稳定在前 20 页内。必须守住三条底线:
-
page和page_size必须校验:page >= 1,page_size严格限制在 1–100(建议min(p.PageSize, 100)) -
Order()必须显式声明,且字段有索引;禁止只写Order("created_at DESC"),应补Order("created_at DESC").Order("id DESC") -
Count(&total)必须独立构造 DB 实例:db.Session(&gorm.Session{NewDB: true}).Model(&User{}).Where(...).Count(&total),否则会继承前面的Limit/Offset导致总数为 0 或恒 ≤page_size
别把 Offset((page - 1) * pageSize) 当成数学公式照搬——如果 page=1,Offset(0) 合法;但 page=0 传进来,Offset(-1) 会 panic。
最容易被忽略的细节:总数查询和关联加载
总数不准,八成是因为复用了同一个 *gorm.DB 链;而 Preload 分页,九成会放大 N+1 问题。
-
Count()不继承Joins、Preload、Group,复杂查询必须手写子查询:db.Raw("SELECT COUNT(*) FROM (SELECT 1 FROM users u JOIN profiles p ON u.id = p.user_id WHERE p.active = ?) AS t", true).Scan(&total) - 游标分页里禁用
Preload:先查主表 ID 列表(SELECT id FROM ... WHERE id > ? ORDER BY id LIMIT 21),再用IN批量查关联数据 - 大数据量下,考虑放弃
total字段,改用has_next(查page_size + 1条,多出来那条只判断是否存在)
游标分页的“游标”不是抽象概念,它是真实存在的、必须由前端透传的、带业务语义的字段值——漏传、错传、缓存旧值,都会让分页逻辑崩塌。











