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

PHP 無限分類實現方案大全:遞歸、非遞歸、層級路徑三大演算法對比

     編輯:PHP 綜合知識

資料表基礎結構

CREATE TABLE `category` (
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
    `parent_id` INT UNSIGNED NOT NULL DEFAULT 0,
    `name` VARCHAR(50) NOT NULL,
    `sort` INT UNSIGNED NOT NULL DEFAULT 0,
    PRIMARY KEY (`id`),
    KEY `idx_parent` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

標準的「鄰接表模型」:每行記錄父節點 ID,根節點 parent_id=0。符合 3NF,寫入效能高,但查詢子樹需遞歸或多次 SQL。

方案一:遞歸查詢(N+1 問題)

function buildTreeRecursive($parentId = 0, $level = 0) {
    global $pdo;
    $stmt = $pdo->prepare("SELECT * FROM category WHERE parent_id = ? ORDER BY sort");
    $stmt->execute([$parentId]);
    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

    $html = '';
    foreach ($rows as $row) {
        $html .= str_repeat(' ', $level * 4) . $row['name'] . "<br>";
        $html .= buildTreeRecursive($row['id'], $level + 1);
    }
    return $html;
}
  • 優點:代碼極簡,邏輯直觀。
  • 缺點:每層發一條 SQL,深層分類導致大量查詢(N+1),資料庫壓力大,不適合高併發。

方案二:一次查詢 + PHP 遞歸組裝(經典折中)

function buildTreeArray($data, $parentId = 0) {
    $tree = [];
    foreach ($data as $row) {
        if ($row['parent_id'] == $parentId) {
            $children = buildTreeArray($data, $row['id']);
            if ($children) $row['children'] = $children;
            $tree[] = $row;
        }
    }
    return $tree;
}

// 使用
$stmt = $pdo->query("SELECT * FROM category ORDER BY sort");
$all = $stmt->fetchAll(PDO::FETCH_ASSOC);
$tree = buildTreeArray($all);
  • 優點:單次 SQL,PHP 端遞歸組裝樹結構,適用萬級分類。
  • 缺點:PHP 端需遍歷整個陣列,分類達數萬層級時記憶體與 CPU 明顯上升。

方案三:單次遍歷非遞歸組裝(引用技巧)

function buildTreeIterative($data) {
    $ref = [];  // id => node reference
    $tree = [];

    // 第一輪:建立所有節點引用
    foreach ($data as &$row) {
        $row['children'] = [];
        $ref[$row['id']] = &$row;
    }

    // 第二輪:掛載父子關係
    foreach ($data as &$row) {
        $pid = $row['parent_id'];
        if ($pid == 0 || !isset($ref[$pid])) {
            $tree[] = &$row;  // 根節點
        } else {
            $ref[$pid]['children'][] = &$row;
        }
    }
    return $tree;
}
  • 優點:O(N) 時間複雜度,無遞歸深度限制,記憶體效率高,適合 10 萬+ 分類。
  • 缺點:代碼較抽象,需理解 PHP 引用機制。

方案四:層級路徑 / Closure Table(資料庫層面優化)

1. 新增 path 欄位(物化路徑)

ALTER TABLE category ADD `path` VARCHAR(500) NOT NULL DEFAULT '' AFTER `parent_id`;
// 範例資料
id | parent_id | name      | path
1  | 0         | 電子產品   | 0001
2  | 1         | 手機      | 00010002
3  | 2         | 蘋果手機   | 000100020003
  • 查詢某節點所有子孫:SELECT * FROM category WHERE path LIKE '00010002%'
  • 查詢祖先鏈:SELECT * FROM category WHERE '000100020003' LIKE CONCAT(path, '%')
  • 移動子樹需更新該分支所有 path,可用觸發器或應用層批次更新。

2. 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),
    KEY idx_descendant (descendant)
);
  • 儲存所有祖先-後代關係(含自身 depth=0)。
  • 查詢子樹:SELECT c.* FROM category c JOIN category_closure cc ON c.id=cc.descendant WHERE cc.ancestor=2
  • 插入/移動/刪除節點時,需同步維護閉包表(可用存儲過程或應用層事務)。
  • 最適合頻繁讀取子樹、移動子樹的複雜場景。

效能對比總結表

方案查詢次數PHP 複雜度適合規模寫入成本
遞歸查詢N+1低< 100 節點低
一次查詢+遞歸組裝1中~ 1 萬低
單次遍歷引用組裝1中高~ 10 萬低
Path 物化路徑1 (LIKE)低~ 10 萬中(移動需更新路徑)
Closure Table1 (JOIN)低無上限高(維護關係表)

選型建議

  1. 中小型專案(分類 < 5000):方案二最划算,開發快、維護易。
  2. 大型電商/內容平台(分類 > 10 萬、頻繁移動子樹):採用 Closure Table,讀寫分離,查詢極快。
  3. 唯讀為主、層級固定(如地區編碼):Path 物化路徑最簡單,LIKE 索引效能佳。
  4. 避免方案一:除非極小型原型,否則上線後極易成效能瓶頸。

現代框架整合提示

  • Laravel:spatie/laravel-nestedset、kalnoy/nestedset(Modified Preorder Tree Traversal)
  • ThinkPHP:內建 Tree 類支援方案二/三
  • Symfony:DoctrineExtensions\Tree 支援 NestedSet、Closure、Materialized Path
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved