Hyperf的with()能将N+1压至2次SQL,因它先查主模型再用IN批量拉关联数据;load()是懒加载,遍历100条即触发100次查询。with()须在get()前调用、嵌套需分层显式声明(如'author.profile'仅当author为一对一时有效)、传参不加括号、闭包中select()有效但whereHas()无效,且必须包含外键字段并建索引。

Hyperf 的 with() 是解决 N+1 的唯一开箱即用方式,但错一个参数、漏一个外键、嵌套写法不对,就等于没写。
为什么 with() 能压到 2 次 SQL,而 load() 仍是 N+1
with() 在主查询执行前就收集所有主模型的 ID,再用一条 WHERE id IN (...) 批量拉取关联数据;load() 是懒加载,每访问一次未加载的关联属性,就单独发一次 SQL——遍历 100 条记录,就是额外 100 次查询。
- 主模型查完后调
->load('author'),本质还是 N+1,只是延迟触发 -
with()必须写在get()、paginate()等终态方法之前,链式调用才生效 -
load()只适合已有模型实例(比如从缓存读出 ID 后补查),不能替代设计阶段的预加载
with() 嵌套写法必须显式分层,且上级必须是一对一
Hyperf 不支持自动递归解析点号路径,with('author.profile') 会静默失败,除非 author() 返回的是单个模型(hasOne 或 belongsTo),否则中间层无法继续调用 profile()。
- ✅ 正确:
Post::with(['author', 'author.profile', 'comments.user'])->get() - ❌ 危险:
Post::with(['author.profile'])—— 若author()是hasMany,profile根本不会加载 - ⚠️ 注意:
comments.user安全,因为comments()是集合,但user()是一对一,Hyperf 能逐条映射 - 禁止写
with(['author.posts.comments'])这类三级以上长链,极易因中间模型无对应关系或外键为空导致空指针
with() 传参不加括号,闭包里不能写 get() 或 whereHas()
with() 接收的是关系名字符串或数组,不是函数调用;传入闭包时,Hyperf 仅识别 select()、orderBy() 等构建器方法,其他逻辑会被忽略。
- ❌ 错误:
->with('author()')、->with(['author()'])—— 括号被当字面量,找不到方法 - ✅ 正确:
->with('author')、->with(['author', 'tags']) - ✅ 闭包可用:
->with(['author' => function ($q) { $q->select(['id', 'name', 'avatar']); }]),但外键字段(如user_id)不能漏 - ❌ 闭包里写
$q->whereHas('xxx')或$q->get():不报错,但条件完全不生效
字段精简和外键索引是隐性性能瓶颈
with() 默认对关联表执行 SELECT *,若主查询只取 id, name,关联却拉回几十个字段,内存和网络开销会指数级上升;更关键的是,IN 查询若无索引,本身就会变慢,with() 反而放大延迟。
- 主模型用了
select(),关联也必须同步限制字段:User::select('id','name')->with(['profile' => fn($q) => $q->select('id','user_id','avatar')])->get() - 外键字段(如
user_id、post_id)必须包含在select()中,否则 Hyperf 无法绑定数据 - 检查数据库:所有外键列(
posts.user_id、comments.post_id)必须有 B-tree 索引,否则IN查询会全表扫描 - 大批量数据(>5000 行)别用
get()+with(),改用chunkById(500)分批,避免 OOM
最常被忽略的点:嵌套预加载是否真生效,不能只看代码写得对不对,得看日志里实际发了几条 SQL、每条的 IN 列表长度是否合理——有时候模型关系定义错了,with() 就是静默跳过,你根本不知道它没干活。











