資料庫表設計-鄰接表、路徑枚舉、巢狀集、閉包表

巴扎黑
發布: 2017-06-23 15:05:36
原創
4025 人瀏覽過
   我們在設計資料庫的時候,是否會突破常規,找到最適合自己需求的設計方案,以下來舉例:
 
   常用的鄰接表設計,都會新增一個parent_id 字段,例如區域表(國、省、市、區):
#
CREATE TABLE Area ([id] [int]  NOT NULL,[name] [nvarchar]  (50) NULL,[parent_id] [int]  NULL,[type] [int]  NULL );
登入後複製

 

name:地域的名稱, parent_id 是父ID,省的父ID是國,市的父ID 為省,以此類推。
type 是區域的階級: 1:國,2:省,3:市,4:區
在層級比較確定的情況下,這麼設計表格沒有什麼問題,呼叫起來也很方便。
 
但是使用這個鄰接表設計方式,並不能滿足所有的需求,當我們不確定層級的情況下,假設我有一個註解結構:
 
    用鄰近表記錄此評論的資料(comments 資料表):
 
##authorcomment10小明#我不大認同這個觀點 21小張我也不認同345#67
#comment_id parent_id
##2 小紅 我同意樓上
#1 小全 你為什麼不認同呢
4 小明 我以前遇過這情況
5 小張 那也不代表你所說是對的
5 小新 這個視情況而定吧
      大家有沒發現,這麼設計表,如果要查詢一個節點的所有後代,是很難實現的,你可以使用關聯查詢來獲取一條評論和他的後代:
##
SELECT c1.*, c2.* FROM comments c1 LEFT OUTER JOIN comments c2 ON c2.parent_id = c1.comment_id;
登入後複製
 

     然而這個查詢只能取得兩層的資料。這種樹的特性就是可以任意深地拓展,你需要有對應的方法來取得它的深度資料。例如,可能需要計算一個評論分支的數量,或計算一個機械設備的所有的總開銷。

    在某些情況下,在專案中使用鄰近表正好適用。鄰接表設計的優點在於能快速的取得一個給定節點的直接父子節點,它也很容易插入新節點。如果這樣的需求就是你的專案對於分層資料的全部操作,那麼使用鄰接表就可以很好的工作了。
 
     遇到上述的樹模型,有幾種方案是可以考慮下的:路徑枚舉、巢狀集合、閉包表。這些解決方案通常看起來比鄰接表複雜很多,但它們的確使得某些使用鄰接表比較複雜或很低效的操作變得更簡單。如果你的專案確實需要提供這些操作,那麼這些設計會是鄰接表更好的選擇。
 
一、路徑枚舉
      在comments 表中,我們使用類型varchar 的path 欄位來取代原來的parent_id 欄位。這個path 欄位所儲存的內容為目前節點的最頂層祖先到它的自己的序列,就像UNIX的路徑一樣,你甚至可以使用 '/' 作為路徑的分隔符號。

 
##pathauthorcomment#1小明#小張1/2/31/4##5#1/ 4/5小明我以前遇過這情況#61/4/5/6小張那也不代表你所說是對的71/4/5/7小新這個視情況而定吧#
 
      你可以通过比较每个节点的路径来查询一个节点祖先。比如:要找到评论#7, 路径是 1/4/5/7一 的祖先,可以这么做:
SELECT * FROM comments AS c WHERE '1/4/5/7' LIKE c.path || '%' ;
登入後複製
    这句话查询语句会匹配到路径为 1/4/5/%,1/4/% 以及 1/% 的节点,而这些节点就是评论#7的祖先。
 
    同时还可以通过将LIKE 关键字两边的参数互换,来查询一个给定节点的所有后代。比如查询评论#4,路径path为 ‘1/4’ 的所有后代,可以使用如下语句:
SELECT * FROM comemnts AS c WHERE c.path LIKE '1/4' || '%' ;
登入後複製
    这句查询语句所有能找到的后台路径分别是:1/4/5、1/4/5/6、1/4/5/7。
 
     一旦你可以很简单地获取一棵子树或者从子孙节点到祖先节点的路径,你就可以很简单地实现更多的查询,如查询一颗子树所有节点上值的总和。
插入一个节点也可以像使用邻接表一样地简单。你所需要做的只是复制一份要插入节点的父亲节点路径,并将这个新节点的ID追加到路径末尾即可。
 
     路径枚举也存在一些缺点,比如数据库不能确保路径的格式总是正确或者路径中的节点确实存在。依赖于应用程序的逻辑代码来维护路径的字符串,并且验证字符串的正确性开销很大。无论将varchar 的长度设定为多大,依旧存在长度的限制,因而并不能够支持树结构无限扩展。
 
二、 嵌套集
 
     嵌套集解决方案是存储子孙节点的相关信息,而不是节点的直接祖先。我们使用两个数字来编码每个节点,从而表示这一信息,可以将这两个数字称为nsleft 和 nsright。
每个节点通过如下的方式确定nsleft 和nsright 的值:nsleft的数值小于该节点所有后代ID,同时nsright 的值大于该节点的所有后代的ID。这些数字和comment_id 的值并没有任何关联。
     确定这三个值(nsleft,comment_id,nsright)的简单方法是对树进行一次深度优先遍历,在逐层深入的过程中依次递增地分配nsleft的值,并在返回时依次递增地分配nsright的值。得到数据如下:
 
 
##1
我不大認同這個觀點 2 1/2
#我也不認同 #3
小紅 我同意樓上 4
小全 為什麼不認同呢
comment_id nsleft nsright author comment
1 1 14 小明 我不大认同这个观点
2 2 5 小张 我也不认同
3 3 4 小红 我同意楼上
4 6 13 小全 你为什么不认同呢
5 7 12 小明 我以前遇到过这情况
6 8 9 小张 那也不代表你所说是对的
7 10 11 小新 这个视情况而定吧
       一旦你为每个节点分配了这些数字,就可以使用它们来找到指定节点的祖先和后代。比如搜索评论#4及其所有后代,可以通过搜索哪些节点的ID在评论 #4 的nsleft 和 nsright 范围之间,例:
SELECT c2.* FROM comments AS c1 JOIN comments AS c2 ON c2.nsleft BETWEEN c1.nsleftAND c1.nsright WHERE c1.comment_id = 4;
登入後複製
比如搜索评论#6及其所有祖先,可以通过搜索#6的ID在哪些节点的nsleft 和 nsright 范围之间,例:
SELECT c2.* FROM comments AS c1 JOIN comments AS c2 ON c1.nsleft BETWEEN c2.nsleftAND c2.nsright WHERE c1.comment_id = 6;
登入後複製


     使用嵌套集设计的主要优势是,当你想要删除一个非叶子节点时,它的后代会自动替代被删除的节点,成为其直接祖先节点的直接后代。就是说已经自动减少了一层。
 
      然而,某些在邻接表的设计中表现得很简单的查询,比如获取一个节点的直接父亲或者直接后代,在嵌套集设计中会变得比较复杂。在嵌套集中,如果需要查询一个节点的直接父亲,我们会这么做,比如要找到评论#6 的直接父亲:
SELECT parent.* FROM comments AS c JOIN comments AS parent ON c.nsleft BETWEEN parent.nsleft AND parent.nsrightLEFT OUTER JOIN comments AS in_between ON c.nsleft BETWEEN in_between.nsleft AND in_between.nsrightAND in_between.nsleft BETWEEN parent.nsleft AND parent.nsright WHERE c.comment_id = 6AND in_between.comment_id IS NULL;
登入後複製
总之有些复杂。
 
    对树进行操作,比如插入和移动节点,使用嵌套集会比其它设计复杂很多。当插入一个新节点时,你需要重新计算新插入节点的相邻兄弟节点、祖先节点和它祖先节点的兄弟,来确保他们的左右值都比这个新节点的左值大。同时,如果这个新节点时一个非叶子节点,你还要检查它的子孙节点。
 
    如果简单快速查询是整个程序中最重要的部分,嵌套集是最好的选择,比操作单独的节点要方便快捷很多。然而,嵌套集的插入和移动节点是比较复杂的,因为需要重新分配左右值,如果你的应用程序需要频繁的插入、删除节点,那么嵌套集可能并不合适。
 
 
三、闭包表
 
     闭包表是解决分级存储的一个简单而优雅的解决方案,它记录了树中所有节点间的关系,而不仅仅只有那些直接的父子节点。
 
     在设计评论系统时,我们额外创建了一个叫 tree_paths 表,它包含两列,每一列都指向 comments 中的外键。
 
     我们不再使用comments 表存储树的结构,而是将树中任何具有(祖先 一 后代)关系的节点对都存储在treepaths 表里,即使这两个节点之间不是直接的父子关系;同时,我们还增加一行指向节点自己。
 
祖先 后代 祖先 后代 祖先 后代 祖先 后代
1 1 1 7 4 6 7 7
1 2 2 2 4 7    
1 3 2 3 5 5    
1 4 3 3 5 6    
1 5 4 4 5 7    
1 6 4 5 6 6    
      通过treepaths 表来获取祖先和后代比使用嵌套集更加的直接。例如要获取评论#4的后代,只需要在 treepaths 表中搜索祖先是评论 #4的行就行了。同样获取后代也是如此。
 
      要插入一个新的叶子节点,比如评论#6的一个子节点,应首先插入一条自己到自己的关系,然后搜索 treepaths 表中后代是评论#6 的节点,增加该节点和新插入节点的“祖先一后代”关系(新节点ID 应该为8):
INSERT INTO treepaths (ancestor, descendant)SELECT t.ancestor, 8FROM treepaths AS tWHERE t.descendant = 6UNION ALL SELECT 8, 8;
登入後複製
 
要删除一个叶子节点,比如评论#7, 应删除所有treepaths 表中后代为评论 #7 的行:
DELETE FROM treepaths WHERE descendant = 7;
登入後複製

 

要删除一颗完整的子树,比如评论#4 和它所有的后代,可删除所有在 treepaths 表中后代为 #4的行,以及那些以评论#4后代为后代的行。
 
     闭包表的设计比嵌套集更加的直接,两者都能快捷地查询给定节点的祖先和后代,但是闭包表能更加简单地维护分层信息。这两个设计都比使用邻接表或者路径枚举更方便地查询给定节点的直接后代和祖先。
 
     然而你可以优化闭包表来使它更方便地查询直接父亲节点或者子节点: 在 treepaths 表中添加一个 path_length 字段。一个节点的自我引用的path_length 为0,到它直接子节点的path_length 为1,再下一层为2,以此类推。这样查询起来就方便多了。
 
总结:你该使用哪种设计:
     种设计都各有优劣,如何选择设计,依赖于应用程序的哪种操作是你最需要性能上的优化。
 
方案 表数量 查询子 查询树 插入 删除 引用完整性
邻接表 1 简单 困难 简单 简单
枚举路径 1 简单 简单 简单 简单
嵌套集 1 困难 简单 困难 困难
闭包表 2 简单 简单 简单 简单
层级数据设计比较
1、邻接表是最方便的设计,并且很多程序员都了解它
2、如果你使用的数据库支持WITH 或者 CONNECT BY PRIOR 的递归查询,那能使得邻接表的查询更高效。
3、枚举路径能够很直观地展示出祖先到后代之间的路径,但同时由于它不能确保引用完整性,使得这个设计非常脆弱。枚举路径也使得数据的存储变得比较冗余。
4、嵌套集是一个聪明的解决方案,但可能过于聪明,它不能确保引用完整性。最好在一个查询性能要求很高而对其他要求一般的场合来使用它。
5、闭包表是最通用的设计,并且以上的方案也只有它能允许一个节点属于多棵树。它要求一张额外的表来存储关系,使用空间换时间的方案减少操作过程中由冗余的计算所造成的消耗。
 
      这几种设计方案只是我们日常设计中的一部分,开发中肯定会遇到更多的选择方案。选择哪一种方案,是需要切合实际,根据自己项目的需求,结合方案的优劣,选择最适合的一种。
 
     我遇到一些开发人员,为了敷衍了事,在设计数据库表时,只考虑能否完成眼下的任务,不太注重以后拓展的问题,不考虑查询起来是否耗性能。可能前期数据量不多的时候,看不出什么影响,但数据量稍微多一点的话,就已经显而易见了(例如:可以使用外联接查询的,偏偏要使用子查询)。
 
     我觉得设计数据库是一个很有趣且充满挑战的工作,它有时能体现你的视野有多宽广,有时它能让你睡不着觉,总之痛并快乐着。

以上是資料庫表設計-鄰接表、路徑枚舉、巢狀集、閉包表的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:php.cn
上一篇:pt-table-checksum校驗與pt-table-sync修復數據 下一篇:分享csv匯入資料到mysql實例
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
作者最新文章
最新問題
相關專題
更多>
熱門推薦
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!