首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP + MySQL 無限分類兩大經典方案深度對比:遞歸樹 vs Path 路徑法

     編輯:PHP 相關知識

無限分類是電商、CMS、知識庫等系統的核心需求。本文深度剖析兩種主流實現方案——遞歸樹(Parent-ID)與Path 路徑法(Materialized Path),從資料結構、查詢效能、寫入複雜度、適用場景全維度對比,助你選型決策。

方案一:遞歸樹—— 經典 Parent-ID 設計

資料表結構

CREATE TABLE `category` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `name` VARCHAR(100) NOT NULL,
  `pid` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '父分類 ID,0 為頂級',
  `sort` INT NOT NULL DEFAULT 0,
  INDEX `idx_pid` (`pid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

核心邏輯:一次查詢 + PHP 記憶體組裝樹

// 1. 取得所有分類(按 sort, id 排序)
$rows = $pdo->query("SELECT id, name, pid FROM category ORDER BY sort, id")->fetchAll(PDO::FETCH_ASSOC);

// 2. 建立 ID 索引映射 + 初始化 children
$tree = [];
foreach ($rows as $row) {
    $tree[$row['id']] = $row;
    $tree[$row['id']]['children'] = [];
}

// 3. 利用引用一次遍歷組裝樹(O(n))
foreach ($tree as $id => &$node) {
    if ($node['pid'] != 0 && isset($tree[$node['pid']])) {
        $tree[$node['pid']]['children'][] = &$node;
    }
}
unset($node); // 釋放引用

// 4. 取得頂級分類樹
$roots = array_filter($tree, fn($n) => $n['pid'] == 0);

輸出範例

Array
(
    [1] => Array
        (
            [id] => 1
            [name] => 電腦
            [pid] => 0
            [children] => Array
                (
                    [0] => Array
                        (
                            [id] => 3
                            [name] => 筆記本
                            [pid] => 1
                            [children] => Array
                                (
                                    [0] => [id=>7, name=>超級本, ...]
                                    [1] => [id=>8, name=>遊戲本, ...]
                                )
                        )
                    [1] => [id=>4, name=>台式機, ...]
                )
        )
    [2] => [id=>2, name=>手機, children=>[...]]
)

優缺點分析

優點缺點
結構直觀,符合直覺分類量大(>5萬)時 PHP 組裝耗記憶體/時間
移動節點簡單:只需更新 pid查詢某節點所有後代需遞歸查詢或全表掃描
支援任意深度,無路徑長度限制難以在 SQL 層面高效實現「查詢所有子孫」
外鍵約束易於維護資料完整性

方案二:Path 路徑法—— Materialized Path

資料表結構

CREATE TABLE `category` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `name` VARCHAR(100) NOT NULL,
  `pid` INT UNSIGNED NOT NULL DEFAULT 0,
  `path` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '從根到父節點的 ID 路徑,如 0-1-3',
  `depth` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '層級深度',
  INDEX `idx_path` (`path`),
  INDEX `idx_pid` (`pid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

範例資料

id | name    | pid | path    | depth
---+---------+-----+---------+------
1  | 電腦     | 0   | 0       | 0
2  | 手機     | 0   | 0       | 0
3  | 筆記本   | 1   | 0-1     | 1
4  | 超級本   | 3   | 0-1-3   | 2
5  | 遊戲本   | 3   | 0-1-3   | 2

核心查詢:單條 SQL 獲取所有後代

-- 獲取 id=1 (電腦) 的所有後代分類
SELECT id, name, path, depth
FROM category
WHERE path LIKE (SELECT CONCAT(path, '-', id, '%') FROM category WHERE id = 1)
ORDER BY path;

結果:

id | name   | path   | depth
---+--------+--------+------
3  | 筆記本  | 0-1    | 1
4  | 超級本  | 0-1-3  | 2
5  | 遊戲本  | 0-1-3  | 2

其他常用查詢

-- 獲取祖先鏈(麵包屑導航)
SELECT * FROM category WHERE id IN (0,1,3) ORDER BY depth; -- 需應用層解析 path

-- 獲取直系子分類
SELECT * FROM category WHERE pid = 1;

-- 判斷 A 是否為 B 的後代
SELECT 1 FROM category WHERE id = B_id AND path LIKE (SELECT CONCAT(path, '-', id, '%') FROM category WHERE id = A_id);

節點移動:更新 path 的觸發器/應用層邏輯

-- 將節點 3 (及其子樹) 移動到節點 2 下
-- 1. 計算新 path 前綴
SELECT CONCAT(path, '-', id) FROM category WHERE id = 2; -- 返回 '0-2'

-- 2. 批次更新子樹 path
UPDATE category
SET path = REPLACE(path, '0-1', '0-2')
WHERE path LIKE '0-1-%' OR path = '0-1';

優缺點分析

優點缺點
查詢後代/祖先極快,單條 SQL,可用索引移動節點需更新整棵子樹 path(寫入放大)
適合「讀多寫少」場景(電商分類、知識庫)path 長度有限制(VARCHAR 500 約支援 50+ 層)
易於分頁、排序、深度過濾資料完整性依賴應用層/觸發器,外鍵難約束 path

深度對比總結表

維度遞歸樹Path 路徑法
讀取後代效能O(n) PHP 組裝 / 遞歸 SQLO(log n) 索引範圍掃描
移動節點成本O(1) 單行更新O(subtree_size) 批次更新
資料完整性外鍵友善需應用層保證
最大深度無限制受限於 VARCHAR 長度
適用場景寫入頻繁、深度較淺、分類數 < 1萬讀取極頻、深度較深、分類數 > 1萬
實現複雜度低中(需維護 path/depth)

進階方案:Closure Table(關係閉包表)

若兩者都不滿足(如需同時高效支援「移動子樹」與「查詢後代」),可引入 Closure Table:

CREATE TABLE category_closure (
  ancestor INT UNSIGNED NOT NULL,
  descendant INT UNSIGNED NOT NULL,
  depth INT UNSIGNED NOT NULL,
  PRIMARY KEY (ancestor, descendant)
);
  • 查詢後代:SELECT descendant FROM category_closure WHERE ancestor = 1 AND depth > 0
  • 移動子樹需更新 closure 表,但可用觸發器/存儲過程自動化
  • 支援任意深度、高效讀寫、標準 SQL 實現

選型決策樹

  1. 分類數 < 5,000 且結構變動頻繁 → 遞歸樹
  2. 分類數 > 10,000、讀多寫少、需高效後代查詢 → Path 路徑法
  3. 極高併發、頻繁移動子樹、需 ACID → Closure Table
  4. 簡單專案、快速原型 → 遞歸樹 + 快取

最佳實踐建議

  • 無論選哪種,**引入 Redis 快取完整樹結構**,降低 DB 壓力
  • Path 法建議加 depth 欄位,便於層級過濾與排序
  • 遞歸樹組裝時使用引用而非值複製,大幅降低記憶體佔用
  • 寫入操作(增/刪/移)建議封裝在 Service 層,統一維護衍生欄位
  • 考慮引入 nesting_set(巢狀集合)作為備選,適合靜態化程度高的目錄

無限分類無銀彈,核心在於讀寫比例、資料規模、變動頻率三要素的權衡。建議先從遞歸樹起步,隨業務增長平滑遷移至 Path 或 Closure Table。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved