数据库无限层级分类设计
轉2019-07-15 數據庫無限層級分類設計 - 云+社區 - 騰訊云
起步
在大多數的系統中,對內容進行分類是必要的。比如電商的商品分類;論壇的板塊等。
需求分析
分類之間的關系是怎樣的? 很明顯,一個分類下面可以是多個下級分類。反過來呢,一個下級分類能夠屬于幾個上級分類呢?這個并不確定,得看具體的業務需求。如果是多個實現上會更加復雜,為了討論層級設計,這里先限定每個分類僅有一個上級分類。
對于某個分類,需要支持的操作如下:
從使用頻率來看,查詢是大多數,畢竟分類也不會改來改去。所以性能考慮以查詢操作優先,特別是操作2 和操作 3。
方案一:記錄父分類的引用
這是一種比較常見且維護的一個方案,添加一個 pid 指向父分類的 id :
20190523135518.png
表中 pid 即直屬上級分類的 id,頂級分類設為 0.這種方案簡單易懂,僅用一個字典,沒有冗余信息,存儲空間占用極小,在數據庫層面是一個很好的設計。(本博客系統的分類就是采用這種結構)
對于分類的移動也非常友好,只要修改 pid 即可。而刪除分類也只需將其直屬子分類的 pid 改成該分類的 pid即可。但對于查詢該分類的所有上級至頂級分類或查詢就不友好了,需要進行遞歸。
查找所有父分類至頂級分類
SELECTID.LEVEL,t.* FROM(SELECT@id AS _id,( SELECT @id := pid FROM lab_goods WHERE id = @id) AS _pid,@l := @l + 1 AS LEVELFROMlab_goods,(SELECT @id := 100, @l := 0) as bWHERE@id > 0) as ID,lab_goods as t WHEREID._id = t.id ORDER BY `LEVEL`;復制
經實驗該語句可用,但當表中數據有一萬條,當查詢的層級有1000時,耗時 2 秒。也就是當分類的數量很多的時候,這個方案的性能并不好,但真實業務上會有這么多的分類嗎?
查詢某分類的所有下級分類為:
SELECTID. LEVEL,DATA .* FROM(SELECT@ids AS _ids,(SELECT @ids := GROUP_CONCAT(id) FROM lab_goods WHERE FIND_IN_SET(pid, @ids)) AS cids,@l := @l + 1 AS LEVELFROMlab_goods,(SELECT @ids := 10000, @l := 0) bWHERE@ids IS NOT NULL) id,lab_goods DATA WHEREFIND_IN_SET(DATA .id, ID._ids) ORDER BY `LEVEL`, id復制
當層級太多的時候性能是不佳的。而對于查詢某一級的所有分類,這個設計簡直是災難。
其實這個方案也是一開始就能想到的,在層級不深的情況下,這個方案不失為一個好的選擇。
方案二:添加路徑列表
針對方案一的短板,我們表中不僅僅記錄父分類id,還將它到頂級分類所有分類的id都保存下來。
| 1 | 家用電器 | 0 | - |
| 2 | 電腦辦公 | 0 | - |
| 3 | 大家電 | 1 | 1 |
| 4 | 生活電器 | 1 | 1 |
| 5 | 電風扇 | 4 | 1,4 |
| 6 | 電腦整機 | 2 | 1,2 |
每個分類都保存從頂級分類到父分類的所有id。查詢某某分下的所有下級就可以用 like 進行匹配:
SELECT id,name FROM pathlist WHERE path LIKE '1,%'復制
查詢某個分類的所有下級:
SELECT id,name FROM pathlist WHERE path LIKE '%2'復制
這個方案在各個方面都具有可以接受的性能,在上述例子中 1w 的數據,1k 的層級,僅耗時 0.0009s 。
但具有兩點不足:1. 不遵守數據庫范式,DBA看了想打人;2. 就是字段長度是有限的,但 longtext 最大能存儲 4G 的文本,我想沒有這么變態的層級數。所以這個分類在許多系統中使用。
方案三:基于ClosureTable的無限級分類存儲
另建一張表存儲節點之間的關系,其中包含了任何兩個有關系的節點的關聯信息:
2696840886-5acc5fd7a44f3.png
定義關系表 CategoryTree,其包含3個字段:
- ancestor 祖先:上級節點的id
- descendant 子代:下級節點的id
- distance 距離:子代到祖先中間隔了幾級
這三個字段的組合是唯一的,因為在樹中,一條路徑可以標識一個節點,所以可以直接把它們的組合作為主鍵。以圖為例,節點6到它上一級的節點(節點4)距離為1在數據庫中存儲為ancestor=4,descendant=6,distance=1,到上兩級的節點(節點1)距離為2,于是有 ancestor=1,descendant=6,distance=2 ,到根節點的距離為3也是如此,最后還要包含一個到自己的連接,當然距離就是0了。
這樣一來,不盡表中包含了所有的路徑信息,還在帶上了路徑中每個節點的位置(距離),對于樹結構常用的查詢都能夠很方便的處理。下面看看如何用用它來實現我們的需求。
子節點查詢
查詢id為5的節點的直屬子節點:
SELECT descendant FROM CategoryTree WHERE ancestor=5 AND distance=1復制
查詢所有子節點:
SELECT descendant FROM CategoryTree WHERE ancestor=5 AND distance>0復制
查詢某個上級節點的子節點,換句話說就是查詢具有指定上級節點的節點,也就是 ancestor 字段等于上級節點id即可,第二個距離 distance 決定了查詢的對象是由上級往下那一層的,等于1就是往下一層(直屬子節點),大于0就是所有子節點。這兩個查詢都是一句完成。
路徑查詢
查詢由根節點到id為10的節點之間的所有節點,按照層級由小到大排序
SELECT ancestor FROM CategoryTree WHERE descendant=10 ORDER BY distance DESC復制
查詢id為10的節點(含)到id為3的節點(不含)之間的所有節點,按照層級由小到大排序
SELECT ancestor FROM CategoryTree WHERE descendant=10 AND distance<(SELECT distance FROM CategoryTree WHERE descendant=10 AND ancestor=3) ORDER BY distance DESC復制
查詢路徑,只需要知道 descendant 即可,因為 descendant 字段所在的行就是記錄這個節點與其上級節點的關系。如果要過濾掉一些則可以限制 distance 的值。
查詢節點所在的層級(深度)
查詢id為5的節點是第幾級的
SELECT distance FROM CategoryTree WHERE descendant=5 AND ancestor=0復制
查詢id為5的節點是id為10的節點往下第幾級
SELECT distance FROM CategoryTree WHERE descendant=5 AND ancestor=10復制
查詢層級(深度)非常簡單,因為distance字段就是。直接以上下級的節點id為條件,查詢距離即可。
查詢某一層的所有節點
查詢所有第三層的節點
SELECT descendant FROM CategoryTree WHERE ancestor=0 AND distance=3復制
這個就不詳細說了,非常簡單。
插入
插入和移動就不是那么方便了,當一個節點插入到某個父節點下方時,它將具有與父節點相似的路徑,然后再加上一個自身連接即可。
所以插入操作需要兩條語句,第一條復制父節點的所有記錄,并把這些記錄的 distance 加一,因為子節點到每個上級節點的距離都比它的父節點多一。當然 descendant 也要改成自己的。
例如把id為10的節點插入到id為5的節點下方(這里用了Mysql的方言)
INSERT INTO CategoryTree(ancestor,descendant,distance) (SELECT ancestor,10,distance+1 FROM CategoryTree WHERE descendant=5)復制
然后就是加入自身連接的記錄。
INSERT INTO CategoryTree(ancestor,descendant,distance) VALUES(10,10,0)復制
刪除
如果刪除分類同時也刪除其所有下級分類那好辦,先找出該節點所有子級節點逐個刪除:
DELETE FROM CategoryTree WHERE descendant in (select descendant from CategoryTree where ancestor=4) ...復制
而如果節點刪除后是需要將所有下級分類都劃分到該節點的直系上級。比如刪除節點4,那么需要把4 的所有子節點都歸到該節點的直接上級:
select descendant from CategoryTree where ancestor=4 // 查詢4的所有子節點// 當子節點的父節點中超過該節點到 4節點距離時,距離- 1 update CategoryTree set distance = distance-1 where descendant=6 and distance > (select b.distance from (select * from CategoryTree where ancestor=4 and descendant=6 ) b); ...DELETE FROM CategoryTree WHERE ancestor=4 // 待下級節點的距離都修復完畢后刪除節點與下級節點的聯系 DELETE FROM CategoryTree WHERE descendant=4 // 刪除4節點本身復制
移動
節點的移動沒有很好的解決方法,因為新位置所在的深度、路徑都可能不一樣,這就導致移動操作不是僅靠UPDATE語句能完成的,這里選擇刪除+插入實現移動。
另外,在有子樹的情況下,上級節點的移動還將導致下級節點的路徑改變,所以移動上級節點之后還需要修復下級節點的記錄,這就需要遞歸所有下級節點。
刪除id=5節點的所有記錄
DELETE FROM CategoryTree WHERE descendant=5復制
然后配合上面一節的插入操作實現移動。
總結
ClosureTable是一種比較完美的解決方案,對于無限分層有很好的適應性,比較適用于大型系統。
總結
以上是生活随笔為你收集整理的数据库无限层级分类设计的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [pytorch、学习] - 3.9 多
- 下一篇: [pytorch、学习] - 3.10