根本原因是MySQLi/PDO默认将全部结果集加载到PHP内存,而非数据量本身;需用MYSQLI_USE_RESULT或关闭PDO缓冲才能配合yield流式处理。

为什么直接 fetch_all() 会爆内存
PHP 进程被 Fatal error: Allowed memory size exhausted 杀掉,根本原因不是数据量大,而是你让 MySQLi 或 PDO 把整张结果集一次性加载进 PHP 用户空间。比如 mysqli_query($conn, "SELECT * FROM logs LIMIT 100000") 后调用 fetch_all(MYSQLI_ASSOC),哪怕只查 10 万行,每行 2KB 就是 200MB —— 这还没算 PHP 数组本身的内存开销。
生成器本身不解决这个问题;它只是“吐值”的方式。真正起作用的是配合底层流式查询机制。
- MySQLi 必须用
MYSQLI_USE_RESULT模式(非默认),否则仍会缓冲全部结果 - PDO 必须关掉缓冲:
$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false) - 不设这个属性,哪怕写了
yield,PDO 也会在execute()时就把所有行读进内存
MySQLi + yield 的正确写法
别用 fetch_all(),改用游标式逐行读取,再用 yield 吐出去:
function queryLargeTable(mysqli $conn): Generator
{
// 关键:用 MYSQLI_USE_RESULT,不缓存结果集
$result = $conn->query("SELECT id, name, email FROM users", MYSQLI_USE_RESULT);
if (!$result) {
throw new RuntimeException($conn->error);
}
<pre class="brush:php;toolbar:false;">while ($row = $result->fetch_assoc()) {
yield $row;
}
$result->free(); // 必须显式释放,否则连接可能卡住}
注意点:
-
MYSQLI_USE_RESULT要求你在读完前不能执行其他查询,否则会报错Commands out of sync - 必须调用
$result->free(),否则 MySQL 连接会被占用,后续查询可能失败 - 不要在生成器里做耗时操作(如发 HTTP 请求、写日志),否则会阻塞整个流
PDO 场景下最容易踩的坑
很多人以为只要写了 yield 就安全了,结果还是 OOM。问题出在 PDO 默认开启缓冲查询 —— 它会在 execute() 返回后立刻把所有结果捞进内存。
必须在创建 PDO 实例后、执行查询前设置属性:
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false, // ⚠️ 这行不能少
]);
<p>function pdoStreamUsers(PDO $pdo): Generator
{
$stmt = $pdo->prepare("SELECT id, title FROM articles WHERE status = ?");
$stmt->execute(['published']);</p><pre class="brush:php;toolbar:false;">while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
yield $row;
}}
常见错误:
- 把
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false写成true(这是默认值,等于没关) - 在
foreach (pdoStreamUsers($pdo) as $row)外层又套了个iterator_to_array(),瞬间吃光内存 - 用
fetchAll()获取元数据后再进生成器 —— 元数据本身也可能很大
比 yield 更关键的是分段逻辑
当表没有自增主键或 ID 不连续时,WHERE id > ? 分段会漏数据。而 LIMIT offset, size 在深分页(比如 OFFSET 500000)时,MySQL 仍要扫描前 50 万行,CPU 和磁盘 IO 并没省。
稳妥做法是结合游标 + 有序字段分段:
- 确保查询有确定顺序:
ORDER BY created_at DESC, id DESC - 每次记录上一批最后一条的
created_at和id,作为下一批的WHERE (created_at, id) - 这种复合游标能避免漏数、跳数,且索引可高效命中
生成器在这里只是管道,真正的稳定性取决于你如何切分数据流 —— 这部分逻辑必须由业务代码控制,不能全丢给 yield。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











