分页接口设计:offset/limit 与游标对比

一句话结论:offset/limit 适合“能跳页、要总数”的管理后台:SELECT ... ORDER BY id LIMIT n OFFSET m,实现简单,但深翻页要扫描并丢弃前面所有行(越翻越慢),且数据变动时会出现重复/漏行;游标分页(keyset)适合 feed/无限滚动:WHERE id > 上次位置 ORDER BY id LIMIT n,每页成本恒定、结果稳定,代价是不能跳页、多条件排序要用复合游标。选择口诀:后台表格用 offset,客户端时间线用 cursor,超大表禁止深 offset。

两种方式对比

维度offset/limitcursor(keyset)
实现LIMIT n OFFSET mWHERE 排序列 > last 值 LIMIT n
跳页支持不支持
深分页成本O(offset+n),越深越慢每页成本相近
并发写稳定性可能重复/漏行稳定,新数据只影响首页
总数可 COUNT(*)(大表也贵)一般不返回总数
适用后台列表、报表信息流、聊天记录、实时榜单

offset 参数处理

page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
if page < 1 { page = 1 }
if size < 1 || size > 100 { size = 20 } // 上限防拖库
db.Order("id DESC").Limit(size).Offset((page - 1) * size).Find(&list)

游标分页

// 响应里带 nextCursor=最后一条 id;请求 ?cursor=xxx
db.Where("id < ?", cursor).Order("id DESC").Limit(size).Find(&list)
if len(list) == size { nextCursor = list[len(list)-1].ID } // 还有下一页
  • 复合排序(如按 updated_at 再按 id)时游标形如 base64(updated_at + "|" + id),WHERE (updated_at,id) < (?,?) 元组比较,保证唯一稳定序。
  • 游标值对客户端是“不透明令牌”,服务端解析并校验,别让客户端拼 SQL 片段。

追问记忆点

  • 追问:深 offset 为什么慢?——数据库要先把前 offset 行全找出来再丢掉,offset=100 万时扫描量巨大。
  • 追问:cursor 分页还能翻回上一页吗?——原生不行,需要反向游标或另存位置快照;交互需要双向时往往回到 offset。
  • 追问:列表总数 COUNT(*) 很慢怎么办?——后台可缓存计数或接受近似;cursor 场景根本不需要总数。
  • 记忆点:跳页要总数→offset;feed 无限滚→cursor;排序字段唯一化(加 id)是稳定分页的前提。
笔记加载中…