無限級分類(無限極分類)是電商、CMS、權限系統的核心模型。本文深度解析三種主流實現:遞歸查詢、PHP 端引用構建、路徑枚舉法,並提供完整代碼與效能對比。
CREATE TABLE `category` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL COMMENT '分類名', `pid` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '父ID,0為頂級', `path` VARCHAR(255) DEFAULT '' COMMENT '路徑枚舉:0,1,3,', `is_dir` TINYINT NOT NULL DEFAULT 1 COMMENT '1目錄 0文件/葉子', `sort` INT NOT NULL DEFAULT 0 COMMENT '排序', PRIMARY KEY (`id`), KEY `idx_pid` (`pid`), KEY `idx_path` (`path`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
function getTreeRecursive($pid = 0, $level = 0) {
global $pdo;
$stmt = $pdo->prepare("SELECT * FROM category WHERE pid = ? ORDER BY sort, id");
$stmt->execute([$pid]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$tree = [];
foreach ($rows as $row) {
$row['level'] = $level;
$row['children'] = getTreeRecursive($row['id'], $level + 1);
$tree[] = $row;
}
return $tree;
}
// 使用
$tree = getTreeRecursive(0);
echo json_encode($tree, JSON_UNESCAPED_UNICODE);
優點:代碼簡潔、邏輯直觀。
缺點:每層查一次庫,N+1 問題嚴重;深層級易超時、棧溢出。
function buildTree(array $categories) {
// 1. 以 id 為 key 建立索引,預創建 children
$tree = [];
foreach ($categories as $cat) {
$tree[$cat['id']] = $cat;
$tree[$cat['id']]['children'] = [];
}
// 2. 引用掛掛:將每個節點掛到父節點 children 下
foreach ($tree as $id => &$node) {
if ($node['pid'] != 0 && isset($tree[$node['pid']])) {
$tree[$node['pid']]['children'][] = &$node;
}
}
unset($node); // 解除引用
// 3. 篩選頂級節點
$roots = [];
foreach ($tree as $id => $node) {
if ($node['pid'] == 0) $roots[] = $node;
}
return $roots;
}
// 使用
$stmt = $pdo->query("SELECT * FROM category ORDER BY sort, id");
$categories = $stmt->fetchAll(PDO::FETCH_ASSOC);
$tree = buildTree($categories);
echo json_encode($tree, JSON_UNESCAPED_UNICODE);
核心技巧:&$node 引用賦值,一次遍歷構建樹,無遞歸,效能極高。
// 寫入時維護 path:父path + 父id + ','
function insertCategory($name, $pid) {
global $pdo;
$path = '0,';
if ($pid != 0) {
$stmt = $pdo->prepare("SELECT path FROM category WHERE id = ?");
$stmt->execute([$pid]);
$parent = $stmt->fetch(PDO::FETCH_ASSOC);
$path = $parent['path'] . $pid . ',';
}
$stmt = $pdo->prepare("INSERT INTO category (name, pid, path) VALUES (?, ?, ?)");
$stmt->execute([$name, $pid, $path]);
return $pdo->lastInsertId();
}
// 讀取某節點所有後代(單條 SQL)
function getDescendants($nodeId) {
global $pdo;
$stmt = $pdo->prepare("SELECT * FROM category WHERE path LIKE ? ORDER BY path");
$stmt->execute(["%,{$nodeId},%"]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// 讀取某節點祖先鏈
function getAncestors($nodeId) {
global $pdo;
$stmt = $pdo->prepare("SELECT * FROM category WHERE id IN (SELECT CAST(TRIM(BOTH ',' FROM REPLACE(path, ',', ',')) AS UNSIGNED) FROM category WHERE id = ?)");
// 簡化:應用層解析 path
$stmt = $pdo->prepare("SELECT path FROM category WHERE id = ?");
$stmt->execute([$nodeId]);
$path = $stmt->fetchColumn(); // 如 "0,1,3,"
$ids = array_filter(explode(',', trim($path, ',')));
if (!$ids) return [];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM category WHERE id IN ($placeholders) ORDER BY FIELD(id, " . implode(',', $ids) . ")");
$stmt->execute($ids);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
優點:讀取子樹/祖先單條 SQL,無遞歸,極快。
缺點:寫入/移動節點需更新所有後代 path,事務複雜;path 長度受限(VARCHAR 255 約支援 50 層)。
function renderTree(array $tree, $level = 0) {
$html = '輸出效果:
電腦 —筆記本 ——超級本 ——遊戲本 —台式機 手機 —智能機 —功能機
| 方案 | 查詢次數 | PHP 處理 | 適合數據量 | 寫入複雜度 | 推薦指數 |
|---|---|---|---|---|---|
| 遞歸查詢 | N(層數) | 遞歸構建 | < 1,000 | 低 | ★★☆☆☆ |
| 一次查詢+引用構建 | 1 | O(N) 遍歷 | < 50,000 | 低 | ★★★★★ |
| 路徑枚舉 | 1(讀) | 應用層解析 | > 100,000 | 高(需更新後代) | ★★★★☆ |
「一次查詢 + 引用構建」是通用最優解,代碼簡潔、效能優異、無遞歸風險。超大數據量或高頻子樹查詢場景,路徑枚舉或 Closure Table 更適合。根據業務規模選型,切忌過度設計。