子查询必须用SubQuery构造,不能拼SQL字符串写Where("id IN (SELECT...)"),否则绕过参数绑定导致SQL注入;正确做法是先建*gorm.DB子查询对象,再用"(?)"占位符注入主查询。

子查询必须用 SubQuery 构造,不能拼 SQL 字符串
直接写 Where("id IN (SELECT ...)") 看似省事,但会绕过 GORM 的参数绑定,导致 SQL 注入。GORM v2 明确不支持这种原始字符串嵌套。
正确做法是先构建一个独立的 *gorm.DB 子查询对象,再用 (?) 占位符注入主查询:
subQuery := db.Table("logins").Select("user_id").Where("created_at > ?", weekAgo)- 主查询中写成:
db.Where("id NOT IN (?)", subQuery)—— 注意括号(?)是强制语法,不是可选 - 若子查询含
GROUP BY或HAVING,也必须在subQuery阶段完成,不能拖到主查询里加
多层嵌套子查询要分步构造,避免 Scopes 内部新建 db
比如“查被 ≥3 个不同管理员标记为高危的用户”,需要两层子查询:第一层查高危标记记录并按 user_id 分组计数,第二层筛选计数 ≥3 的 user_id。
常见错误是试图在一个 Scope 函数里用 gorm.Open(...) 新建 db 实例,或返回非 *gorm.DB 类型;这会导致链式调用中断、条件丢失。
- 正确方式:把每层子查询拆成独立变量,逐层传入,例如
sub1 := db.Table(...).Group(...).Having(...),再用sub2 := db.Table("users").Where("id IN (?)", sub1) - 所有中间步骤都必须基于原始
db(或其派生),不能db.Session()后不返回 - 如果逻辑复用频繁,用闭包封装整个子查询链:
func WithHighRiskUsers(threshold int) func(*gorm.DB) *gorm.DB
子查询放在 WHERE 还是 FROM,性能差异极大
MySQL 对 WHERE ... IN (subquery) 有一定优化能力,可能自动转为 JOIN;但 PostgreSQL 更严格,大结果集下容易触发全表扫描。
当子查询结果集超过几千行,或需多次复用时,应考虑改用 FROM 子句(即派生表):
- 用
Joins("(?) AS risky_users ON users.id = risky_users.user_id", subQuery)替代Where("id IN (?)", subQuery) - 此时子查询必须明确
Select("user_id"),否则字段名冲突或缺失 - 执行前务必用
db.Session(&gorm.Session{DryRun: true}).Where(...).Find(&[]any{})打印 SQL,再用EXPLAIN验证执行计划
Unscoped 会影响子查询,软删除场景必须显式处理
如果子查询涉及带 gorm.DeletedAt 的模型(如 logins 表也启用了软删除),默认情况下 db.Table("logins") 会自动加 WHERE deleted_at IS NULL,导致“从未登录用户”误判为“最近没登录用户”。
- 子查询中需显式调用
db.Unscoped().Table("logins"),否则过滤逻辑失效 - 但注意:主查询若不需要软删除数据,不要对主表也
Unscoped,避免污染业务语义 - 更安全的做法是统一在子查询 Scope 中封装:
func WithActiveLogins() func(*gorm.DB) *gorm.DB,内部处理Unscoped和时间范围
子查询真正难的不是写法,而是判断它是否该存在——一旦你发现自己在 Where 链里反复加 AND EXISTS、或 Joins 后要对关联表字段做聚合再反向过滤主表,那基本就是子查询该出场的时候了。











