GORM不提供Paginate方法,广告曝光等高写入场景应弃用Limit+Offset而采用游标分页;需以主键或时间戳+ID组合为游标,避免COUNT(*)统计总数。

GORM 本身不提供 Paginate 方法,广告曝光这类高写入、实时性要求强的场景下,用 Limit + Offset 分页极易出现漏数、重复、延迟飙升等问题——这不是配置问题,是 SQL 模型本身的缺陷。
为什么 Offset 分页在曝光日志里会丢数据
曝光数据每秒写入数百条,OFFSET 10000 查询时,数据库要真实扫描并丢弃前 10000 行。但在这期间,新曝光记录持续插入,导致:同一页请求两次可能拿到不同结果;翻页时某条曝光记录“跳过”或“重现”。这不是 GORM 的 bug,是 MySQL/PostgreSQL 对无唯一排序约束 + 大偏移量查询的固有行为。
必须满足两个前提才可勉强用 Offset:
- 数据写入频率极低(如后台管理类静态配置)
- 排序字段(如
id或created_at)有联合索引且严格单调
广告曝光显然不满足。
游标分页必须用主键或时间戳+ID组合
替代方案不是“换插件”,而是改查询逻辑:前端不再传 page=5,而是传上一页最后一条的 id 或 created_at 值。
例如:
- 首次请求:
db.Order("id ASC").Limit(20).Find(&exposures) - 后续请求:
db.Where("id > ?", lastID).Order("id ASC").Limit(20).Find(&exposures)
关键点:
-
id必须是自增主键或 UUID(带时间序),不能用created_at单独做游标(存在并发写入时间相同的情况) - WHERE 条件和 ORDER 必须严格对应,否则索引失效,性能崩盘
- 不要在游标查询里用
Preload加载关联广告详情——先查出 ID 列表,再用IN批量查一次
Count 总数在曝光场景下通常没必要
广告曝光列表几乎从不显示“共 XXX 条”,因为总数增长太快,查一次 COUNT(*) 可能已过期。如果 UI 确实需要页码控件(比如后台 debug 页面),只对最近 1 小时或 10 万条内数据做近似统计:
- 用
SELECT COUNT(*) FROM exposure_log WHERE created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR) - 避免全表
COUNT,它会锁表或拖慢写入 - 总数和分页查询必须用独立
*gorm.DB实例,否则Where条件污染
更实际的做法是禁用总数,前端用“加载更多”按钮代替页码。
别碰第三方 pagination 插件
像 github.com/joshbetz/pagination 这类库,底层仍是 Limit/Offset,只是帮你算了个 (page-1)*size。它掩盖了三个致命问题:
- 没校验
page是否 ≤ 0,传page=0可能触发Offset(-10)导致 panic - 不强制你加
Order,结果不可预测 - 升级 GORM v2.6+ 后,Scope 行为变更导致分页失效,但错误静默
广告系统对稳定性敏感,多一层封装就多一层失控风险。手写游标逻辑 10 行以内,可控、可测、可加监控。
真正麻烦的不是写代码,而是说服产品接受“没有总页数”和“只能往前翻”的交互——这恰恰说明你的分页方案选对了。











