lazycollection 通过生成器实现流式处理,避免全量数据驻留内存;cursor()跳过模型实例化更省内存,lazy()支持完整模型功能但内存开销大;复杂查询应选chunkbyid();count()、toarray()等操作会破坏惰性。

当数据库查询返回数万甚至百万级记录时,Laravel 8 默认的 get() 会一次性将全部结果加载进 PHP 内存、实例化为模型对象、建立关系引用——这极易触发内存溢出(OOM),导致请求失败或服务卡顿。
LazyCollection 不是“懒加载关联”,而是“懒加载数据流”
很多人误以为 LazyCollection 是用来替代 load() 或解决 N+1 查询的,其实完全不是。它不处理模型关联逻辑,只解决一个核心问题:不让全量数据同时驻留内存。
普通 Collection 是数组容器,所有项在构造时就已存在;LazyCollection 是生成器封装,每次 foreach 迭代才从源头(数据库游标、文件句柄、自定义生成器)取一条数据,用完即释放 PHP 引用。
这意味着:处理 100 万条用户记录时,内存占用可能稳定在 8MB 左右,而不是飙升到 2GB+。
为什么 cursor() 比 lazy() 更省内存
【cursor() 绕过 Eloquent 模型实例化】 它直接从 PDO 获取 stdClass 或关联数组,跳过属性赋值、访问器调用、事件触发、类型转换等开销。
例如:User::orderBy('id')->cursor()->each(fn($row) => $row->email) 中的 $row 是原生对象,没有 $row->posts,也不能调用 $row->save()。
而 lazy() 仍会为每一行创建完整 User 模型实例,支持 $user->name、$user->posts、$user->is_active 等全部模型能力,代价是每行多占用 3–5KB 内存。
如果你只做字段提取、统计、导出 CSV,选 cursor();如果要批量调用模型方法或保存修改,才用 lazy()。
真正起效的前提:必须从查询阶段控制,不能事后包装
第一步:确认 Laravel 版本 ≥ 8.29(cursor() 引入时间)。
第二步:确保目标表主键(如 id)有索引且单调递增,否则游标分页会漏数据或重复读取。
第三步:显式写 orderBy('id'),禁用 orderByRaw() 或复杂表达式排序——cursor() 依赖确定性顺序。
第四步:直接调用 cursor() 或 lazy(),【绝不要对 Model::get() 结果再套 LazyCollection::make()】。因为 get() 已把全部数据加载进内存,此时再“懒”毫无意义,反而增加对象封装开销。
遇到复杂查询时,chunkById() 是更安全的选择
方法一:当查询含 join、groupBy、whereHas() 或自定义 selectRaw() 时,cursor() 会抛出 NotSupportedException。
方法二:改用 chunkById(1000, fn($users) => $users->each(...)),它基于主键分段,兼容绝大多数 where 条件,还能配合 with() 分批预加载关联模型。
注意:它内部仍用 OFFSET/LIMIT 变体,性能略低于 cursor(),但远优于 get(),且不会漏数据。
哪些操作会立刻破坏惰性
① 调用 count()、sum()、max():必须遍历全部数据才能得出结果,生成器被耗尽。
② 执行 toArray()、all()、values():强制将所有项转为数组,内存瞬间暴涨。
③ 使用 sortBy()、groupBy():需随机访问或分组聚合,底层会先收集全部数据再处理,等价于 toArray() + 普通 Collection。
安全操作只有:filter()、map()、skip()、take()、each()、chunk()(指 Collection 的 chunk 方法,非查询构建器的 chunkById)——它们保持流式,按需触发 fetch。











