LazyCollection 不能通过包装 Model::get() 省内存,必须用 cursor() 或 chunkById() 在查询阶段控制;前者基于游标逐批取原始数据,后者兼容复杂查询并支持预加载。

直接说结论:LazyCollection 不是给 Model::get() 包一层就能省内存的,真要降内存,必须从查询执行阶段就控制——用 cursor() 或 chunkById(),而不是事后包装。
为什么 LazyCollection::make(Model::get()) 反而更耗内存
因为 Model::get() 已经把全部结果查出来、实例化成模型、塞进 PHP 数组了。此时内存峰值早已打满,再套个 LazyCollection,只是对一个已加载完的大数组做“假装懒”的迭代,还多了一层对象开销。
-
Model::get()→ 执行 SQL + 全量 hydrate + 全部存入内存 → 内存爆了才开始遍历 -
LazyCollection::make(Model::get())→ 先爆内存,再包装 → 毫无意义 - 真正惰性必须让 PDO 逐批 fetch,而不是等数据全在 PHP 里了才“懒”
cursor() 是什么,以及它为什么能恒定内存
cursor() 是 Laravel 8.29+ 提供的底层惰性方案,它不依赖 OFFSET/LIMIT,而是基于主键(如 id)游标分批读取,默认每次 fetch 1000 行,边读边实例化、用完即释放引用。
- 要求表有带索引的单调字段(
id或created_at),否则会漏或重复 - 不能用
orderByRaw()或复杂排序;显式写orderBy('id')最稳 -
cursor()返回原始数组(stdClass),不走模型构造逻辑,内存更低、速度更快 - 示例:
User::orderBy('id')->cursor()->each(fn($row) => process($row));
lazy() 和 cursor() 的关键区别在哪
两者都返回 LazyCollection,但行为完全不同:
-
cursor()→ 绕过 Eloquent 模型,只取原始数据,适合导出、统计、清洗字段 -
lazy()→ 仍为每行创建完整模型实例,支持访问器、关系、save()等,适合需要复用模型逻辑的场景 - 链式调用中,
sortBy()、groupBy()、toArray()、count()会提前耗尽生成器,失去惰性优势 - 安全操作包括:
filter()、map()、skip()、take()、chunk()
什么时候该用 chunkById() 而不是 cursor()
当你用了 join、groupBy、whereHas() 或其他导致 cursor() 报 NotSupportedException 的复杂查询时,chunkById() 是更兼容的选择。
- 它基于主键分段,支持绝大多数 where 条件,且能配合
with()预加载关联(分批预加载) - 性能略低于
cursor()(因需维护 offset + limit + 主键范围),但远优于get() - 示例:
User::where('status', 'active')->chunkById(1000, fn($users) => $users->each(...)); - 注意:不能用
orderBy自定义字段,它内部固定按id排序分片
最容易被忽略的一点:cursor() 不支持 with(),所以如果你需要关联数据,要么改用 chunkById() 分批预加载,要么在 cursor() 后手动查关联——后者得自己控制 N+1 风险。别指望“又懒又全”。











