登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  php教程

PHP Generators 怎样把数据库结果转成惰性序列

来源:17golang原创

时间:2026-11-02 10:24:56 496浏览 收藏

PHP Generators 可以在每次读取数据库一行后 yield,让调用方通过 foreach按需消费,不必先调用 fetchAll()。但生成器的惰性与数据库驱动的结果缓冲是两层机制:要降低大结果集的客户端缓冲占用,还需选择适合的查询模式,并管理游标释放。

官方地址:https://www.php.net/

先约定这是一次性的行序列

假设 CLI 导出任务需要按商品 ID 顺序读取记录并逐行输出。接口应承诺“依次提供行”,而不是“返回可随意索引、计数和重放的集合”。生成器概述说明,它能为循环逐个提供值,无须预先构建完整数组,也不提供随机访问。

接口约定本文的选择
输入已经执行的 PDOStatement
输出每次一行关联数组
查询何时执行由调用方显式 execute
游标由谁释放创建查询的调用方
是否可重复遍历按单次消费设计,需要重读则重新查询

这个划分刻意让 SQL 执行与序列读取分开。生成器函数被调用时返回对象,函数体在迭代时推进;由于本文将 execute()放在外面,SQL 本身不会等到第一次 foreach才执行。这样调用方能清楚地知道何时开始占用连接。

用 fetch 和 yield 实现读取接口

保存下面函数,随后接上连接与消费代码。它只负责将行读取适配为序列,不接管语句的执行、重试或释放。

fetch(PDO::FETCH_ASSOC)) !== false) {
        yield $row; // 当前行交给调用方,下一次迭代才继续读取。
    }
}

fetch 文档定义了关联数组读取模式及 false返回条件。配合异常错误模式,数据库读取故障会作为异常传播,而不是被业务代码当成一个普通行处理。

不要在函数内先 fetchAll(),再遍历那个数组并 yield:虽然接口形式仍是 Generator,完整结果数组已经存在,无法得到预期的应用层内存收益。生成器语法中暂停和恢复的是函数状态,并不会撤销先前已分配的内存。

已执行语句、逐行读取接口与消费者之间的静态依赖
图1:惰性行接口结构说明图。生成器关联逐行读取与当前行,消费者通过迭代使用它;连线不是查询执行时序。

让调用方管理非缓冲查询与释放

MySQL 缓冲说明指出,默认缓冲模式会将结果传到客户端保存;非缓冲模式减少这一层内存占用,但结果未取完时同一连接不能继续发送其他查询。因此适合顺序处理大量行的专用读取连接,不能直接替换所有业务连接的配置。

下面假定已有 products表,含 id与 sku字段,id是可排序且在 PHP 整数范围内的正整数。环境变量保存 MySQL DSN、用户与密码,DSN 应指定正确字符集。本文没有执行数据库或输出真实商品数据。

// 与 rowSequence 放在同一 PHP CLI 文件中。
$dsn = getenv('DB_DSN');
$user = getenv('DB_USER');
$password = getenv('DB_PASSWORD');
if ($dsn === false || $user === false || $password === false
    || !str_starts_with($dsn, 'mysql:')) {
    throw new RuntimeException('缺少有效的 MySQL 连接配置');
}

// 这是专门用于顺序读取的连接;不要在消费循环里用它做其他查询。
$pdo = new PDO($dsn, $user, $password, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_EMULATE_PREPARES => false,
    PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false,
]);

$statement = $pdo->prepare(
    'SELECT id, sku FROM products WHERE id > :after_id ORDER BY id'
);
$rows = null;
try {
    $statement->bindValue(':after_id', 0, PDO::PARAM_INT);
    $statement->execute();
    $rows = rowSequence($statement);

    foreach ($rows as $row) {
        // 逐行编码,不把记录追加到一个不断增长的数组。
        echo json_encode(
            $row,
            JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
        ), PHP_EOL;
        // 业务需要时可 break;外层 finally 仍负责释放游标。
    }
} finally {
    // rowSequence 不拥有游标:释放义务明确属于这一层。
    unset($rows);
    $statement->closeCursor();
}

这里没有将列值强制转整数或浮点数,避免把商品编码、超大编号等业务值误改。消费逻辑也可以替换成写文件或批量发送,但应限制下游队列长度;如果输出缓冲一直累积内容,整条链路仍可能占用大量内存。

closeCursor 文档说明它释放连接以便执行其他 SQL。调用可能需要处理剩余结果,不能将其理解成“立刻取消服务器端工作”或承诺零耗时。若只需要有限行,应尽可能在 SQL 中限定范围,而不是读取开头几行后让剩余结果等待清理。

专用连接、结果游标、生成器引用与 finally 释放的资源归属
图2:游标资源归属说明图。调用方拥有专用连接和结果游标,并通过 finally 清理;生成器只是借用语句的读取接口。

把异常、提前结束与兼容性写进契约

查询执行异常发生在 execute()处;读取异常可能发生在迭代中;编码或下游写入也可能失败。因此异常处理应包围整个消费范围。示例让异常继续交给 CLI 入口处理,入口可记录诊断并设置失败退出码,不应对外输出连接凭据。

break只停止当前循环,不保证一个仍被引用的生成器立刻销毁。本文在调用方的 finally显式关闭游标,清理不依赖生成器析构。rowSequence的对象不得逃逸到这个作用域之外,游标关闭后不应继续恢复它。

调用方需要相应设计
一次顺序导出专用非缓冲连接加逐行消费
循环里查询关联信息改写查询或预先准备关联数据;必要时使用独立连接
可恢复的长任务另设计有界批次与进度记录,不把活游标当持久进度
随机访问、重复排序由数据库处理,或在明确内存预算内物化小集合

非缓冲读取只约束客户端取结果的方式,不保证数据库不排序、不使用临时空间,也不提供跨多个查询的一致快照。连接、事务、查询计划与业务一致性要求仍要独立设计。

返回 Generator 后内存就与行数完全无关了吗?

不能这样承诺。还要计算单行大小、驱动缓冲、下游累积、日志和输出缓冲。逐行接口避免的是“为了遍历而预先收集全部行”。

可以对这个结果直接 count 或遍历两次吗?

应按单次序列使用。需要总数时另写合适的计数查询,并考虑一致性;需要再次遍历时重新创建查询及序列,不把已消费对象当成数组。

iterator_to_array 会保留惰性吗?

不会。它把遍历结果重新收集为数组。小结果集可以明确选择物化,大导出则应保持逐行消费或只建立有界批次。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>