Preload一对多分页时数据量爆炸,因Preload不支持分页且无视Limit;应改用Joins一次性查询必要字段,或先分页查主表ID再批量查关联数据。

Preload 一对多分页时数据量爆炸怎么办
Preload 本身不支持分页,Preload("Orders") 会把关联的全部订单都查出来,哪怕主表只分页取了 20 条用户,也可能加载几千条订单。这不是 GORM 的 bug,是它默认行为——关联加载不感知 Limit。
- 用
Joins替代Preload:只取需要字段,一次 SQL 完成,例如db.Joins("LEFT JOIN orders ON orders.user_id = users.id").Select("users.id, users.name, orders.amount") - 若必须用 Preload,先分页查出主表 ID 列表(
SELECT id FROM users LIMIT 20 OFFSET 40),再用db.Where("user_id IN ?", ids).Find(&orders)批量查关联数据 - 避免
Preload("Orders").Limit(20)这种写法——Limit 只作用于主表,Orders 仍全量加载
带 Joins 的分页为什么 Count 总不准
db.Joins("LEFT JOIN orders...").Where("orders.status = ?", "paid").Count(&total) 算出来的不是“满足条件的用户数”,而是“满足条件的用户-订单组合数”。LEFT JOIN 后一行用户可能对应多行订单,Count 就被放大了。
- Count 必须基于主表语义:手写子查询,例如
db.Raw("SELECT COUNT(*) FROM (SELECT DISTINCT users.id FROM users LEFT JOIN orders ON ... WHERE orders.status = ?) t", "paid").Scan(&total) - 别依赖
Model(&User{})自动推断表名——Joins 后 Model 仍指向 User,但 WHERE 实际作用在 JOIN 结果集上 - 如果业务允许去重,加
DISTINCT;否则必须用子查询明确主表范围
子查询嵌套分页时 Offset 失效的根源
写 db.Table("(SELECT * FROM users WHERE deleted_at IS NULL) AS u").Offset(100).Limit(20).Find(&users),看起来封装了逻辑,但 MySQL/PostgreSQL 仍需先执行完整子查询生成临时结果集,再丢弃前 100 行——性能没改善,还多了层解析开销。
- 子查询分页 ≠ 游标分页:它只是语法包装,底层仍是扫描+丢弃
- 真正有效的做法是把游标条件下推到子查询内,例如
WHERE id > ?放进子查询括号里,而不是套一层再 Offset - 联合索引必须覆盖子查询中的所有 WHERE + ORDER BY 字段,否则子查询本身就会慢
复合排序字段分页时索引怎么建才不白搭
写 Order("status ASC, created_at DESC, id DESC") 却只在 (status) 上建了单列索引?那 ORDER BY 基本不走索引,MySQL 只能 filesort,Offset 越大越卡。
- 联合索引顺序必须严格匹配 Order 字段顺序:
INDEX idx_status_created_id (status, created_at, id) - WHERE 条件字段要前置:如果还有
Where("tenant_id = ?", tid),索引得改成(tenant_id, status, created_at, id) - 避免在排序字段中混用 ASC/DESC:MySQL 8.0+ 支持混合方向索引,但低版本只对首个字段方向敏感,其余会被忽略
游标分页在联合查询和子查询里最难绷的一点:你得确保游标值(比如上一页最后一条的 status+created_at+id)能在 JOIN 或子查询结果中稳定定位,而不是只在主表里存在。稍有不匹配,下一页就漏数据或重复。


















