关联字段必须显式裁剪,主模型field()对关联表无效;生效方式为关联方法中固化裁剪或运行时闭包控制;外键与查询字段须建索引;避免n+1用withcount或join;sql日志与explain必须开启。

关联字段必须显式裁剪
默认查全部字段是性能杀手,尤其当关联表含大文本、JSON或二进制字段时。主模型的 field() 对关联表完全无效,写在 with('profile')->field(...) 后面会被忽略。
真正生效的方式只有两种:
- 在关联方法定义里固化裁剪,例如:
public function profile() { return $this->hasOne(Profile::class)->field(['id', 'user_id', 'nickname', 'avatar']); } —— 注意 user_id(外键)和 id(主键)必须包含,否则数据绑定失败,返回空数组 - 运行时用闭包动态控制:
User::with(['profile' => fn($q) => $q->field(['user_id', 'nickname'])])->find(123) —— 每个关联需独立写闭包,不可复用同一 $query 实例
外键与查询条件字段必须建索引
没索引的外键会让 JOIN 或子查询退化为全表扫描,EXPLAIN 中 type=ALL 就是明确警告。ThinkPHP 不会自动建索引,得手动补上。
常见索引策略:
- 单外键索引:如 profiles.user_id、orders.user_id 必须单独建 B+ 树索引
- 联合索引优先:若常查 where user_id = ? and status = ?,应建 idx_user_status (user_id, status),而非两个单列索引
- whereHas 场景:如 whereHas('posts', fn($q) => $q->where('status', 1)),则 posts 表需有 (user_id, status) 联合索引
建完后执行 SHOW INDEX FROM profiles 确认索引存在,再跑 EXPLAIN 验证 type 是否变为 ref 或 eq_ref。
避免 N+1,善用 withCount 和 JOIN
用 $user->posts()->count() 是典型 N+1,100 个用户就发 100 次 COUNT 查询。正确做法是:
- 简单统计一律用 withCount():
User::withCount(['posts' => fn($q) => $q->where('status', 1)])->select() —— 生成一条带子查询或 LEFT JOIN 的 SQL,返回 posts_count 属性 - 多条件或分组统计,手写 JOIN 更可控:
UserModel::field('user.*, COUNT(post.id) as post_count')
->join('post', 'post.user_id = user.id', 'LEFT')
->group('user.id')
->select() —— 注意必须显式写 field(),不能用 * - 千万级关联表且外键无索引时,withCount 可能比批量 ID 查 COUNT 还慢,此时应切回「先取主表 ID 列表 → 分批查子表 COUNT」策略
SQL 日志与执行计划必须开起来
不看真实 SQL 和 EXPLAIN,所有优化都是拍脑袋。TP8.0 要三步齐开:
- APP_DEBUG=true(.env 中等号无空格),并清空 runtime/ 目录
- 在 config/log.php 的 'level' 数组中加入 'sql'
- 确认 config/database.php 中 'trigger_sql' => true(默认开启,但自定义配置常被删)
验证是否生效:执行 User::with('profile')->find(1) 后,立刻 dump(User::getLastSql());若为空,说明某步失效。更可靠的是查 runtime/log/sql/ 下最新日志,里面含完整 SQL 和 EXPLAIN 结果(需开启 'sql_explain' => true)。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











