在處理大量資料集時,PHP 預設的「緩衝查詢」模式極易導致記憶體耗盡 (Allowed memory size exhausted)。理解緩衝與非緩衝查詢的機制差異、適用場景及三大擴展的實作方式,是 PHP 後端工程師的必修課。
| 維度 | 緩衝查詢 | 非緩衝查詢 |
|---|---|---|
| 資料傳輸 | 一次性將整個結果集從 MySQL 服務端傳輸至 PHP 記憶體 | MySQL 服務端保留結果集,PHP 逐行拉取 (串流模式) |
| PHP 記憶體佔用 | 高:與結果集大小成正比 | 極低:僅佔用單行資料大小 |
| MySQL 服務端壓力 | 低:快速釋放資源 | 高:需維持結果集、連線佔用、鎖持有時間長 |
| 功能支援 | 完整:num_rows、data_seek、多次遍歷、隨機存取 | 受限:僅單向順序讀取,無法獲取總行數、無法跳轉 |
| 適用場景 | 小/中型結果集 (建議 < 10 萬行)、需多次處理、需隨機存取 | 大型結果集 (百萬行+)、串流處理、記憶體受限環境 |
| 預設模式 | 所有 PHP MySQL 擴展預設啟用 | 需顯式開啟 |
// 危險範例:4萬筆資料一次性載入記憶體
$pdo = new PDO("mysql:host=localhost;dbname=test", 'user', 'pass');
$stmt = $pdo->query("SELECT * FROM large_table LIMIT 40000");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); // 記憶體飆升,極易 OOM
foreach ($rows as $row) { /* 處理 */ }
改用非緩衝查詢,記憶體曲線變平坦:
// 安全範例:非緩衝逐行處理
$pdo = new PDO("mysql:host=localhost;dbname=test", 'user', 'pass');
$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
$stmt = $pdo->query("SELECT * FROM large_table LIMIT 40000");
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { // 單行處理,即用即釋
/* 處理單行資料 */
}
$mysqli = new mysqli("localhost", "user", "pass", "db");
// 關鍵:MYSQLI_USE_RESULT 開啟非緩衝模式
$uresult = $mysqli->query("SELECT id, name FROM big_table", MYSQLI_USE_RESULT);
if ($uresult) {
while ($row = $uresult->fetch_assoc()) {
echo $row['name'] . PHP_EOL; // 串流處理
}
$uresult->close(); // 必須關閉釋放 MySQL 端資源
}
$mysqli->close();
注意:非緩衝模式下,若未讀完所有行就關閉結果集或執行新查詢,會觸發 Commands out of sync 錯誤。
$pdo = new PDO("mysql:host=localhost;dbname=db", 'user', 'pass');
// 關鍵屬性:MYSQL_ATTR_USE_BUFFERED_QUERY = false
$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
$stmt = $pdo->query("SELECT id, name FROM big_table");
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
// 處理
}
// PDO 無需顯式 closeCursor() 即可釋放,但建議良好習慣呼叫
$stmt = null;
$conn = mysql_connect("localhost", "user", "pass");
mysql_select_db("db");
// 專用函式:mysql_unbuffered_query
$uresult = mysql_unbuffered_query("SELECT id, name FROM big_table");
if ($uresult) {
while ($row = mysql_fetch_assoc($uresult)) {
echo $row['name'] . PHP_EOL;
}
}
// 注意:舊擴展無預處理支援,極易 SQL 注入,嚴禁新專案使用
非緩衝查詢雖省記憶體,但長連線佔用 MySQL 資源,易導致「Too many connections」或主從同步延遲。生產環境推薦 **分批緩衝查詢**:
// PDO 分批處理範例:每次 1000 筆
$pageSize = 1000;
$offset = 0;
$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true); // 回歸緩衝
do {
$stmt = $pdo->prepare("SELECT id, data FROM big_table LIMIT :limit OFFSET :offset");
$stmt->bindValue(':limit', $pageSize, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $row) {
// 處理單行
}
$offset += $pageSize;
} while (count($rows) === $pageSize);
優點:記憶體可控、MySQL 連線短暫、支援預處理防注入、可並行化處理。
num_rows 返回 0 或不可用。SELECT COUNT(*) (利用覆蓋索引極快) 再決定是否分批。MYSQL_OPT_READ_TIMEOUT、MYSQL_OPT_WRITE_TIMEOUT。SET SESSION max_statement_time=... 限制執行時間。