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。
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;
}
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);
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;
}
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,可用觸發器或應用層批次更新。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)
);
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 Table | 1 (JOIN) | 低 | 無上限 | 高(維護關係表) |
LIKE 索引效能佳。spatie/laravel-nestedset、kalnoy/nestedset(Modified Preorder Tree Traversal)Tree 類支援方案二/三DoctrineExtensions\Tree 支援 NestedSet、Closure、Materialized Path