必须强制分批并启用iterate()+$em->clear()+非缓冲查询,否则单次查询或循环累积即导致OOM;禁用getIterator()、懒加载和字符串拼接,改用生成器与流式输出。

FrankenPHP 导入大量数据必须强制分批,否则 memory_limit 很快耗尽,且非缓冲查询未启用时,单次 SELECT 就可能吃掉几百 MB 内存。关键不是“能不能分”,而是“怎么分才不崩”。
用 iterate() + $em->clear() 绕过 Doctrine 实体缓存
Doctrine 默认把所有结果加载进 EntityManager 一级缓存,几万行就撑爆内存。必须用原生数组流,彻底跳过对象构建:
-
iterate()返回的是[$row]数组,不是实体对象;getIterator()仍会全量加载实体,禁用 - 每次循环后必须调用
$em->clear(),否则后续persist()或关联查询会复用残留实体,内存持续累积 - 禁止在循环里调用
$em->find()、$user->getOrders()等懒加载,这会触发全表扫描 - 示例写法:
foreach ($em->createQuery('SELECT u.id, u.email FROM User u')->iterate() as [$id, $email]) { $em->persist(new ImportLog($id, $email)); if ($i++ % 1000 === 0) { $em->flush(); $em->clear(); // 清空所有已托管对象 } }
MySQLi/PDO 必须设为非缓冲查询
默认的缓冲查询会把整个结果集一次性读进 PHP 内存,和分批无关。必须显式切换为流式吐出:
- MySQLi:用
mysqli_use_result()替代mysqli_store_result(),再配合mysqli_fetch_assoc() - PDO:创建
PDOStatement时传入[PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false],然后foreach ($stmt as $row) - 注意:非缓冲查询期间不能执行其他 SQL,查完立刻
$mysqli->close()或置$pdo = null,否则连接泄漏
避免在循环中拼接大字符串或累积数组
常见低级错误是 $rows[] = $row 或 $csv .= "$id,$email\n",10k 行后内存直接翻倍:
立即学习“PHP免费学习笔记(深入)”;
- 导出 CSV/JSON:用
fopen()+fwrite()直接写文件或响应流,不拼接完整字符串 - 必须暂存中间数据:设硬性上限,如
if (count($batch) >= 500) { processBatch($batch); $batch = []; } - 优先用生成器函数封装逻辑:
function fetchUsersAsGenerator() { /* yield $row */ },调用方用foreach (fetchUsersAsGenerator() as $row)
确认 OPcache 和 Symfony 缓存没因大查询失效
大查询本身不触发重编译,但若逻辑写在控制器里、又没预热,或用了动态 SQL 拼接(如 "WHERE id IN (" . implode(',', $ids) . ")"),会导致每次请求都重建容器、重解析模板:
- 把数据导入逻辑抽成独立命令(
php bin/console app:import:users),避免走 HTTP 请求生命周期 - 禁用开发环境的
.env自动加载,Worker 模式下环境变量必须通过ENV文件或 Dockerenvironment注入 - 运行前清空缓存:
php bin/console cache:clear --no-warmup && php bin/console doctrine:cache:clear-metadata
最易被忽略的是:即使用了 iterate() 和非缓冲查询,如果忘了 $em->clear() 或在循环里调了任意 ORM 方法,内存还是会线性增长——这不是配置问题,是代码路径没切断。



















