默認情況下,PHP MySQL 查詢會將**整個結果集**一次性拉取到 PHP 內存(緩衝查詢)。面對百萬行大數據導出、流式處理、內存受限場景,**非緩衝查詢**可逐行獲取,內存佔用恆定。本文詳解 mysqli、PDO、mysql (已棄用) 三種驅動的非緩衝查詢用法、適用場景與避坑指南。
| 特性 | 緩衝查詢 (默認) | 非緩衝查詢 |
|---|---|---|
| 內存佔用 | O(結果集大小),大結果集易 OOM | O(1) 單行,恆定極低 |
| 網絡交互 | 一次性拉取全部 | 按需流式拉取 |
| 可操作性 | 可 `num_rows`、`data_seek`、多次遍歷 | 僅單向單次遍歷,不可求行數、不可指針跳轉 |
| 並發限制 | 同一連接可並發多查詢 | **同一連接同一時間僅能有一個未關閉的非緩衝結果集** |
| 適用場景 | 小/中結果集、需隨機訪問、多次遍歷 | 大結果集導出、ETL、流式寫入文件/隊列 |
$mysqli = new mysqli('localhost', 'user', 'pass', 'db');
$mysqli->set_charset('utf8mb4');
// 關鍵:MYSQLI_USE_RESULT
$result = $mysqli->query("SELECT id, name, email FROM users WHERE status=1", MYSQLI_USE_RESULT);
if ($result) {
// 逐行處理,內存恆定
while ($row = $result->fetch_assoc()) {
// 處理邏輯:寫 CSV、推隊列、發郵件...
fputcsv($fp, $row);
}
$result->free(); // 必須釋放,否則該連接無法執行下一條查詢
}
// 同一連接再次查詢前,必須確保上一個非緩衝結果集已 free
$mysqli->query("UPDATE stats SET exported=1 WHERE date=CURDATE()");
$pdo = new PDO('mysql:host=localhost;dbname=db;charset=utf8mb4', 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false, // 使用原生預處理
// 關鍵屬性:禁用緩衝
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false,
]);
$stmt = $pdo->query("SELECT id, name, data FROM large_table");
/* 或預處理
$stmt = $pdo->prepare("SELECT ... WHERE id > ?");
$stmt->execute([$lastId]);
*/
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
// 流式處理
processRow($row);
}
// 關閉游標,釋放連接
$stmt->closeCursor();
$conn = mysql_connect('localhost', 'user', 'pass');
mysql_select_db('db', $conn);
mysql_set_charset('utf8', $conn);
// mysql_unbuffered_query 返回資源
$result = mysql_unbuffered_query("SELECT * FROM huge_logs");
if ($result) {
while ($row = mysql_fetch_assoc($result)) {
// 處理
}
mysql_free_result($result); // 必須釋放
}
警告:`ext/mysql` 自 PHP 5.5 棄用,PHP 7.0 移除。老項目**必須遷移至 mysqli/PDO**。
// 測試表:100 萬行,每行 ~200 字節
// PHP memory_limit = 128M
// ❌ 緩衝查詢:內存峰值 ~200MB → Fatal error: Allowed memory size exhausted
$result = $pdo->query("SELECT * FROM big_table");
$rows = $result->fetchAll(); // 一次性載入
foreach ($rows as $row) fputcsv($fp, $row);
// ✅ 非緩衝查詢:內存峰值 ~2MB(僅單行 + 輸出緩衝)
$stmt = $pdo->query("SELECT * FROM big_table", PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false);
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
fputcsv($fp, $row);
}
$stmt->closeCursor();
function unbufferedQuery(PDO $pdo, string $sql, array $params = []): Generator {
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
try {
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
yield $row;
}
} finally {
$stmt->closeCursor(); // 確保釋放
}
}
// 使用:極其優雅的內存安全遍歷
foreach (unbufferedQuery($pdo, "SELECT * FROM logs WHERE created_at > ?", [$since]) as $row) {
// 處理邏輯
// 即使 break 也會執行 finally 釋放游標
}
| 坑點 | 現象 | 解決 |
|---|---|---|
| 未 free/closeCursor 再次查詢 | `Commands out of sync; you can't run this command now` | 循環結束後、break/return 前、異常捕獲 finally 中**必須釋放** |
| 同連接並發非緩衝 | 同一連接開啟第二個非緩衝結果集失敗 | 串行處理;或開多連接(連接池) |
| 非緩衝中調用 num_rows | mysqli: 返回 0/報錯;PDO: 無此方法 | 改用 `SELECT COUNT(*)` 獨立查詢 |
| 事務中使用非緩衝 | 鎖持有時間過長,併發性能下降 | 導出類只讀操作**不開事務**;讀已提交隔離級別 |
| PDO 模擬預處理模式 | `ATTR_EMULATE_PREPARES=true` 時非緩衝可能不生效 | 設置 `false` 使用原生預處理 |
掌握非緩衝查詢,是 PHP 開發者從「增刪改查」進階到「大數據處理、高併發架構」的必經之路。