電商系統常見需求:用戶在前台選擇一個大分類(如「所有商品」或「國產商品」),後台需一次性查出該分類及其所有**子孫分類**下的商品。但資料表設計中,商品只關聯到**最底層無子分類的葉子節點**(即 `goods.typeid = category.id` 且該 `id` 在 `category` 表中沒有對應的 `pid`)。若直接用 `WHERE typeid = 1` 只會查到直接掛在 ID=1 下的商品(通常為零),必須遞歸拿到所有葉子節點 ID,再用 `WHERE typeid IN (...)` 查詢。
| id | pid | path | categoryname |
|---|---|---|---|
| 1 | 0 | 0 | 所有商品 |
| 2 | 1 | 0-1 | 國產商品 |
| 4 | 2 | 0-1-2 | 國產水貨 |
| 5 | 2 | 0-1-2 | 國產行貨 |
| 3 | 1 | 0-1 | 進口商品 |
| 6 | 3 | 0-1-3 | 進口水貨 |
| 7 | 3 | 進口行貨 | |
| 8 | 1 | 0-1 | 測試類 |
| 9 | 1 | 0-1 | 雜貨專區 |
期望結果:
1 → 返回 4,5,6,7,8,9(所有葉子節點)2 → 返回 4,54 → 返回 4(自身即葉子)<?php
/**
* 遞歸獲取指定分類下所有「無子分類」的葉子節點 ID
* @param int $id 起始分類 ID
* @param string $table 分類表名,預設 category
* @param PDO $pdo 資料庫連接(建議注入 PDO 實例)
* @return string 以逗號分隔的 ID 字串,如 "4,5,6,7,8,9"
*/
function get_leaf_category_ids(int $id, string $table = 'category', PDO $pdo): string {
// 1. 查詢直接子分類
$stmt = $pdo->prepare("SELECT id FROM {$table} WHERE pid = ?");
$stmt->execute([$id]);
$children = $stmt->fetchAll(PDO::FETCH_COLUMN);
// 2. 無子分類 → 當前 ID 即葉子節點
if (empty($children)) {
return (string)$id;
}
// 3. 有子分類 → 遞歸處理每個子節點,合併結果
$leafIds = [];
foreach ($children as $childId) {
$leafIds[] = get_leaf_category_ids($childId, $table, $pdo);
}
return implode(',', $leafIds);
}
// 使用示例
$pdo = new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
echo get_leaf_category_ids(1, 'category', $pdo); // 輸出: 4,5,6,7,8,9
echo get_leaf_category_ids(2, 'category', $pdo); // 輸出: 4,5
echo get_leaf_category_ids(4, 'category', $pdo); // 輸出: 4
// 直接用於 SQL 查詢商品
$leafIds = get_leaf_category_ids(1, 'category', $pdo);
$sql = "SELECT * FROM goods WHERE typeid IN ({$leafIds})";
$goods = $pdo->query($sql)->fetchAll();
?>
IN (...) 可直接使用的 ID 列表。PDO 實例而非在函數內建立連接,符合依賴倒置原則,便於單元測試與連線複用。pid 參數。path 欄位(如 0-1-2),查詢 WHERE path LIKE '0-1-%' 可一次取出所有後代,再 PHP 端過濾葉子節點,減少遞歸查詢次數。category_closure 表記錄所有祖先-後代關係,查詢極快,寫入稍複雜。function get_all_descendant_ids(int $id, string $table, PDO $pdo): array {
$stmt = $pdo->prepare("SELECT id FROM {$table} WHERE pid = ?");
$stmt->execute([$id]);
$ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
foreach ($ids as $childId) {
$ids = array_merge($ids, get_all_descendant_ids($childId, $table, $pdo));
}
return $ids;
}
根據業務靈活選擇「僅葉子」或「所有後代」策略。