Eager Loading 指定列减少内存
解读
国内高并发业务(电商秒杀、CMS 列表页、SaaS 报表)中,ORM 查询最容易被批的就是“N+1”。Laravel 用 with() 做预加载,但很多候选人只答“用 with 就行”,却忽略了 with() 默认 select * 会把整行数据拉进 PHP 内存;当关联表字段多、记录数大(如订单明细表 60 个字段、200 万行)时,一次请求就可能吃掉几百兆内存,FPM 进程被 OOM killer 干掉,接口 502。面试官问“怎么在 Eager Loading 里减少内存”,就是在考察:
- 是否知道 with() 可以约束列;
- 是否理解约束列的语法细节(主键、外键必须保留,否则会出现“null”关联或重复查询);
- 是否能在业务层评估字段必要性,把内存占用量化到 MB 级别;
- 是否了解 cursor()、chunk()、只读数组、序列化字段等配套手段,形成“内存-网络-CPU”三位一体的优化闭环。
知识点
- Laravel Eloquent 关联预加载原理:
- 第一次查主表;
- 收集外键集合;
- 第二次用 whereIn 一次性查关联表;
- 在内存中做 collect()->map() 匹配。
- with() 的“列约束”语法:
with(['posts:id,user_id,title,created_at']) // 必须带 id(主键)与 user_id(外键) - 内存估算公式:
单行字节 ≈ 字段长度和 + 56 字节 Zend 数组开销;
总内存 = 行数 × 单行字节 × 1.5(Zend 扩容系数)。
例:60 个字段平均 200 字节 → 单行 256 字节;1 万行 ≈ 3.8 MB;若只取 4 个字段 40 字节 → 0.6 MB,降低 6 倍。 - 主键/外键缺失导致的隐性 N+1:
如果省略 user_id,Laravel 无法把 posts 匹配到 users,会触发 fallback 查询,反而增加内存与 SQL 次数。 - 配套内存优化:
- cursor() 替代 get(),使用生成器;
- chunkById() 分段;
- 只读数组:$casts = ['*' => 'float'] 关闭模型同步;
- 关闭反序列化:$with = [],避免加载冗余 relation;
- OPcache 保存模型结构,减少对象头。
- 国内主流框架版本差异:
- Laravel ≥ 5.6 支持 with() 指定列;
- ThinkPHP 6 使用 with('profile')->field('id,name') 语法不同,但原理一致;
- Hyperf 协程态下内存常驻,更需严格控制列。
- 线上监控:
在 Telescope、OneAPM、阿里云 ARMS 中可查看 Memory Peak;面试时可举例“把 230 MB 峰值压到 45 MB,QPS 提升 18%”。
答案
“在 Laravel 里,用 Eager Loading 指定列可以显著降低内存,核心口诀是‘必须带主键、必须带外键、只拿业务需要的字段’。具体分四步:
第一步,确定关联。假设要查用户及其最新 3 篇文章,关联名是 posts。
第二步,写 with 约束列,代码示例:
$users = User::query()
->select('id', 'name', 'avatar') // 主表只拿 3 列
->with(['posts:id,user_id,title,summary']) // 关联表保留主键 id、外键 user_id,再拿业务字段 title、summary
->paginate(20);
第三步,验证 SQL:
- 第一条:select id,name,avatar from users limit 20 offset 0;
- 第二条:select id,user_id,title,summary from posts where user_id in (…20 个 id…);
通过 explain 确认两条语句都用到索引,rows 扫描量下降。
第四步,压测内存:
本地 PHP_CLI 内存限制 128 M,ab -c 50 -n 1000 压测,开启 memory_get_peak_usage();
对比 select * 峰值 230 MB → 指定列后 45 MB,QPS 从 420 提升到 495,FPM 进程数减少 30%。
如果数据量更大,可再叠加 cursor() 分块:
User::cursor()->filter(...)->chunk(500, fn($cs) => $cs->load(['posts:id,user_id,title']));
这样既利用 Eager Loading 避免 N+1,又通过指定列把内存压到最低,线上 502 告警清零。”
拓展思考
- 字段黑名单机制:
当表字段超过 100 个且经常变动时,可维护一个“敏感字段列表”(如 text 类型 content),用 Model::getHidden() 动态追加,避免每次改代码。 - 分布式场景:
在微服务拆分后,用户服务只返回 open_api_users 视图(含 5 个字段),订单服务通过 RPC 批量拿用户,需要约定“最小字段契约”,防止网络包膨胀。 - 只读 DTO:
用 PHP 8.1 readonly 类或 Spatie Data 包,把查询结果直接映射成不可变对象,关闭 ORM 模型同步,可再省 20% 内存。 - 协程常驻:
Hyperf/Swoole 下内存常驻,Eager Loading 指定列后仍需手动 unset 大数组,否则 Worker 内存只增不减;可结合 MaxRequest=5000 定期重启。 - 面试反向提问:
当面试官认可答案后,可以主动追问“贵司日均订单量多少?MySQL 网卡打满还是 CPU 打满?”体现性能调优闭环思维,加分。