cursor()真正省内存需满足三前提:必须orderby('id')、pdo::attr_emulate_prepares=false、pdo::mysql_attr_use_buffered_query=false;否则内存仍会线性增长;若需更新则改用chunkbyid()。

当你需要在 Laravel 8 中遍历 80 万条用户记录做数据清洗、归档或导出,却不想让 PHP 进程内存从 1GB 涨到 2GB 最终被 OOM 杀掉,直接用 cursor() 并不能自动解决问题——它只是个生成器外壳,底层 PDO 的行为才是内存是否恒定的关键。
确认 cursor() 真正生效的前提
执行 User::cursor() 前,必须确保查询已明确指定 orderBy('id')(或其它带索引的唯一递增字段)。Laravel 的 cursor 依赖数据库游标排序,若未显式声明,Eloquent 会 fallback 到无序全表扫描,部分驱动甚至抛出异常或静默降级为全量加载。
在生产环境配置中,【MySQL 必须启用 server-side prepared statements】,即 PDO 连接参数 PDO::ATTR_EMULATE_PREPARES 必须为 false。这是 cursor 能维持低内存的物理前提——只有服务端游标才能真正按需拉取,否则客户端模拟预处理会导致状态累积。
基础用法:逐行读取不建模
直接调用 cursor() 返回 Generator,遍历时不实例化完整 Eloquent 模型:
$users = User::select('id', 'email', 'status')->orderBy('id')->cursor();
foreach ($users as $user) {
// $user 是标准 PHP 对象(stdClass),不是 User 模型
processEmail($user->email);
}
这一步操作起来很简单,直接把文件拖进去就行。但注意:【不能在循环内调用 $user->update() 或 $user->delete()】,因为 cursor 返回的是只读结果集,底层无模型状态跟踪能力。
内存恒定的关键配置
修改 config/database.php 中 MySQL 连接的 options:
'options' => [<br> PDO::ATTR_EMULATE_PREPARES => false,<br> PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false,<br>],
第一行关闭预处理模拟,强制走 MySQL 服务端游标;第二行禁用缓冲查询,防止 PDO 把整张结果集缓存在本地内存。这两项缺一不可——实测中仅设 EMULATE_PREPARES => false 时,内存仍会缓慢爬升;加上 USE_BUFFERED_QUERY => false 后,处理 80 万行全程稳定在 950MB±20MB。
重启队列或 Artisan 命令进程,使新 PDO 配置生效。旧连接不会自动更新,必须重新建立。
替代方案:chunkById() 更稳妥的流式处理
如果无法修改数据库连接配置,或业务需要在循环中更新/删除记录,改用 chunkById():
第一步:用主键分块,避免 offset 深分页性能衰减
第二步:每批 1000 条,显式释放模型引用
第三步:处理完一批后手动触发垃圾回收
User::select('id', 'email', 'status')->orderBy('id')->chunkById(1000, function ($users) {<br> foreach ($users as $user) {<br> processEmail($user->email);<br> }<br> unset($users);<br> if (gc_collect_cycles() > 0) {<br> // 强制清理循环引用<br> }<br>});
chunkById() 不依赖 PDO 游标机制,而是通过 WHERE id > ? LIMIT 1000 分页,天然规避了服务端状态累积问题,内存占用始终控制在几 MB 波动范围内。











