分页前必须用 session 隔离 count,否则复用 limit/offset 导致总数错误;join 分页须显式 order by 且字段带联合索引;关联过滤条件应写入 on 而非 where;避免 preload 分页和 order by rand();大偏移量应改用游标分页。

分页前必须用 Session 隔离 Count 查询
直接在同一个 *gorm.DB 实例上链式调用 Where + Joins + Count,得到的总数永远是错的——Count 会复用前面的 LIMIT 和 OFFSET,结果恒 ≤ 当前页条数。这不是 bug,是 GORM 显式设计行为。
- 简单场景:用
db.Session(&gorm.Session{NewDB: true}).Model(&User{}).Where(...).Joins(...).Count(&total),靠NewDB: true切断上下文 - 复杂 JOIN(含多表、子查询、ON 条件带函数):手写子查询更可靠,例如
db.Raw("SELECT COUNT(*) FROM (SELECT 1 FROM users u LEFT JOIN profiles p ON u.id = p.user_id WHERE u.status = ?) t", status).Scan(&total) - 别在事务里反复查
Count+Find,两次查询间若数据变更,总数和当前页记录可能不一致
JOIN 分页必须显式指定 ORDER BY 且字段要带索引
没 ORDER BY 的 Limit+Offset 在 JOIN 场景下极不稳定:同一请求执行两次,可能漏掉或重复返回记录。MySQL 对无序 JOIN 结果不做任何顺序承诺,尤其当关联表有并发写入时。
- 排序字段必须是联合索引的最左前缀,比如
ORDER BY u.created_at DESC, u.id DESC,对应索引INDEX(created_at, id) - 避免只用
created_at排序——时间相同记录太多时,MySQL 内部排序随机,翻页会跳变 - 别用
ORDER BY RAND()做“随机分页”,性能爆炸且无法实现翻页语义 - 如果主表和关联表都需过滤,把主表条件放
WHERE,关联表条件务必写进ON子句,否则 LEFT JOIN 变成 INNER JOIN
JOIN 分页不能用 Find,得用 Select + Scan
Find(&users) 只认模型定义的字段映射,对 JOIN 后的混合列(如 users.name, profiles.age)完全不可控,字段全为零值或 nil,JSON 序列化后要么丢字段,要么返回全空对象。
- 定义专用响应结构体,字段名与 SELECT 别名严格对齐:
type UserWithProfile struct { ID uint; Name string; Age *int `json:"age,omitempty"` } - 用
db.Table("users").Select("users.*, profiles.age as age").Joins("LEFT JOIN profiles ON users.id = profiles.user_id").Where(...).Order(...).Limit(...).Offset(...).Scan(&results) - 关联字段可能为空时,结构体字段类型建议用指针(如
*string,*int),否则无法区分 “数据库 NULL” 和 “Go 零值” - 别依赖
Preload做分页——它发两条 SQL(主表 + IN 查询关联表),OFFSET 是在主表上算的,但最终结果集大小受关联数据影响,页码语义断裂
大偏移量时 OFFSET 必然变慢,得换游标分页
当 OFFSET 超过 10 万行,MySQL 必须扫描并丢弃前 N 行,响应从毫秒级拉到秒级。这不是 GORM 层能优化的,是 SQL 执行模型决定的。
- 游标分页优先:前端传上一页最后一条的
created_at和id,SQL 改成WHERE created_at - 确保
created_at+id是联合索引,且覆盖查询所需字段(避免回表) - 如果业务允许不显示总页数,可只查
size + 1条,判断是否有下一页,彻底省掉COUNT查询 - 缓存 JOIN 分页结果时,key 必须包含所有动态参数(WHERE 值、ORDER 字段、LIMIT/OFFSET 或游标值),否则不同请求会互相污染
ORDER BY 字段没建索引,或者缓存 key 漏了游标值,导致用户翻页时数据重复或丢失——这些都不是代码写错,而是设计阶段就埋下的坑。











