别无脑用chunk(),它在多数大数据场景下反而让内存越查越高、漏数据、卡死;真正靠谱的分块方式取决于你「要不要模型实例」和「主键是不是自增整型」。

直接说结论:别无脑用 chunk(),它在多数大数据场景下反而让内存越查越高、漏数据、卡死;真正靠谱的分块方式取决于你「要不要模型实例」和「主键是不是自增整型」。
chunk() 为什么内存不降反升
它只是把 get() 拆成多次执行,但每次返回的仍是完整 Eloquent 模型对象,且全部缓存在静态属性 Model::$instances 中。PHP GC 清不掉这些引用,哪怕你没显式保存变量。
- 每批回调里的
$users是User模型集合,每个实例都带访问器、关系、事件监听器 - 加了
->withTrashed()或->withoutGlobalScopes()后构造逻辑更重,缓存更难释放 - 解决办法不是调大 chunk 尺寸,而是绕过模型层:
DB::table('users')->select('id', 'name', 'email')->chunk(500, function ($rows) { })—— 返回原生数组,无实例开销
chunkById() 必须满足的三个前提
它靠 WHERE id > ? 推进,比 chunk() 稳定,但极易踩坑:
- 主键必须是自增整数(
INT或BIGINT),不能是 UUID、字符串或复合主键 - 表里不能有
id为NULL的行,否则WHERE id > last_id会漏掉 - 处理过程中不能有新记录插入到当前批次范围中间(比如并发写入
id=1005,而上一批停在1000),否则会跳过 - 安全用法示例:
User::where('status', 'active')->orderBy('id')->chunkById(1000, function ($users) { }, 'id')—— 注意必须显式写orderBy('id'),否则 Laravel 不保证顺序
真正低内存的分块:DB::table + cursor()
如果你只读字段、不做模型操作(比如导出 CSV、统计积分、同步 ID 列表),别碰 Eloquent:
-
DB::table('users')->cursor()返回可遍历游标,逐行取,内存恒定 ≈ 1KB,适合百万级 -
DB::table('users')->pluck('id')只取单列值,返回Collection,比get()轻 80% 以上 - 需要关联字段?用
leftJoin+selectRaw拼好再cursor(),别with():DB::table('users')->leftJoin('profiles', 'users.id', '=', 'profiles.user_id')->select('users.id', 'users.email', 'profiles.bio')->cursor()
漏数据和死循环的隐蔽原因
这不是 bug,是设计使然:
-
chunk()基于OFFSET + LIMIT,数据有增删就会跳行或重复 -
chunkById()遇到非单调主键(如 UUID、软删除干扰、deleted_at IS NOT NULL被跳过)时,可能漏数据或死循环 - MySQL 快照读机制决定:即使表在处理中被并发修改,你也只能看到事务开始那一刻的数据视图 —— 这是正常行为,不是框架缺陷
最易被忽略的一点:无论用哪个方法,只要涉及模型实例化,就绕不开 Model::$instances 缓存累积;想彻底省内存,就得接受「不走 Eloquent」这个代价。











