TP6.0中用yield生成器或chunk分页是处理万级以上数据的必选项,因select()等方法会一次性加载全部数据导致内存溢出;需避免模型实例化、显式指定字段,并确保每行处理后无引用残留。

直接说结论:TP6.0 里用 yield 写生成器查询,配合 chunk 或手动游标分页,能避免一次性加载全部结果到内存——这不是“可选优化”,而是处理万级以上数据时的必选项。
为什么 TP6.0 默认查询会吃光内存?
ThinkPHP6.0 的 select()、get() 或 all() 方法底层调用 PDO::fetchAll(),会把整张表所有记录一次性读入 PHP 数组,再实例化模型对象。哪怕只是 10 万条简单记录,每个对象占 2–3KB,轻松突破 256MB 内存限制。
- 错误现象:
Fatal error: Allowed memory size of XXX bytes exhausted - 常见场景:导出全量用户、同步日志表、ETL 清洗老数据
- 陷阱:用
limit(1000)->select()仍会把这 1000 条全加载进数组,不是流式;chunk方法虽好,但默认不支持自定义 SQL 或复杂关联
手写生成器函数:绕过 ORM 全量加载
核心是放弃 Db::table()->select(),改用原生 PDO 游标遍历,每次只 fetch 一行并 yield。TP6.0 的 Db 类支持 query 返回 PDOStatement,可直接控制读取粒度。
- 关键点:必须用
PDO::FETCH_ASSOC,避免模型实例化开销 - 示例片段:
function largeTableGenerator($sql, $params = []) {
$pdo = \think\facade\Db::getPdo();
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
yield $row; // 每次只返回一行,不累积
}
}
// 调用
foreach (largeTableGenerator('SELECT id, name FROM user WHERE status = ?', [1]) as $user) {
processUser($user); // 处理单行
}
- 注意:
$stmt->fetch()不会缓存历史行,内存恒定在 KB 级 - 不支持自动模型转换,需手动映射字段(但换来的是确定性内存占用)
chunk 方法的坑与安全用法
TP6.0 的 chunk 看似省事,但默认行为容易误踩内存雷:
-
Db::name('user')->chunk(500, function($users) { ... })中的$users是一个Collection对象,内部仍含 500 个模型实例 —— 若模型有大量属性或关联,单批仍可能爆内存 - 必须配合
withoutField和field显式指定字段,禁用自动关联加载 - 安全写法:
Db::name('user')
->field('id,name,email') // 只取必要字段
->where('status', 1)
->chunk(200, function($users) {
foreach ($users as $user) {
// 直接用 $user['id'],别调 $user->relation()
sendNotification($user['id'], $user['email']);
}
// 批处理完立刻 unset,防 GC 延迟
unset($users);
});
- chunk 的 offset/limit 是自动拼的,大数据量下 limit 越大越慢,建议控制在 100–500 行
最后提醒:生成器不是万能解药
生成器解决的是“数据读取阶段”的内存问题,但如果你在 yield 后做了 array_push($buffer, $row) 或缓存中间结果,内存照样涨。真正要盯住的,是每一行数据被消费后是否还有引用残留——比如闭包里捕获了整个 $row,或写入了一个全局数组。内存释放不靠魔法,靠明确的生命周期控制。


















