直接用fetchAll()读大表必然内存溢出,必须用非缓冲查询(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false)配合fetch()逐行处理,禁用fetchAll();游标分页替代OFFSET/LIMIT;及时unset、closeCursor并释放资源。

直接用 PDO::fetchAll() 或 mysqli_fetch_all() 读取大表,内存爆掉是大概率事件。这不是配置调高 memory_limit 就能绕过去的——数据库结果集一旦缓冲进 PHP 进程内存,就再也无法被数据库端释放,哪怕你只取前 10 行也得把全部结果先拉进来。
用非缓冲查询(unbuffered query)逐行取数据
缓冲查询是 PHP 默认行为:MySQL 把整张表查完、打包发给 PHP,PHP 再一次性解包进内存;非缓冲查询则是让 MySQL 持有结果集,PHP 每调一次 fetch,它才吐一行出来。关键在连接配置和语句执行方式。
- PDO 必须显式关闭缓冲:
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false,且不能用fetchAll()—— 只能用fetch()或fetchAssoc()循环 - MySQLi 要用
mysqli_use_result()替代mysqli_store_result(),或直接调mysqli_query($conn, $sql, MYSQLI_USE_RESULT) - 注意:非缓冲查询期间不能对同一连接发起其他查询,否则会报
Commands out of sync - PHP CLI 下更稳妥,Web SAPI(如 Apache mod_php)中若超时或连接中断,未读完的结果可能卡在 MySQL 端,需加超时控制
游标分页替代 OFFSET/LIMIT
当必须分批处理(比如导出、统计),LIMIT 10000 OFFSET 100000 这类语句在 MySQL 中实际仍要扫描前 10 万行,越往后越慢,且 PDO 缓冲模式下每批仍会加载全部 LIMIT 行到内存。游标分页靠主键/时间戳推进,无性能衰减,也天然适配流式处理。
- 确保排序字段(如
id)有索引,且查询条件能走索引范围扫描 - 每次查完一批,记下这批最后一条的
id,下次查询用WHERE id > ? ORDER BY id LIMIT ? -
unset($rows)必须跟在循环体外立即执行,否则数组变量持续持有引用,GC 不会回收 - 避免在循环内拼接大字符串或累积大数组,统计类逻辑尽量用原子变量(如
$sum += $row['amount'])
提前释放 PDOStatement 和连接资源
PDOStatement 对象本身不占多少内存,但它背后绑着 MySQL 的结果集缓冲区。如果忘了清理,即使 PHP 变量被 unset,只要 Statement 还活着,内存就不会真正归还。
立即学习“PHP免费学习笔记(深入)”;
- 查询结束后立刻调
$stmt->closeCursor(),尤其在复用$stmt执行不同 SQL 时 - 批量处理完一个大查询后,设
$stmt = null,并确认没有其他变量引用该对象 - 长脚本中,用完数据库连接后可主动
$pdo = null,触发析构释放底层连接 - 检查是否有静态属性缓存了
PDO实例或查询结果(如static $cache = []),这类“内存锚点”不会随函数退出消失
警惕那些看似无害的“辅助操作”
很多溢出不是出在主查询,而是后续加工环节。比如把 10 万行转成 JSON、生成 Excel、做 array_merge 拼接,这些操作会瞬间复制数倍内存。
-
json_encode($bigArray)会创建新字符串,原数组 + 新 JSON 字符串同时驻留内存 - 用
PHPExcel或PhpSpreadsheet读写时,务必启用磁盘缓存(如cache_to_phpTemp),禁用cache_in_memory -
var_dump()、print_r()打印大数组,哪怕只是调试,也会触发完整序列化,极易 OOM - 日志里写
Log::info(['data' => $hugeArray])同样危险,应改用Log::info('processing batch {start}–{end}', ['start' => $minId, 'end' => $maxId])
真正卡住人的,往往不是“怎么读”,而是“读完之后干了什么”。游标分页 + 非缓冲 + 即时 unset 是骨架,但所有中间态数据结构、日志输出、第三方库缓存策略,都得同步收紧——内存不是用完才溢出,是留不住才崩掉。



















