不能直接用 db.Raw().Limit().Offset() 做分页,因为 Raw() 返回的 gorm.DB 的 Limit/Offset 不参与 Scan/Count 计算,Count() 对 Raw() 默认失效或返回 0;手动写 COUNT() 子查询易因数据库差异(如 MySQL 缺 AS 别名、PostgreSQL 禁止子查询 LIMIT)报错,且 GROUP BY/窗口函数场景需重写聚合逻辑。

为什么不能直接用 db.Raw().Limit().Offset() 做分页
因为 db.Raw() 返回的是 *gorm.DB,但它的 Limit() 和 Offset() 不会自动参与后续的 Scan() 或 Count() 计算——尤其是总数统计时,你没法复用原SQL的 WHERE 条件,硬写 SELECT COUNT(*) FROM (...) AS t 容易出错或不兼容不同数据库。
更关键的是:GORM 的 Count() 方法对 Raw() 结果默认不生效,调用后常返回 0 或 panic。
- MySQL 下子查询别名缺
AS可能报Every derived table must have its own alias - PostgreSQL 对
LIMIT/OFFSET在子查询中限制更严,直接套用会报错 - 如果原生SQL含
GROUP BY或窗口函数,COUNT(*)不能简单包裹,必须重写聚合逻辑
用 Session().DryRun 提前拿到完整 SQL 再手动分页
这是最可控的方式:先让 GORM 构建带条件的原生查询,用 DryRun 拦截 SQL 和参数,再拼出 COUNT 和分页主查询。适合复杂过滤+原生 JOIN 场景。
示例:查用户订单数 + 最近下单时间(需原生聚合)
// 构造带条件的原生SQL(含参数占位)
sql := "SELECT u.id, u.name, COUNT(o.id) as order_count, MAX(o.created_at) as last_order FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE u.status = ? GROUP BY u.id"
session := db.Session(&gorm.Session{DryRun: true})
var dummy []map[string]interface{}
session.Raw(sql, "active").Find(&dummy)
<p>// DryRun 后从 session.Statement 获取 SQL 和 Args
stmt := session.Statement
countSQL := "SELECT COUNT(*) FROM (" + stmt.SQL.String() + ") AS count<em>table"
rows, </em> := db.Raw(countSQL, stmt.Vars...).Rows()
rows.Next()
var total int64
rows.Scan(&total)</p><p>// 分页主查询(加 LIMIT/OFFSET)
pageSQL := stmt.SQL.String() + " ORDER BY u.id LIMIT ? OFFSET ?"
var results []UserOrderSummary
db.Raw(pageSQL, stmt.Vars..., 20, (page-1)*20).Scan(&results)
-
stmt.Vars...必须原样传给 COUNT 和分页查询,否则参数错位 - MySQL 要确保子查询有别名(
AS count_table),PostgreSQL 可省略 - ORDER BY 必须显式声明,否则
LIMIT/OFFSET行为不确定
混合模式下复用 ORM 查询条件生成原生 COUNT
当主体是 ORM 链式调用(如 db.Where().Joins()),但需要嵌入一段原生字段(如 JSON_EXTRACT、自定义函数),可用 Scopes + Count() 组合,再用 SubQuery() 包装原生部分。
例如:查 status=1 且 JSON 字段中 tags 包含 "vip" 的用户数
subQuery := db.Table("users").Select("id").Where("status = ? AND JSON_CONTAINS(metadata, '\"vip\"', '$.tags')", 1)
total := int64(0)
db.Model(&User{}).Where("id IN (?)", subQuery).Count(&total).Error
<p>// 分页主查(同样用 subQuery,但加 LIMIT/OFFSET)
var users []User
db.Table("users").
Where("id IN (?)", subQuery).
Limit(20).Offset((page-1)*20).
Find(&users)
- 避免在
WHERE中直接写JSON_CONTAINS(...)然后调Count()—— GORM 无法解析该表达式,Count()会忽略它 -
subQuery必须是*gorm.DB类型,不能是字符串 - MySQL 8.0+ 支持 JSON 函数索引,但 COUNT 仍走全表扫描,注意性能
分页参数绑定顺序错乱导致 OFFSET 跳行
原生 SQL 中 ? 占位符顺序必须和 Vars 严格一致,一旦混入 ORM 条件(如 Where("id > ?", id))再拼接原生 SQL,参数顺序极易错乱。
- 错误写法:
db.Raw("SELECT * FROM x WHERE a = ? AND "+customCond, val1).Where("b = ?", val2)→ 两个?顺序不可控 - 正确做法:所有条件统一进
Raw(),用map[string]interface{}或明确按序传参 - 调试技巧:开启 GORM 日志(
db.Debug()),看实际执行的 SQL 和 args 是否匹配 - PostgreSQL 对
OFFSET值为负数或非整数会直接报错,前端传参务必校验
混合分页没有银弹。最稳的路径是:原生部分尽量收敛到单个 Raw(),ORM 部分用 Scopes 封装可复用条件,COUNT 和主查共享同一组参数和逻辑结构——任何试图“半 ORM 半原生”自由拼接的地方,都是 offset 错位和 count 失准的高发区。


















