[英]Categories and subcategories from one mysql table
您正在使用SQL Antipatterns這本書稱為“鄰接表”的設計。 這是大多數程序員選擇在關系數據庫中表示層次結構數據的方式,因為它相當健壯,存儲經濟,易於理解並且可以使用外鍵約束來防止創建無效的父子關系。 但是,它的最大缺點是您無法在單個查詢中檢索整個樹或子樹。 除非使用的數據庫不支持遞歸查詢,否則您只能獲取節點的直接子節點,其直接父節點或其兄弟姐妹。 遞歸僅由某些數據庫實現,並且由於沒有標准,它們都以不同的方式實現。
本書提供了許多用於處理層次結構的替代方案,我最喜歡的是Closure Table。 用這種方法,您無需將父級的行存儲在類別表中,而是將關系數據存儲在一個完全獨立的表中。 您的類別表將具有唯一的類別ID,類別名稱等,然后您的關閉表將存儲以下信息:
因此,您的類別表將如下所示:
CREATE TABLE categories (
cat_id INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
cat_name VARCHAR(64),
# Other columns...
) ENGINE=InnoDB;
和您關聯的關閉表看起來像...
CREATE TABLE category_relations (
ancestor_id INT(11) UNSIGNED,
descendant_id INT(11) UNSIGNED,
depth INT(11) UNSIGNED
PRIMARY KEY (ancestor_id, descendant_id),
FOREIGN KEY (ancestor_id) REFERENCES categories.cat_id,
FOREIGN KEY (descendant_id) REFERENCES categories.cat_id
) ENGINE=InnoDB;
祖先和后代ID的組合是閉合表的主鍵。 深度字段將在以后用於優化某些查詢類別,但是我們已經超越了自己。
現在,無論何時將新值插入類別表中,現在都將始終始終將至少一個節點插入類別關系表中。
INSERT INTO categories (
cat_name
) VALUES (
'animal'
);
獲取新插入的節點的ID后,也應運行以下查詢(我們假設新行的ID為1):
INSERT INTO category_relations (
ancestor_id,
descendant_id,
depth
) VALUES (
1,
1,
0
);
這稱為自引用行。 您可以使用觸發器或代碼中的其他操作來完成此操作,這取決於您。
現在,我們有一個“動物”節點,可以將其視為根節點。 如果要添加“貓科動物”字段,則可以執行以下操作:
INSERT INTO categories (
cat_name
) VALUES (
'feline'
);
# We'll assume the new ID is 2
INSERT INTO category_relations (
ancestor_id,
descendant_id,
depth
) VALUES (
2,
2,
0
);
但是,如何使“貓科動物”成為“動物”的孩子呢? 我們通過在關系表中插入其他節點來做到這一點:
INSERT INTO category_relations (
ancestor_id,
descendant_id,
depth
) VALUES (
1,
2,
1
);
現在說我們要在“貓科動物”中添加“家貓”和“獅子”孩子
INSERT INTO categories (
cat_name
) VALUES (
'house cat'
);
# We'll assume the new ID is 3
INSERT INTO category_relations (
ancestor_id,
descendant_id,
depth
) VALUES (
3,
3,
0
);
INSERT INTO categories (
cat_name
) VALUES (
'lion'
);
# We'll assume the new ID is 4
INSERT INTO category_relations (
ancestor_id,
descendant_id,
depth
) VALUES (
4,
4,
0
);
現在,我們將那些節點作為“貓科動物”節點的子級。
INSERT INTO category_relations (
ancestor_id,
descendant_id,
depth
) VALUES (
2,
3,
1
);
INSERT INTO category_relations (
ancestor_id,
descendant_id,
depth
) VALUES (
2,
4,
1
);
現在,家貓和獅子是貓科動物的孩子,但它們也必須是動物的祖先。
INSERT INTO category_relations (
ancestor_id,
descendant_id,
depth
) VALUES (
1,
3,
2
);
INSERT INTO category_relations (
ancestor_id,
descendant_id,
depth
) VALUES (
1,
4,
2
);
至此,該表應如下所示:
ancestor_id | descendant_id | depth
=============+===============+=======
1 | 1 | 0
2 | 2 | 0
3 | 3 | 0
4 | 4 | 0
1 | 2 | 1
2 | 3 | 1
2 | 4 | 1
1 | 3 | 2
1 | 4 | 2
現在,如果您要獲取整個動物樹,則可以進行簡單的查詢,而無需迭代或遞歸。
SELECT c.*
FROM category AS c
JOIN category_relations AS r ON c.cat_id = r.descendant_id
WHERE r.ancestor_id = 1;
該查詢將在關系表中選擇以下行:
ancestor_id | descendant_id | depth
=============+===============+=======
1 | 1 | 0
1 | 2 | 1
1 | 3 | 2
1 | 4 | 2
並返回類別行:
cat_id | cat_name
========+==========
1 | animal
2 | feline
3 | house cat
4 | lion
如果我們想要以貓科動物開頭的子樹,我們只需使用貓科動物類別ID作為祖先即可。
SELECT c.*
FROM categories AS c
JOIN category_relations AS r ON c.cat_id = r.descendant_id
WHERE r.ancestor_id = 2;
ancestor_id | descendant_id | depth
=============+===============+=======
2 | 2 | 0
2 | 3 | 1
2 | 4 | 1
cat_id | cat_name
========+==========
2 | feline
3 | house cat
4 | lion
但是,如果您只想要給定節點的子節點,而不是整個子樹,該怎么辦? 這就是深度場的來源:
SELECT c.*
FROM categories AS c
JOIN category_relations AS r ON c.cat_id = r.descendant_id
WHERE r.ancestor_id = 1
AND r.depth = 1;
ancestor_id | descendant_id | depth
=============+===============+=======
1 | 2 | 1
cat_id | cat_name
========+==========
2 | feline
希望您現在就明白了。 您還可以做更多的事情,例如刪除和移動子樹,等等。
此處的示例可能使您看起來似乎需要做大量工作來維護閉合表,但是可以通過觸發器,巧妙地使用聯接查詢等使我在此處手動完成的許多工作自動化。 ,但我將不做更詳細的介紹,因為《 SQL Antipatterns》這本書涵蓋的內容遠勝於我的能力,並且互聯網上也有很多封閉表教程。
您可能還認為,與簡單的鄰接表相比,將存儲許多其他數據,而且您會正確的。 但是,在大多數數據庫中,存儲整數表通常是非常有效的,除非您要處理的樹非常大,否則不應遇到閉包表引起的任何短缺。 所使用的額外存儲空間是您不必經歷使用迭代或遞歸遍歷樹的繁瑣操作而付出的代價。
如果要處理存儲在SQL中的任意樹結構,我強烈建議您查看閉包表。 在非常簡單的情況下,鄰接表模型很好,因此您不應該完全打折,但是當事情開始變得更加復雜時,閉包表方法會帶來很多好處。
您可以使用類似以下示例的某種遞歸函數:
// Recursive function
function getList($rows, $id=0){
// Start a new list
$newList = null;
foreach($rows as $key => $row) {
if ($row['parent_id'] == $id) {
// Add an UL tag when LI exists
$newList == null ? $newList = "<ul>" : $newList;
$newList .= "<li>";
$newList .= "<a href=\"\">{$row['category']}</a>";
// Find childrens
$newList .= getList(array_slice($rows,$key),$row['id']);
$newList .= "</li>";
}
}
// Add an UL end tag
strlen($newList) > 0 ? $newList .= "</ul>" : $newList;
return $newList;
}
// Your database registers
$rows = [
['id'=>1,'category'=>'Indland','parent_id'=>0],
['id'=>2,'category'=>'Udland','parent_id'=>0],
['id'=>3,'category'=>'Sport','parent_id'=>0],
['id'=>4,'category'=>'Handbold','parent_id'=>3],
['id'=>5,'category'=>'Fodbold','parent_id'=>3],
['id'=>6,'category'=>'Sample','parent_id'=>4]
];
print_r(getList($rows));
但它僅適用於有序行。
試試這個查詢
select a.id,a.category,b.category from tablename a
left outer join tablename b on a.id=b.parent_id
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.