gorm无原生paginate方法,limit+offset在join、group by等复杂场景下会导致总数不准、数据重复;应改用raw()子查询分页或游标分页。

为什么不能直接用 Paginate 或 Limit+Offset 做复杂分页
因为 GORM 本身没有 Paginate 方法——它不是内置函数,所有“自动分页”封装都是第三方或手写逻辑。而 Limit+Offset 在 JOIN、子查询、GROUP BY 或带 WHERE 条件的关联统计场景下,会直接失效:总数查不准、数据重复、翻页错位。比如你写 db.Joins("JOIN profiles ON users.id = profiles.user_id").Where("profiles.status = ?", "active").Count(&total),这个 Count 可能漏掉未匹配 profile 的 user,但主查询又只取有 profile 的记录,总数和列表根本对不上。
用 db.Raw() 写带子查询的 COUNT + 主查询
这是最可控、兼容性最强的方式,尤其适合含 JOIN、聚合、去重等逻辑的分页。关键点是把 COUNT 和主查询都包裹在子查询里,避免 GORM 链式调用污染。
- 总数必须独立子查询:
SELECT COUNT(*) FROM (SELECT 1 FROM users u JOIN profiles p ON u.id = p.user_id WHERE p.status = ?) AS t - 主查询也得用同结构子查询 + LIMIT/OFFSET:
SELECT u.*, p.name FROM users u JOIN profiles p ON u.id = p.user_id WHERE p.status = ? ORDER BY u.id LIMIT ? OFFSET ? - 两个查询参数要严格一致(特别是 WHERE 条件值),否则总数和实际返回条数不匹配
- 别在
Raw()里拼字符串传参,用问号占位符防 SQL 注入:db.Raw(sql, status, limit, offset).Scan(&users)
如何让 Raw() 分页结果映射到结构体
db.Raw() 默认不支持自动字段映射到 struct 字段,尤其是 JOIN 后字段名冲突(如 users.id 和 profiles.id)。必须显式指定别名,并确保结构体字段标签匹配:
- SQL 中给每个字段加别名:
SELECT u.id AS user_id, u.name AS user_name, p.id AS profile_id - 结构体字段用
gorm标签对齐:UserID uint `gorm:"column:user_id"` - 如果字段太多,可定义嵌套结构体 +
Embedded,但 Raw 不识别 Embedded,仍需手动别名 - 更轻量做法:用
map[string]interface{}接收,再按需转成业务 struct,避免字段爆炸
游标分页必须自己拼 WHERE 条件,Raw() 是唯一出路
当数据量大、OFFSET 超过 10 万行时,Offset(999999).Limit(20) 在 MySQL 上会扫描百万行再丢弃,响应从毫秒变秒级。此时必须改用基于主键或时间戳的游标分页,而 GORM 的链式 API 完全无法表达 WHERE created_at 这种复合条件——只能靠 <code>Raw() 手写。
- 前端需传上一页最后一条的
created_at和id,后端校验非空且类型正确 - WHERE 条件必须覆盖索引:联合索引
(created_at, id)才能高效走范围扫描 - ORDER BY 必须和 WHERE 顺序严格一致:
ORDER BY created_at DESC, id DESC - 查
size + 1条,判断是否有下一页,不查总数(避免全表 COUNT)
真正难的不是写 SQL,而是保证 COUNT 子查询、主查询、游标条件三者语义完全一致;任何一处 WHERE 条件漏写、索引缺失、排序字段没覆盖,都会导致分页错乱。别省那几行代码,该手写就手写,该加索引就加索引。











