三大主流方案對比
| 維度 | 遞歸查詢 (Adjacency List) | Path 路徑 (Materialized Path) | 左右值 (Nested Set) |
| 表結構 | id, parent_id, name | id, parent_id, name, path | id, name, lft, rgt |
| 查詢子樹 | 多次查詢/遞歸 PHP | 單查詢 WHERE path LIKE '%,1,%' | 單查詢 WHERE lft>2 AND rgt<11 |
| 查詢祖先 | 遞歸向上 | 單查詢 WHERE FIND_IN_SET(id, REPLACE(path,',',',')) | 單查詢 WHERE lft<2 AND rgt>11 |
| 插入節點 | 簡單 | 簡單 (path = parent.path + id + ',') | 複雜 (需更新所有後續 lft/rgt +2) |
| 移動子樹 | 簡單 (改 parent_id) | 中等 (批量更新 path 前綴) | 極其複雜 (需重算大量區間) |
| 刪除子樹 | 遞歸刪除 | 單查詢 DELETE WHERE path LIKE '%,3,%' | 單查詢但需修復間隙 |
| 適合場景 | 層級淺、變動頻繁 | 讀多寫少、層級深、需完整路徑 | 極少變動、頻繁讀取子樹/祖先 |
Path 路徑模型實現細節 (推薦通用)
// 表結構
CREATE TABLE categories (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
parent_id INT UNSIGNED DEFAULT 0,
name VARCHAR(100) NOT NULL,
path VARCHAR(500) NOT NULL COMMENT '如: ,1,5,12,',
depth TINYINT UNSIGNED DEFAULT 0,
sort INT DEFAULT 0,
INDEX idx_path (path),
INDEX idx_parent (parent_id)
) ENGINE=InnoDB;
// 新增節點
function addNode($parentId, $name) {
$pdo = getPDO();
if ($parentId == 0) {
$path = ',';
$depth = 0;
} else {
$stmt = $pdo->prepare("SELECT path, depth FROM categories WHERE id = ?");
$stmt->execute([$parentId]);
$parent = $stmt->fetch();
$path = $parent['path'] . $parentId . ',';
$depth = $parent['depth'] + 1;
}
$stmt = $pdo->prepare("INSERT INTO categories (parent_id, name, path, depth) VALUES (?, ?, ?, ?)");
$stmt->execute([$parentId, $name, $path, $depth]);
$newId = $pdo->lastInsertId();
// 更新自身 path 包含自己
$pdo->prepare("UPDATE categories SET path = CONCAT(path, ?, ',') WHERE id = ?")->execute([$newId, $newId]);
return $newId;
}
// 獲取完整樹 (按 path 排序即為層級順序)
function getTree() {
return getPDO()->query("SELECT id, name, path, depth FROM categories ORDER BY path")->fetchAll();
}
// 顯示縮進
foreach (getTree() as $node) {
echo str_repeat(' ', $node['depth']) . htmlspecialchars($node['name']) . "
";
}
// 移動子樹 (將節點 12 移至新父節點 8)
function moveSubtree($nodeId, $newParentId) {
$pdo = getPDO();
$pdo->beginTransaction();
try {
// 1. 獲取舊路徑前綴與新路徑前綴
$old = $pdo->prepare("SELECT path FROM categories WHERE id = ?")->execute([$nodeId])->fetchColumn();
$newParent = $pdo->prepare("SELECT path, id FROM categories WHERE id = ?")->execute([$newParentId])->fetch();
$newPrefix = $newParent['path'] . $newParent['id'] . ',';
$depthDiff = substr_count($newPrefix, ',') - substr_count($old, ','); // 深度差
// 2. 批量更新 path 與 depth
$pdo->prepare("UPDATE categories SET path = REPLACE(path, ?, ?), depth = depth + ? WHERE path LIKE ?")
->execute([$old, $newPrefix, $depthDiff, $old . '%']);
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
throw $e;
}
}
左右值模型適用場景
- 分類幾乎不變 (如地理區劃、圖書館分類法)。
- 需要高性能獲取「所有後代」或「所有祖先」。
- 不適合頻繁增刪改的業務分類。
選型建議
- 優先 Path 路徑: 實現簡單、查詢靈活、移動子樹只需批量 UPDATE path 前綴,適合 90% 業務場景。
- 極簡場景用遞歸: 層級 ≤ 3、數據量 < 1000、變動極頻繁,PHP 遞歸 + 緩存即可。
- 只讀/極少寫用左右值: 如多級評論展示、固定分類導航。
- 資料庫原生支援: PostgreSQL
ltree、MySQL 8.0 WITH RECURSIVE 可直接遞歸查詢,無需冗余字段。