處理訂單明細、銷售報表、日誌統計時,常需「依特定欄位分組,並對數值欄位求和」。雖資料庫 GROUP BY 為首選,但資料已在 PHP 陣列中(如多表 JOIN 後合併、API 彙總、快取資料)時,需高效的記憶體內分組演算法。本文提供 O(n) 時間複雜度的標準實作與進階優化。
/**
* 依指定鍵分組,數值欄位自動加總
* @param array $rows 來源二維陣列
* @param string $groupKey 分組鍵名
* @return array 分組後陣列(索引重排為 0,1,2...)
*/
function groupSum(array $rows, string $groupKey): array {
$map = []; // 分組鍵 => 累加陣列
foreach ($rows as $row) {
$key = $row[$groupKey] ?? null;
if ($key === null) continue; // 無分組鍵跳過
if (!isset($map[$key])) {
$map[$key] = $row; // 首筆直接複製
continue;
}
// 後續筆:數值欄位累加
foreach ($row as $k => $v) {
if ($k === $groupKey) continue;
if (is_numeric($v)) {
$map[$key][$k] = ($map[$key][$k] ?? 0) + $v;
}
// 非數值欄位(如日期、狀態)保留首筆或依業務邏輯處理
}
}
return array_values($map); // 重排數值索引
}
13, 'team_price' => 88.00, 'satopay_price' => 85.00, 'origin' => 440, 'gain' => 14.45, 'quantity' => 5],
['group_id' => 13, 'team_price' => 12.00, 'satopay_price' => 11.00, 'origin' => 36, 'gain' => 2.76, 'quantity' => 3],
['group_id' => 14, 'team_price' => 4.99, 'satopay_price' => 4.60, 'origin' => 4.99, 'gain' => 0.31915,'quantity' => 1],
['group_id' => 14, 'team_price' => 12.00, 'satopay_price' => 11.00, 'origin' => 24, 'gain' => 1.84, 'quantity' => 2],
['group_id' => 15, 'team_price' => 13.00, 'satopay_price' => 11.00, 'origin' => 24, 'gain' => 1.84, 'quantity' => 2],
];
$result = groupSum($data, 'group_id');
/*
[
['group_id'=>13, 'team_price'=>100.00, 'satopay_price'=>96.00, 'origin'=>476, 'gain'=>17.21, 'quantity'=>8],
['group_id'=>14, 'team_price'=>16.99, 'satopay_price'=>15.60, 'origin'=>28.99, 'gain'=>2.15915,'quantity'=>3],
['group_id'=>15, 'team_price'=>13.00, 'satopay_price'=>11.00, 'origin'=>24, 'gain'=>1.84, 'quantity'=>2],
]
*/
function groupAggregate(
array $rows,
string $groupKey,
array $sumFields = [], // 需加總欄位,空則自動偵測數值欄位
array $firstFields = [], // 保留首筆欄位
array $lastFields = [], // 保留末筆欄位
callable $customReducer = null // 自定義 reducer(key, carry, row)
): array {
$map = [];
foreach ($rows as $row) {
$key = $row[$groupKey] ?? null;
if ($key === null) continue;
if (!isset($map[$key])) {
$map[$key] = $row;
continue;
}
$carry = &$map[$key];
foreach ($row as $k => $v) {
if ($k === $groupKey) continue;
if ($customReducer) {
$carry[$k] = $customReducer($k, $carry[$k] ?? null, $v);
continue;
}
if ($sumFields && in_array($k, $sumFields, true)) {
$carry[$k] = ($carry[$k] ?? 0) + $v;
} elseif ($lastFields && in_array($k, $lastFields, true)) {
$carry[$k] = $v; // 覆蓋為最新
} // firstFields 無需處理,保留首筆
}
}
return array_values($map);
}
// 使用範例
$result = groupAggregate($data, 'group_id',
sumFields: ['team_price', 'satopay_price', 'origin', 'gain', 'quantity'],
lastFields: ['updated_at']
);
| 資料筆數 | 時間複雜度 | 記憶體 | 建議 |
|---|---|---|---|
| < 10,000 | O(n) | ~2x 原陣列 | 直接用上述函式 |
| 10,000 ~ 1,000,000 | O(n) | 較大 | 分批處理、Generator 串流、或下推 SQL |
| > 1,000,000 | O(n) | 極大 | ClickHouse、Druid、資料倉儲預聚合 |
function groupSumBatch(Iterator $source, string $groupKey, int $batchSize = 10000): Generator {
$map = [];
$count = 0;
foreach ($source as $row) {
$key = $row[$groupKey] ?? null;
if ($key === null) continue;
if (!isset($map[$key])) {
$map[$key] = $row;
} else {
foreach ($row as $k => $v) {
if ($k === $groupKey) continue;
if (is_numeric($v)) $map[$key][$k] = ($map[$key][$k] ?? 0) + $v;
}
}
if (++$count % $batchSize === 0) {
yield $map;
$map = []; // 釋放記憶體,下游自行合併多批次結果
}
}
if ($map) yield $map;
}
// 合併多批次結果
$final = [];
foreach (groupSumBatch($largeIterator, 'group_id') as $batch) {
foreach ($batch as $key => $row) {
if (!isset($final[$key])) $final[$key] = $row;
else foreach ($row as $k => $v) if ($k !== 'group_id' && is_numeric($v)) $final[$key][$k] += $v;
}
}
$result = array_values($final);
use Illuminate\Support\Collection;
$collect = collect($data);
$grouped = $collect->groupBy('group_id')
->map(fn($rows) => $rows->reduce(function($carry, $row) {
foreach ($row as $k => $v) {
if ($k === 'group_id') continue;
$carry[$k] = ($carry[$k] ?? 0) + (is_numeric($v) ? $v : 0);
}
return $carry;
}, ['group_id' => $rows->first()['group_id']]))
->values()
->all();
bcadd()、decimal 型別。Group_ID,統一 strtolower() 或對應映射表。status、created_at 需決定取首筆、末筆、最頻值等。PHP 記憶體內分組加總核心是「雜湊表累加」,時間 O(n)、空間 O(k)(k 為分組數)。掌握標準演算法、自定義聚合規則、分批處理與下推 SQL 三層策略,即能應對從小型報表到百萬筆資料的各類彙總需求。