hyperf 连表查询 oom 的根本原因是 pdo 默认启用缓冲查询,导致全量结果集一次性加载到内存;需禁用 pdo::mysql_attr_use_buffered_query 并手动 fetch 循环处理。

Hyperf 里连表查询 OOM,不是 SQL 写得不对,而是 PDO 默认把整张结果集全 load 到 PHP 内存里了——必须关缓冲、开游标、手动控制读取节奏。
为什么 Hyerf 的连表查询容易触发 PHP 内存溢出
Hyperf 默认用 PDO 驱动执行 SQL,而 PDO 的 PDO::MYSQL_ATTR_USE_BUFFERED_QUERY 默认为 true。这意味着:即使你只 fetch() 一行,PDO 也会在内部把全部结果集(比如 50 万行关联数据)一次性从 MySQL 拉到 PHP 进程内存中缓存起来。协程再轻量,也扛不住单次加载几百 MB 的数组。
更隐蔽的是:Hyperf 的 Db::select() 或 ORM 的 get() 方法底层都走这个缓冲模式,表面看是“逐行处理”,实际早把全量数据吃进内存了。
- 连表后结果行数翻倍(如 orders × order_items),放大内存压力
- Hyperf Worker 进程常驻,内存不自动释放,泄漏会累积
- 错误日志里往往只报
Fatal error: Allowed memory size of ... exhausted,没线索指向数据库层
在 Hyperf 中启用非缓冲查询 + 游标读取
核心是两步:禁用 PDO 缓冲 + 显式调用 fetch() 循环。不能依赖 ORM 的集合方法(get() / cursor()),它们在 Hyperf 3.x 前不真正支持底层游标流式。
正确写法:
$pdo = Db::getPdo();
$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
$stmt = $pdo->prepare(
'SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id WHERE o.created_at > ?'
);
$stmt->execute([date('Y-m-d H:i:s', strtotime('-7 days'))]);
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
// 处理单行,不累积
processOrderRow($row);
// 关键:及时 unset 大字段或中间变量
unset($row['raw_blob_data']);
}
-
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY = false必须在prepare()前设置,设晚了无效 - 不要用
fetchAll()或get(),哪怕加了limit 1000,缓冲模式下仍会预加载全部匹配行 - Hyperf 的
Db::cursor()在 3.1+ 才真正透传游标行为,低版本请绕过 ORM 直接操作 PDO
连表场景下的内存安全边界控制
光开游标还不够。连表本身可能产生笛卡尔积或重复膨胀,需在 SQL 层压制中间结果规模:
- 用
STRAIGHT_JOIN强制驱动表顺序,避免优化器选错主表导致临时结果暴增 - WHERE 条件必须落在被驱动表的索引字段上(如
orders.user_id有索引,但orders.status没索引就别放这里) - 避免
SELECT *,只取业务需要的字段;大文本/JSON 字段显式NULL掉 - 如果必须分页,用主键偏移(
WHERE id > ? ORDER BY id LIMIT 1000),别用OFFSET——它仍会扫描前 N 行
例如,替代 LIMIT 100000, 1000 的安全写法:
SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id AND o.id > 1000000 WHERE u.status = 'active' ORDER BY o.id LIMIT 1000;
协程环境下容易忽略的内存陷阱
Hyperf 是协程常驻进程,同一 Worker 内多个请求共享堆内存。一个游标查询没清理干净,可能污染后续请求:
- 别在协程外声明静态大数组(如
public static $cache = []),连表结果塞进去就再也出不来 - 用
Context::set()存临时数据时,务必配defer清理:defer(fn() => Context::set('temp_rows', null)); - 查完立刻
unset($stmt)和unset($pdo)(虽然 PDO 对象小,但明确释放更稳妥) - 监控 Worker 内存:加一行
logger()->info('mem usage: '.memory_get_usage());在循环内,确认是否线性增长
真正的难点不在“怎么开游标”,而在“游标开着的时候,其他协程和上下文有没有偷偷 hold 住引用”。这点不盯紧,调参再细也白搭。











