ThinkPHP 6 的 with 预载入越用越慢,因其本质是改良版 N+1 查询:先查主表,再对每条记录发起完整关联查询,不支持跨表筛选、字段精简和条件下推;应优先使用 join + field 手动关联,或通过 field 限制、闭包约束、拆分嵌套、JSON 字段/懒加载等方式优化。

ThinkPHP 6 的 with 关联预载入为什么越用越慢?
因为默认是「N+1 查询」的改良版,不是真正的一次性 JOIN。它先查主表,再对每条记录单独发起关联查询——哪怕你只想要关联表的 1 个字段,也会把整张关联表 SELECT * 拉一遍。
- 用
with(['profile', 'posts'])时,profile和posts各自触发一次完整查询,且互相不复用 - 如果主表查出 100 条,
profile就执行 100 次单条查询(即使加了缓存也绕不开网络往返) -
with不支持跨关联表筛选(比如「查用户 + 且其 profile.sex = 'female'」),强行写会导致 SQL 错误或漏数据
改用 join + field 手动关联更可控
ThinkPHP 的 join 方法能真正合并为一条 SQL,配合 field 可精确控制字段,避免冗余传输和内存膨胀。
- 别写
$user->with('profile')->select(),改用:$users = Db::table('user') ->alias('u') ->join('profile p', 'u.id = p.user_id', 'LEFT') ->field('u.id,u.name,u.email,p.nickname,p.avatar') ->select(); - 多个关联时,每个
join都要显式指定别名和 ON 条件,不能依赖模型定义 - 注意字段冲突:如果两张表都有
id,必须用别名限定,如u.id as user_id - WHERE 条件里想过滤关联字段,得写在
join之后、select之前,例如where('p.status', 1)
with 还能救吗?加 scope 和 field 限制范围
如果必须用模型关联(比如要复用软删除、自动时间戳等逻辑),至少得砍掉默认行为里的“全量字段”和“无条件加载”。
- 在关联方法里加
field:在User模型中定义profile()关联时,末尾链式调用->field('user_id,nickname,avatar') - 用闭包约束条件:
$users = User::with(['profile' => function ($query) { $query->where('status', 1)->field('user_id,nickname'); }])->select(); - 慎用嵌套
with(如with(['profile.photos'])):三层以上会指数级放大查询次数,优先拆成两层 + 应用层组装 -
with不支持limit下推到子查询,分页时主表 limit 10,关联仍可能查出全部匹配记录
关联字段太多时,JSON 字段或应用层懒加载更实际
当某个关联表字段极多(比如日志详情、配置 JSON),硬 JOIN 或预载入会拖垮主查询响应时间和内存占用。
立即学习“PHP免费学习笔记(深入)”;
- 把非关键字段收进一个
json字段(如extra_info),主表只查 ID 和摘要,需要时再按需Db::table('profile')->where('user_id', $id)->value('extra_info') - 前端需要详情时,走独立 API 接口(如
/api/profile/123),而非塞进列表接口 - 模型里可定义访问器:
getDetailAttr,内部做单条延迟查询,但要注意避免在循环里触发(N+1 回归) - 别迷信“一次查完”,数据库连接数、网络带宽、PHP 内存都是真实瓶颈,拆比硬扛更稳
关联嵌套不是模型写得漂亮就万事大吉,TP 的 with 是语法糖,不是银弹;JOIN 要手写 ON 条件,但换来的是确定性;最容易被忽略的,其实是字段粒度——查 20 个字段还是 3 个,对慢查询的影响,往往比换 ORM 更直接。



















