給定一個分類 ID,獲取其所有後代分類 ID(子、孫、曾孫...),返回逗號分隔字串供 SQL IN (...) 使用。
<?php
// 原文代碼:每層執行一次 SELECT,深度 N 執行 N 次查詢
private function sonCategoryIds(int $categoryID): string {
$array[] = $categoryID;
do {
$ids = '';
$temp = $this->mysql->select(
"SELECT `id` FROM `pcb_article_category` WHERE `parentID` IN ($categoryID)"
);
foreach ($temp as $v) {
$array[] = $v['id'];
$ids .= ',' . $v['id'];
}
$ids = substr($ids, 1);
$categoryID = $ids;
} while (!empty($temp));
return implode(',', $array);
}
缺點: 分類層級越深、分支越多,查詢次數線性增長,資料庫壓力大。
<?php
/**
* 獲取所有後代分類 ID(單查詢版)
*
* @param PDO $pdo
* @param int $rootId
* @return string 逗號分隔 ID 字串
*/
function getAllDescendantIds(PDO $pdo, int $rootId): string {
// 1. 單次查詢所有分類(id, parentID)
$stmt = $pdo->query('SELECT `id`, `parentID` FROM `pcb_article_category`');
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 2. 建立 parentID => [childIds] 映射
$childrenMap = [];
foreach ($rows as $row) {
$childrenMap[$row['parentID']][] = $row['id'];
}
// 3. 遞歸收集後代
$descendants = [];
$collect = function (int $pid) use (&$collect, &$childrenMap, &$descendants) {
if (empty($childrenMap[$pid])) return;
foreach ($childrenMap[$pid] as $cid) {
$descendants[] = $cid;
$collect($cid);
}
};
$collect($rootId);
// 4. 含根節點自身
array_unshift($descendants, $rootId);
return implode(',', $descendants);
}
SELECT,資料庫往返 1 次-- MySQL 8.0+ WITH RECURSIVE CTE
WITH RECURSIVE `descendants` AS (
SELECT `id`, `parentID` FROM `pcb_article_category` WHERE `id` = :rootId
UNION ALL
SELECT c.`id`, c.`parentID`
FROM `pcb_article_category` c
INNER JOIN `descendants` d ON c.`parentID` = d.`id`
)
SELECT GROUP_CONCAT(`id` SEPARATOR ',') AS `ids` FROM `descendants`;
<?php
function getDescendantsCte(PDO $pdo, int $rootId): string {
$sql = <<prepare($sql);
$stmt->execute([':rootId' => $rootId]);
return $stmt->fetchColumn() ?: (string)$rootId;
}
parentID 應建索引)極快額外維護一張表 category_closure:
CREATE TABLE `category_closure` (
`ancestor` INT NOT NULL,
`descendant` INT NOT NULL,
`depth` INT NOT NULL,
PRIMARY KEY (`ancestor`, `descendant`),
KEY `idx_descendant` (`descendant`)
);
-- 範例資料:1 是 2、3 的祖先;2 是 4 的祖先
INSERT INTO `category_closure` VALUES
(1,1,0), (1,2,1), (1,3,1), (1,4,2),
(2,2,0), (2,4,1),
(3,3,0),
(4,4,0);
<?php
// 獲取所有後代(含自身):depth > 0 或 depth >= 0
$stmt = $pdo->prepare('SELECT GROUP_CONCAT(`descendant` SEPARATOR ",") FROM `category_closure` WHERE `ancestor` = :rootId');
$stmt->execute([':rootId' => $rootId]);
$ids = $stmt->fetchColumn(); // 直接拿到 "1,2,3,4"
// 獲取直接子分類:depth = 1
$stmt = $pdo->prepare('SELECT GROUP_CONCAT(`descendant` SEPARATOR ",") FROM `category_closure` WHERE `ancestor` = :rootId AND `depth` = 1');
| 分類規模 | 變更頻率 | 資料庫版本 | 推薦方案 |
|---|---|---|---|
| < 5 萬 | 低 | 任意 | 單查詢 + PHP 遞歸 |
| 5 萬 ~ 50 萬 | 中 | MySQL 8.0+ | CTE 遞歸 |
| > 50 萬 | 高 | 任意 | Closure Table |
| 任意 | 極高 | MySQL 5.7 | Closure Table (唯一選擇) |
原始代碼的 N+1 查詢是典型效能陷阱。根據業務規模選擇:單查詢映射法最通用、CTE最現代、Closure Table最極致。記得為 parentID 建立索引,並將獲取邏輯封裝為 Repository 方法,上層業務碼保持乾淨。