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