offset((page - 1) * page_size)是正确偏移量计算公式,gorm不自动换算;page=1时offset(0),page=0会panic;offset必须在limit前调用,且须配合显式order排序和独立count查询。

Limit 和 Offset 在 GORM 中怎么算偏移量
Offset 不是“第几页”,而是数据库扫描时真实跳过的行数。第 page 页、每页 page_size 条,对应 Offset((page - 1) * page_size) —— 这个公式必须手写,GORM 不会帮你换算。
常见错误是把 page=1 当成 Offset(1),结果第一条数据被跳过;或者传 page=0 导致 Offset(-1),直接 panic。
-
Offset()必须在Limit()前调用(语义清晰且兼容旧版 GORM) - 传入负数或非整数会触发 panic,不能依赖数据库兜底
- MySQL 中
Limit 0可能返回全表,PostgreSQL 则报错,务必校验page_size > 0
为什么没加 Order 的分页会漏数据或重复
数据库对无序查询不保证返回顺序。同一条 SQL 执行两次,可能因索引选择、MVCC 版本、缓冲区状态不同,导致行序变化。一旦顺序浮动,Offset 就失去锚点。
比如 Offset(100).Limit(20) 第一次取到 id=105~124,第二次因排序漂移取到 id=103~122,中间两条就重复,另两条丢失。
- 必须显式写
Order("id ASC")或Order("created_at DESC, id DESC") - 复合排序字段需与数据库索引顺序完全一致,否则索引失效
- 时间字段做主排序时,高并发下可能有精度冲突,建议补
id作为第二排序字段
Count 查询为什么不能和分页共用同一个 DB 实例
链式调用如 db.Where("status = ?", "active").Offset(...).Limit(...).Count(&total) 是错的:Count() 会忽略 Offset 和 Limit,但部分条件(如 Where)可能残留或被误清空,导致总数不准。
更隐蔽的问题是预加载(Preload)混入后,Count() 可能生成 JOIN 查询,而实际列表查的是单表,总数和分页数据对不上。
- 总数查询必须新建实例:
db.Model(&User{}).Where("status = ?", "active").Count(&total) - 不能复用带
Find()或Preload()的链式对象 - 如果用了软删除(
gorm.DeletedAt),Model()要明确指定结构体,避免被全局 Scope 干扰
Offset 超过 5000 后性能断崖的真实原因
数据库执行 OFFSET 10000 LIMIT 20 时,并不是“跳过前 10000 行”,而是逐行扫描、过滤、计数,直到跳过 10000 行才开始取结果。这过程要读磁盘、解压行、校验 WHERE 条件——和全表扫描代价接近。
尤其当排序字段无索引,或 WHERE 条件无法走索引时,OFFSET 会放大所有低效因素。
- MySQL 通常在
OFFSET > 5000开始明显变慢,PostgreSQL 约在10000左右 - 即使加了索引,如果排序字段和 WHERE 字段不在同一索引中,仍可能回表+排序,拖慢 OFFSET
- 线上应设硬限制:比如
page_size * (page - 1) ,超限则拒绝或自动切游标
真正难处理的不是语法怎么写,而是排序字段是否稳定、索引是否覆盖、以及前端传来的 last_id 是否可信——这些细节一错,游标分页也会翻车。











