SELECT 是數據庫操作的核心。本文系統梳理 PHP 執行 SELECT 查詢的完整流程:連接、編碼、查詢構建、結果遍歷、資源釋放,重點對比 mysqli 與 PDO 兩大擴展的用法差異,並確立 預處理語句 作為防注入標準。
基礎流程圖解
1. 建立連接 (mysqli_connect / new PDO)
2. 設置字符集 (SET NAMES utf8mb4)
3. 構建 SQL (參數化佔位符 ? / :name)
4. 預處理 (prepare)
5. 綁定參數 (bind_param / execute([...]))
6. 執行 (execute)
7. 獲取結果集 (get_result / fetchAll)
8. 遍歷數據 (fetch_assoc / foreach)
9. 釋放結果 (free_result / null)
10. 關閉連接 (close / null)
方案一:mysqli 面向過程風格(兼容舊代碼)
<?php
$host = 'localhost'; $user = 'user'; $pass = 'pass'; $db = 'testdb';
$link = mysqli_connect($host, $user, $pass, $db);
if (!$link) { die('連接失敗: ' . mysqli_connect_error()); }
// 關鍵:設置字符集防止中文亂碼
mysqli_set_charset($link, 'utf8mb4');
// 預處理防注入
$sql = "SELECT uri, title FROM articles WHERE status = ? AND category_id = ? ORDER BY created_at DESC LIMIT ?";
$stmt = mysqli_prepare($link, $sql);
mysqli_stmt_bind_param($stmt, 'iii', $status, $catId, $limit);
$status = 1; $catId = 5; $limit = 10;
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt); // 需 mysqlnd 驅動
while ($row = mysqli_fetch_assoc($result)) {
echo "<p><strong>{$row['title']}</strong>: {$row['uri']}</p>";
}
mysqli_free_result($result);
mysqli_stmt_close($stmt);
mysqli_close($link);
?>
方案二:PDO 面向對象風格(現代首選)
<?php
$dsn = "mysql:host=localhost;dbname=testdb;charset=utf8mb4";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false, // 真實預處理
];
try {
$pdo = new PDO($dsn, 'user', 'pass', $options);
} catch (PDOException $e) {
die('連接失敗: ' . $e->getMessage());
}
$sql = "SELECT uri, title FROM articles WHERE status = :status AND category_id = :cat ORDER BY created_at DESC LIMIT :limit";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'status' => 1,
'cat' => 5,
'limit' => 10,
]);
foreach ($stmt as $row) {
echo "<p><strong>{$row['title']}</strong>: {$row['uri']}</p>";
}
// $stmt = null; // 釋放語句句柄
// $pdo = null; // 關閉連接
?>
關鍵知識點深度解析
1. 字符集:必須用 utf8mb4
utf8 (MySQL 別名 utf8mb3) 僅支持 BMP 平面,**無法存儲 Emoji、生僻字**。
- 連接、表、列、數據庫四層均應為
utf8mb4 + utf8mb4_unicode_ci。
- PDO DSN 中 `charset=utf8mb4` + `SET NAMES` 雙重保障。
2. 預處理原理與防注入
-- 服務端解析階段即確定查詢計劃,參數作為數據傳輸,永不參與 SQL 解析
PREPARE stmt FROM 'SELECT * FROM users WHERE name = ?';
SET @n = 'admin'' OR 1=1 --';
EXECUTE stmt USING @n; -- 安全:參數被視為字面字符串,不會改變邏輯
- 禁止任何形式的字符串拼接 SQL:
"WHERE name = '$name'"。
- 識別符(表名、列名、排序字段)無法參數化,需通過白名單驗證:
in_array($orderBy, $allowedColumns)。
3. 結果集獲取模式對比
| 模式 | mysqli | PDO | 適用場景 |
| 緩衝模式 (默認) | mysqli_stmt_get_result / fetch_all | fetchAll() | 結果集較小(<10萬行),需隨機訪問、多次遍歷 |
| 非緩衝/流式 | mysqli_stmt_fetch() 逐行 | fetch() 逐行迭代 | 大數據集、導出 CSV、內存受限 |
4. 隨機查詢優化
-- 低效:全表排序
SELECT * FROM articles ORDER BY RAND() LIMIT 1;
-- 高效:主鍵範圍隨機 (需連續自增 ID)
SELECT * FROM articles
WHERE id >= (SELECT FLOOR(RAND() * (SELECT MAX(id) FROM articles)))
ORDER BY id LIMIT 1;
常見錯誤與修正
| 錯誤代碼 | 問題 | 修正 |
mysql_query("SET NAMES GBK") | 使用已移除的 mysql_* 擴展 | 改用 mysqli/PDO + utf8mb4 |
$sql = "SELECT * FROM t WHERE x = $input" | 直接拼接變量,SQL 注入高危 | 預處理 + 參數綁定 |
| 未檢查連接/查詢返回值 | 靜默失敗,難以排查 | PDO 設置 ERRMODE_EXCEPTION;mysqli 檢查 false |
| 循環中執行查詢 | N+1 問題,性能災難 | JOIN、IN 子查詢、批量預處理 |
封裝建議:通用查詢構建器
<?php
class QueryBuilder
{
public function __construct(private PDO $pdo) {}
public function select(string $table, array $columns = ['*'], array $where = [], array $orderBy = [], int $limit = 0): array
{
$sql = "SELECT " . implode(', ', array_map(fn($c)=>"`$c`", $columns)) . " FROM `$table`";
$params = [];
if ($where) {
$conditions = [];
foreach ($where as $col => $val) {
if (is_array($val)) { // IN 查詢
$placeholders = implode(',', array_fill(0, count($val), '?'));
$conditions[] = "`$col` IN ($placeholders)";
$params = array_merge($params, $val);
} else {
$conditions[] = "`$col` = ?";
$params[] = $val;
}
}
$sql .= " WHERE " . implode(' AND ', $conditions);
}
if ($orderBy) {
$sql .= " ORDER BY " . implode(', ', $orderBy);
}
if ($limit > 0) {
$sql .= " LIMIT $limit";
}
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
}
// 使用
$qb = new QueryBuilder($pdo);
$articles = $qb->select('articles', ['title', 'uri'], ['status'=>1, 'category_id'=>[5,6]], ['created_at DESC'], 10);
?>
SELECT 看似簡單,但「字符集、預處理、流式讀取、索引優化」四大支柱缺一不可。養成「永不拼接 SQL」的肌肉記憶,是 PHP 開發者的職業底線。