查詢資料庫取得結果集後,常需以某欄位(如 id、sid)為鍵名重組陣列,方便後續 O(1) 查找、合併、比對,避免迴圈遍歷。
<?php
/**
* 查詢結果重組:以指定欄位值為鍵名
*
* @param resource|PDOStatement $result 查詢結果資源
* @param string $keyField 作為鍵名的欄位名
* @param string $fetchMode 取得模式:assoc|num|both|obj
* @return array 重組後的關聯陣列
*/
function fetchAllKeyed($result, $keyField, $fetchMode = 'assoc') {
$data = [];
$fetchFunc = [
'assoc' => MYSQLI_ASSOC,
'num' => MYSQLI_NUM,
'both' => MYSQLI_BOTH,
'obj' => MYSQLI_ASSOC, // 物件模式另行處理
][$fetchMode] ?? MYSQLI_ASSOC;
// mysqli_result
if ($result instanceof mysqli_result) {
while ($row = $result->fetch_assoc()) {
if (!array_key_exists($keyField, $row)) {
trigger_error("欄位 '$keyField' 不存在於結果集", E_USER_WARNING);
continue;
}
$key = $row[$keyField];
$data[$key] = $row;
}
return $data;
}
// PDOStatement
if ($result instanceof PDOStatement) {
while ($row = $result->fetch(PDO::FETCH_ASSOC)) {
if (!array_key_exists($keyField, $row)) {
trigger_error("欄位 '$keyField' 不存在於結果集", E_USER_WARNING);
continue;
}
$key = $row[$keyField];
$data[$key] = $row;
}
return $data;
}
// 陣列輸入(已取得的資料)
if (is_array($result)) {
foreach ($result as $row) {
if (is_array($row) && array_key_exists($keyField, $row)) {
$data[$row[$keyField]] = $row;
}
}
return $data;
}
throw new InvalidArgumentException('不支援的結果集類型');
}
-- 站點設定表(讀多寫少 → MyISAM)
CREATE TABLE site_config (
id SMALLINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
sid SMALLINT UNSIGNED UNIQUE COMMENT '站點ID',
site_url VARCHAR(128) NOT NULL,
site_name VARCHAR(80) NOT NULL,
add_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=MyISAM CHARSET=utf8mb4;
-- 流量統計表(寫多讀少 → InnoDB)
CREATE TABLE site_stat (
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
sid SMALLINT UNSIGNED NOT NULL,
ip_address VARCHAR(45) NOT NULL,
add_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB CHARSET=utf8mb4;
<?php
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', 'user', 'pass');
// 1. 取得所有站點(不需重組,或以 sid 為鍵)
$sitesStmt = $pdo->query('SELECT sid, site_url, site_name FROM site_config');
$sites = fetchAllKeyed($sitesStmt, 'sid');
// 結果:[200 => ['sid'=>200, 'site_url'=>'baidu.com', ...], 201 => [...], ...]
// 2. 取得今日流量統計(GROUP BY sid)
$today = date('Y-m-d');
$statStmt = $pdo->prepare("
SELECT sid, COUNT(*) AS come_total
FROM site_stat
WHERE add_time >= ? AND add_time < ?
GROUP BY sid
");
$statStmt->execute([$today . ' 00:00:00', $today . ' 23:59:59']);
$stats = fetchAllKeyed($statStmt, 'sid');
// 結果:[200 => ['sid'=>200, 'come_total'=>3], 202 => ['sid'=>202, 'come_total'=>3], ...]
// 3. 產出報表(O(1) 查找統計資料)
echo "日期,站點ID,站點網址,流量數\n";
foreach ($sites as $sid => $site) {
$comeTotal = $stats[$sid]['come_total'] ?? 0;
echo "{$today},{$sid},{$site['site_url']},{$comeTotal}\n";
}
/*
輸出:
2024-01-15,200,baidu.com,3
2024-01-15,201,google.com,0
2024-01-15,202,cnblogs.com,3
2024-01-15,203,codejia.net,1
*/
<?php
/**
* 進階重組:支援多欄位組合鍵、分組模式
*/
function fetchAllKeyedAdvanced($result, $keyFields, $group = false) {
$data = [];
$isMultiple = is_array($keyFields);
$rows = is_array($result) ? $result : iterator_to_array($result);
foreach ($rows as $row) {
// 組合鍵名
if ($isMultiple) {
$keyParts = [];
foreach ($keyFields as $f) $keyParts[] = $row[$f] ?? '';
$key = implode('|', $keyParts);
} else {
$key = $row[$keyFields] ?? null;
}
if ($key === null) continue;
if ($group) {
$data[$key][] = $row; // 分組:同鍵值收集為陣列
} else {
$data[$key] = $row; // 唯一:後者覆蓋前者
}
}
return $data;
}
// 使用:以 user_id + date 為鍵分組
$logs = $pdo->query("SELECT user_id, DATE(created_at) as log_date, action FROM user_logs")->fetchAll();
$grouped = fetchAllKeyedAdvanced($logs, ['user_id', 'log_date'], true);
/*
[
'1001|2024-01-15' => [
['user_id'=>1001, 'log_date'=>'2024-01-15', 'action'=>'login'],
['user_id'=>1001, 'log_date'=>'2024-01-15', 'action'=>'view'],
],
...
]
*/
| 場景 | 建議 |
|---|---|
| 大量資料(>10萬筆) | SQL 層面用 GROUP BY、INDEX,減少 PHP 端處理 |
| 頻繁查找 | 重組為鍵值陣列,享受 O(1) 查找 |
| 記憶體敏感 | 使用 Generator 逐行處理,或 MYSQLI_USE_RESULT 非緩衝查詢 |
| 多表關聯 | 優先考慮 SQL JOIN,或分別查詢後 PHP 端合併(如本例) |
$group=true)NULL 或空字串 → 導致資料遺失或覆蓋AS 統一別名、PHP 端統一 strtolower() 鍵名