first 会隐式添加 order by 主键 asc 并 limit 1,语义为取主键升序第一条;take 不排序、无序返回任意一条,性能更高但结果不可预测;两者在 where 条件下差异显著,first 强制排序,take 仅取匹配集首行。

First 总是隐式加 ORDER BY 主键 ASC
当你调用 db.First(&u),GORM 会无条件生成 SELECT * FROM users ORDER BY id ASC LIMIT 1(假设主键是 id)。哪怕你没写 Order(),它也强制排序——这是语义决定的:「First」意味着“按默认顺序取第一条”,而 GORM 的默认顺序就是主键升序。
常见错误现象:db.Where("status = ?", "active").First(&u) 看似只查活跃用户,但实际执行的是带 ORDER BY id 的全表扫描+排序,数据量大时性能明显下降;如果你本意只是快速确认是否存在活跃用户,这纯属多余开销。
适用场景:
- 需要稳定、可预期的“最早创建记录”(比如取注册时间最早的用户)
- 业务逻辑依赖主键顺序,且主键是自增或时间戳型
Take 完全不加 ORDER BY,结果不可预测
db.Take(&u) 对应的 SQL 就是 SELECT * FROM users LIMIT 1,不排序、不保证顺序。MySQL 返回哪一行,取决于存储引擎的物理组织、索引覆盖、查询优化器选择等,每次执行都可能不同。
容易踩的坑:
- 误以为
Take是“随机取一条”——它不是随机,只是无序;真要随机需显式写Order("RAND()")或Order("RANDOM()")(SQLite) - 在有 WHERE 条件时,
db.Where("status = ?", "active").Take(&u)可能返回任意一条活跃用户,不能用于分页或幂等逻辑 - 测试环境数据少时看似稳定,上线后因数据分布变化导致行为漂移
WHERE 条件下两者差异被放大
带条件查询时,First 和 Take 的语义分歧最危险:
-
db.Where("email LIKE ?", "%@example.com").First(&u)→ 先过滤再按id排序取最小 ID 的那条 -
db.Where("email LIKE ?", "%@example.com").Take(&u)→ 过滤后直接取结果集第一行(引擎返回顺序)
如果该邮箱条件命中 10 万条,First 会强制对这 10 万条做排序(即使只取 1 条),而 Take 只要找到第一条匹配就停——但前提是索引能高效定位。若没有合适索引,两者都可能全表扫描,只是 First 多了排序成本。
性能提示:检查执行计划时,重点看 Extra 字段是否含 Using filesort —— 出现在 First 查询里基本就是它干的。
读写分离下它们都走从库,但事务内失效
GORM 的读写分离路由规则是方法名驱动的:First 和 Take 都属于读操作,默认发往 Replicas(从库)。这点很关键——很多人配了读写分离却没生效,就是因为没意识到 First 也算读操作。
但有一个例外必须记住:
- 一旦进入事务(
db.Transaction(...)),所有查询包括First和Take都强制走开启事务的那个主库连接 - 哪怕你在事务里只读不写,也无法切到从库;这是 GORM 的硬性设计,不是配置问题
所以如果你在事务中频繁调用 First 做存在性校验,又希望减轻主库压力,得手动用 db.Session(&gorm.Session{Read: true}).First(...) 显式声明读会话——但要注意,这不会脱离事务上下文,仍受限于事务隔离级别。











