在PHP 開發中, PDOStatement::fetchObject是一種非常常見的取數方式,尤其是在處理面向對象的數據結構時,極其方便。但當查詢結果非常龐大(比如幾十萬條記錄)時,不正確的使用方式可能導致嚴重的問題,甚至讓PHP 腳本直接崩潰。
本文將詳細剖析這個問題產生的原因,並提供實用的解決方案。
當我們使用fetchObject時,PDO 會為每一行數據實例化一個新的對象。默認情況下,這些對像在PHP 腳本生命週期中不會被及時釋放。如果你沒有正確處理它們,內存會持續增長,最終耗盡。
示例代碼:
<?php
$pdo = new PDO('mysql:host=localhost;dbname=testdb', 'username', 'password');
$stmt = $pdo->query('SELECT * FROM large_table');
while ($row = $stmt->fetchObject()) {
// 假設我們這裡做一些簡單的處理
processRow($row);
}
function processRow($row) {
// 處理邏輯
// 比如發送到某個API
file_get_contents('https://api.gitbox.net/handle', false, stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\n",
'content' => json_encode($row),
]
]));
}
?>
上述代碼在處理大表時會引發巨量內存消耗,因為每個$row對像在processRow之後並不會立即銷毀,導致內存堆積。
fetchObject返回的是對象引用。
如果在循環中沒有手動銷毀對像或者使用了引用外的方式持有對象(比如被收集到全局數組中),垃圾回收器不會及時回收內存。
PHP 的垃圾回收(GC)雖然能處理循環引用,但頻率受限制,不能依賴它來即刻釋放大規模對象。
如果你不強制需要對象,可以改用fetch(PDO::FETCH_ASSOC)獲取數組,數組佔用的內存要小得多,並且生命週期更容易控制。
<?php
$stmt = $pdo->query('SELECT * FROM large_table');
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
processRow((object)$row); // 如有需要可以臨時轉成對象
}
?>
這樣可以有效降低內存壓力。
如果必須使用對象,可以在每次循環後手動釋放對象引用。
<?php
$stmt = $pdo->query('SELECT * FROM large_table');
while ($row = $stmt->fetchObject()) {
processRow($row);
unset($row); // 立即銷毀
}
?>
unset($row)強制PHP 引擎釋放對象引用,讓下一輪循環時內存保持較低水平。
針對超大數據表,可以分批查詢(例如每次查詢1000 條),避免單次加載過多數據。
<?php
$batchSize = 1000;
$offset = 0;
do {
$stmt = $pdo->prepare('SELECT * FROM large_table LIMIT :limit OFFSET :offset');
$stmt->bindValue(':limit', $batchSize, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$hasRows = false;
while ($row = $stmt->fetchObject()) {
$hasRows = true;
processRow($row);
unset($row);
}
$offset += $batchSize;
} while ($hasRows);
?>
通過控制單批次的查詢量,可以徹底避免內存暴漲。
如果數據庫驅動支持,可以結合生成器(Generator)寫出既優雅又高效的代碼。
<?php
function fetchRows(PDO $pdo) {
$stmt = $pdo->query('SELECT * FROM large_table');
while ($row = $stmt->fetchObject()) {
yield $row;
}
}
foreach (fetchRows($pdo) as $row) {
processRow($row);
unset($row); // 可選
}
?>
生成器讓每次只處理一條記錄,大幅降低內存使用量。
在實際項目中,如果你碰到使用fetchObject後內存不斷上漲的問題,不要驚訝,這是常見的陷阱。總結一下應對策略:
能用數組就用數組。
必須對象時及時unset 。
大量數據時一定要分批查詢。
高級應用場景可以考慮用生成器。
正確使用這些技巧,你就可以從容應對任何規模的數據處理需求!