必须禁用pdo缓冲查询并采用游标分页流式导出:设置pdo::mysql_attr_use_buffered_query为false,基于索引字段(如id)分批fetch,逐行处理输出,避免内存溢出。

直接用 PDO::fetchAll() 读取百万行数据导出,必然触发 Allowed memory size of XXX bytes exhausted。这不是配置太小,而是你让 PHP 把整张结果集从 MySQL 服务端一口气拖进内存——100 万行 × 平均 1.5KB/行 ≈ 1.5GB 原始数据,PHP 数组结构再翻 3 倍,2GB+ 内存瞬间见底。
关掉 PDO 缓冲查询是第一步
PDO 默认开启 PDO::MYSQL_ATTR_USE_BUFFERED_QUERY,哪怕你只调 fetch(),驱动仍会把全部结果缓存在客户端内存里。不关它,游标、生成器全白搭。
- 必须在创建 PDO 实例时显式禁用:
$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false) - 别信“我只 fetch 一行”,没关这个属性,
query()返回的PDOStatement已经把整张表拉下来了 - MySQLi 用户同理:用
mysqli::use_result()替代store_result()
用游标分页替代 OFFSET 分页
LIMIT 1000000, 1000 这种写法在百万级表上会扫描前 100 万行,CPU 和 IO 都爆,且无法缓解内存压力——PDO 仍要缓存这 1000 行前的所有中间结果。
- 基于自增主键或时间戳字段做游标:
WHERE id > ? ORDER BY id LIMIT 1000 - 每次循环后更新游标值:
$lastId = end($rows)['id'],注意判空 - 批量大小建议 500–2000 行:太小增加数据库往返次数,太大仍可能局部溢出
- 别在 WHERE 条件里混用非索引字段,否则游标失效,退化成全表扫描
逐行处理 + 即时释放变量
即使用了游标和非缓冲查询,如果还把每批结果存进一个大数组(比如 $allData[] = $row),内存照样线性上涨。
- 直接在
foreach($rows as $row)里处理并输出,不累积 - 处理完立刻
unset($rows),尤其在 while 循环里,避免上一批残留 - 导出 CSV 时用
fputcsv($fp, $row)直接写文件句柄,别拼大字符串 - 如果逻辑复杂需暂存中间状态(如统计用户数),用单层变量(
$userCount[$uid]++)而非嵌套大数组
生成器封装更安全,但别滥用
生成器能天然避免数组累积,但前提是它真在“流式”产出——如果生成器内部还是 fetchAll(),只是换个壳,照样崩。
- 正确姿势:生成器里调用非缓冲查询 +
fetch(),每次 yield 一行:yield $stmt->fetch(PDO::FETCH_ASSOC) - 错误姿势:
$rows = $stmt->fetchAll(); foreach ($rows as $row) yield $row;—— 全量加载没变 - CLI 脚本中可加
gc_collect_cycles()在每批后手动触发回收,对循环引用敏感的场景有效 - 生成器函数本身不能带 try/catch 包裹整个 yield 流程,异常会中断迭代;需在 yield 前做单行容错
最易被忽略的一点:游标字段必须有索引,且类型一致。比如 id 是 BIGINT UNSIGNED,但 PHP 传参用了 int(32 位),游标值截断导致重复读或漏读——这种问题不会报错,只会导出数据不全或重复,查三天都找不到根因。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











