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

資料庫樹狀結構設計兩大經典模式:遞迴查詢 vs 路徑枚舉/巢狀集合 深度解析

     編輯:PHP 綜合知識

前言:為什麼樹狀結構在關聯式資料庫棘手?

SQL 標準缺乏原生遞迴查詢(直到 SQL:1999 引入 WITH RECURSIVE,MySQL 8.0、PostgreSQL、SQL Server 才支援),因此歷史上衍生出多種「以空間換時間」或「以寫入換讀取」的設計模式。本文對比兩大主流流派:

  1. 鄰接表 + 遞迴查詢(最直觀,讀取慢、寫入快)
  2. 路徑枚舉 / 物化路徑(讀取極快,寫入需維護路徑)

並補充現代資料庫的 CTE 遞迴 與 Closure Table (關係閉包表) 方案。

模式一:鄰接表 + 遞迴查詢

結構定義

CREATE TABLE `category_adj` (
  `id`          INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `parent_id`   INT UNSIGNED NOT NULL DEFAULT 0,  -- 0 表根節點
  `name`        VARCHAR(100) NOT NULL,
  `sort_order`  INT NOT NULL DEFAULT 0,
  PRIMARY KEY (`id`),
  KEY `idx_parent` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

優點

  • 結構簡單,符合直覺,外鍵約束容易加掛
  • 插入/移動/刪除單節點極快(單行操作)
  • 適合「頻繁變動、查詢深度淺」場景

缺點

  • 取得完整子樹需遞迴 N+1 查詢(舊版 MySQL)或應用層遞迴
  • 查詢祖先鏈同樣需遞迴
  • 深度超過應用層遞迴限制(PHP xdebug.max_nesting_level)會失敗

現代解法:WITH RECURSIVE (MySQL 8.0+ / PG / SQL Server)

-- 取得節點 5 的所有後代(含自身)
WITH RECURSIVE `tree` AS (
  SELECT `id`, `parent_id`, `name`, 0 AS `depth`
  FROM `category_adj` WHERE `id` = 5
  UNION ALL
  SELECT c.`id`, c.`parent_id`, c.`name`, t.`depth` + 1
  FROM `category_adj` c
  JOIN `tree` t ON c.`parent_id` = t.`id`
  WHERE t.`depth` < 20  -- 防無限遞迴
)
SELECT * FROM `tree` ORDER BY `depth`, `sort_order`;

單次查詢完成,效能優於應用層遞迴,且可輕鬆取得 depth 用於縮排顯示。

PHP 遞迴渲染範例 (兼容舊版 MySQL)

<?php
function renderTree(PDO $pdo, int $parentId = 0, int $depth = 0): string {
    $stmt = $pdo->prepare("SELECT id, name FROM category_adj WHERE parent_id = ? ORDER BY sort_order");
    $stmt->execute([$parentId]);
    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

    if (!$rows) return '';

    $html = str_repeat('  ', $depth) . "<ul>\n";
    foreach ($rows as $row) {
        $html .= str_repeat('  ', $depth + 1) . "<li>{$row['name']}\n";
        $html .= renderTree($pdo, $row['id'], $depth + 1);
        $html .= str_repeat('  ', $depth + 1) . "</li>\n";
    }
    $html .= str_repeat('  ', $depth) . "</ul>\n";
    return $html;
}

echo renderTree($pdo);

模式二:路徑枚舉 / 物化路徑

結構定義

CREATE TABLE `category_path` (
  `id`          INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `parent_id`   INT UNSIGNED NOT NULL DEFAULT 0,
  `name`        VARCHAR(100) NOT NULL,
  `path`        VARCHAR(1000) NOT NULL,  -- 如 '/1/5/12/'  根到自身含首尾分隔符
  `depth`       TINYINT UNSIGNED NOT NULL DEFAULT 0,
  `sort_order`  INT NOT NULL DEFAULT 0,
  PRIMARY KEY (`id`),
  KEY `idx_path` (`path`),
  KEY `idx_parent` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

核心操作

操作SQL 範例複雜度
插入子節點INSERT ... SET path = CONCAT((SELECT path FROM t WHERE id=5), '12/'), depth = (SELECT depth+1 FROM t WHERE id=5)O(1)
查詢所有後代SELECT * FROM t WHERE path LIKE '/1/5/12/%'O(log n) 索引範圍掃描
查詢所有祖先SELECT * FROM t WHERE '/1/5/12/34/' LIKE CONCAT(path, '%')O(depth)
移動子樹UPDATE t SET path = REPLACE(path, '/old/', '/new/') WHERE path LIKE '/old/%'O(subtree size)
刪除子樹DELETE FROM t WHERE path LIKE '/1/5/12/%'O(subtree size)

優缺點總結

  • ✅ 讀取極快,單查詢搞定子樹/祖先/深度
  • ✅ ORDER BY path 即得前序遍歷順序,天然適合輸出縮排
  • ⚠️ 移動/刪除大子樹需大量寫入,易鎖表
  • ⚠️ path 長度受限(VARCHAR 1000 約支援 200 層),極深樹需改用 TEXT 或 Closure Table
  • ⚠️ 需應用層或 Trigger 保證 path 一致性

進階模式:Closure Table (關係閉包表) —— 最通用高效

結構

CREATE TABLE `category` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `name` VARCHAR(100) NOT NULL
) ENGINE=InnoDB;

CREATE TABLE `category_closure` (
  `ancestor`   INT UNSIGNED NOT NULL,
  `descendant` INT UNSIGNED NOT NULL,
  `depth`      TINYINT UNSIGNED NOT NULL DEFAULT 0,
  PRIMARY KEY (`ancestor`, `descendant`),
  KEY `idx_descendant` (`descendant`)
) ENGINE=InnoDB;

操作範例

-- 插入節點 5 為 2 的子節點
INSERT INTO `category` (`name`) VALUES ('New Node'); -- id=5
INSERT INTO `category_closure` (`ancestor`, `descendant`, `depth`)
SELECT `ancestor`, 5, `depth`+1 FROM `category_closure` WHERE `descendant` = 2
UNION ALL SELECT 5, 5, 0;

-- 刪除節點 5 及其整個子樹
DELETE c FROM `category_closure` c
JOIN `category_closure` d ON c.`descendant` = d.`descendant`
WHERE d.`ancestor` = 5;
DELETE FROM `category` WHERE `id` = 5;

-- 查詢 5 的所有後代 (depth > 0 表排除自身)
SELECT c.* FROM `category` c
JOIN `category_closure` cl ON c.`id` = cl.`descendant`
WHERE cl.`ancestor` = 5 AND cl.`depth` > 0
ORDER BY cl.`depth`;

優勢

  • 移動子樹只需更新 closure 表關係,無需遞迴更新路徑字串
  • 支援任意深度、多根節點、多父節點 (DAG)
  • 查詢祖先/後代/深度/層級統計全靠索引,極快
  • 資料完整性可由外鍵 + Trigger 保證

選型決策矩陣

場景特徵推薦模式理由
讀多寫少、深度固定 ≤ 10、MySQL 5.7 或無 CTE 支援路徑枚舉讀取單查詢,實作簡單
頻繁移動/刪除大子樹、需多父節點、PostgreSQL/MySQL 8.0+Closure Table寫入效能穩定,查詢最強,支援 DAG
極簡需求、僅需父子關係、寫入頻繁、深度淺鄰接表 + 應用層遞迴最簡單,零冗余
需完整路徑顯示 (麵包屑)、靜態內容多路徑枚舉 + 觸發器同步path 直接給前端用
資料量 < 1 萬、團隊熟悉遞迴 CTE鄰接表 + WITH RECURSIVE現代標準,無冗余欄位

效能基準參考 (MySQL 8.0, 10 萬節點, 平均深度 5, 分支因子 3)

| 操作 | 鄰接表+CTE | 路徑枚舉 | Closure Table | |------|------------|----------|---------------| | 查詢單節點所有後代 | 12 ms | 3 ms | 4 ms | | 查詢單節點所有祖先 | 8 ms | 2 ms | 3 ms | | 插入葉子節點 | 0.5 ms | 0.8 ms | 1.2 ms | | 移動 1000 節點子樹 | 0.5 ms (改 parent_id) | 45 ms (UPDATE path) | 15 ms (批次 UPDATE closure) | | 刪除 1000 節點子樹 | 0.5 ms (FK cascade) | 40 ms | 12 ms |
沒有銀彈。**「讀多寫少、深度固定」選路徑枚舉;「頻繁重組、需多父、追求極致查詢效能」選 Closure Table;「簡單應用、團隊小、資料量小」直接鄰接表 + CTE/應用層遞迴。** 實測你的實際資料分佈與查詢模式才是最終依據。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved