多級目錄樹(森林)的三種數據庫存儲結構介紹


去年做過一個項目,需要每日對上千個Android內存泄漏(OOM)時core dump出的hprof文件進行分析,希望借助海量數據來快速定位內存泄漏的原因。最終的分析結果是一個類森林,因為時隔較遠,只找到下面這個截圖了。

點擊打開折疊的項目,會看到該類的每個屬性,類有多少個實例,占用的大小等等信息,樹的深度可以達到10^2級別。重點是項目需要實時,每個hprof文件解析出來的節點達到5w+,千萬級節點已經由mapreduce進行過一次匯聚計算才出庫,在展示時,依然需要一次實時計算,當點擊項目時,需要快速將該類下所有的子孫節點占用的字節大小累加到該節點,因此對森林要有很高的查詢效率

好的查詢效率取決於好的存儲機構,眾所周知,多級目錄樹有如下三種存儲方法,這里主要講解這三種方式,並對其做了一些修改。這里使用同一個森林為模型(字母為節點名稱,數字為節點權重)

 

鄰接列表

這種方式最為開發人員熟知,每一個節點持有父節點的引用。為了更好的處理森林,抽象一個不存在的0節點,森林中所有樹掛在改節點下,將森林轉換為一顆樹來處理

改方法的SQL如下

1 CREATE TABLE node1 (
2   id INT AUTO_INCREMENT PRIMARY KEY ,
3   name VARCHAR(12) NOT NULL,
4   num INT NOT NULL DEFAULT 0 COMMENT '節點下葉子的數量、節點權重(可認為分類下產品數量)',
5   p_id INT NOT NULL DEFAULT 0 COMMENT '0表示根節點'
6 );

 此方法結構簡單,更新也簡單,但是在查詢子孫節點時,效率低下,不能滿足項目需求,因為這種方式過於簡單,這里就不寫該結構的查詢更新刪除SQL了。

進階鄰接列表

該方法僅僅需要在鄰接列表的基礎上,添加path_key(search_key)字段,該字段存儲從根節點到節點的標識路徑,這里依然抽象一個不存在的0節點。

該結構SQL表示如下:

1 CREATE TABLE node2 (
2   id INT AUTO_INCREMENT PRIMARY KEY ,
3   name VARCHAR(12) NOT NULL ,
4   num INT NOT NULL DEFAULT 0 COMMENT '節點下葉子的數量、節點權重(可認為分類下產品數量)',
5   p_id INT NOT NULL DEFAULT 0 COMMENT '0表示根節點',
6   search_key VARCHAR(128)  DEFAULT '' COMMENT '用來快速搜索子孫的key,存儲根節點到該節點的路徑',
7   level INT DEFAULT 0 COMMENT '層級'
8 );

重點在於search_key字段

插入測試數據

1 INSERT INTO node2(id,name, num, p_id,search_key) VALUES
2   (1,'A',10,0,'0-1'),
3   (2,'B',7,1,'0-1-2'),
4   (3,'C',3,1,'0-1-3'),
5   (4,'D',1,3,'0-1-3-4'),
6   (5,'E',2,3,'0-1-3-5'),
7   (6,'F',2,0,'0-6'),
8   (7,'G',2,6,'0-6-7');

查詢森林中的根節點

1 # 查詢森林的根節點
2 SELECT * FROM node2 WHERE p_id = 0 AND search_key LIKE '0-%' AND level = 0;

查詢節點A的所有子孫節點

1 # SELECT * FROM node2 WHERE search_key LIKE '{A.search_key}%';
2 SELECT * FROM node2 WHERE search_key LIKE '0-1-%';

更新某個節點的權值,只需要一次select與一次update操作

1 # 例如,更新節點C的權重
2 UPDATE node2,( SELECT sum(num) AS sum FROM node2 WHERE search_key LIKE '0-1-3-%') rt SET num = rt.sum WHERE id=3;

有節點權重累加時,將所有父輩權重再加1,只需要將該節點的search_key以'-' 切分,得到的就是所有父輩的id(0除外)。例如,將節點D的權重+1,這里使用where locate,實際更好是先將search_key split之后使用where in查詢

1 UPDATE node2,(SELECT search_key FROM node2 WHERE id = 4) rt SET num=num+1 WHERE locate(id,rt.search_key);

刪除某個節點,比如刪除B節點

假設刪除節點子孫全部清理

1 DELETE FROM node2 WHERE search_key LIKE '0-1-2%';

假設子節點不清除 ,將子孫節點掛到父輩節點下,則需要更新兒子節點的search_key、p_id、level字段

1 # UPDATE node2, SET p_id = {B.p_id}
2 UPDATE node2 SET p_id = 1 AND search_key = concat('0-1-',id);
3 # 刪除
4 DELETE FROM node2 WHERE id=2;

方式2僅僅添加了一個路徑字段,使得查詢變的簡單,並且更新也容易,在節點深度有限的情況下,個人認為第二種方式是比較優的選擇。

先序樹結構

先序樹即按照先序遍歷的方式,給節點分配左右值,第一次到達該節點時,設置左值,第二次到達該節點,設置右值,每走一步,序號加1。這里以一段php代碼來生成第一張圖片中的森林的先序樹結構

 1 <?php
 2 /**
 3  * Created by PhpStorm.
 4  * User: samlv
 5  * Date: 2017/2/23
 6  * Time: 16:44
 7  */
 8 
 9 $forest = array(
10     array(
11         'name'  =>  'A',
12         'num'   =>  10,
13         'childs' => array(
14             array(
15                 'name' => 'B',
16                 'num'   =>  7,
17                 'childs' => array()
18             ),
19             array(
20                 'name' => 'C',
21                 'num'   =>  3,
22                 'childs' => array(
23                     array(
24                         'name' => 'D',
25                         'num'   =>  1,
26                         'childs' => array()
27                     ),
28                     array(
29                         'name' => 'E',
30                         'num'   =>  2,
31                         'childs' => array()
32                     )
33                 )
34             )
35         )
36     ),
37     array(
38         'name' => 'F',
39         'num'   =>  2,
40         'childs' => array(
41             array(
42                 'name' => 'G',
43                 'num'   =>  2,
44                 'childs' => array()
45             )
46         )
47     )
48 );
49 
50 function pre_order(& $forset,$level){
51     static $i = 1;
52     static $tree_id = 1;
53     foreach($forset as & $node){
54         $node['lft'] = $i ++ ;
55         if(!empty($node['childs'])){
56             pre_order($node['childs'],$level + 1);
57         }
58         $node['rgt'] = $i ++ ;
59         echo "{$node['lft']} | {$node['name']} | {$node['rgt']} \n";
60         //echo "insert into node3 (tree_id, name, num, lft, rgt, level) VALUE ($tree_id,'{$node['name']}',{$node['num']},{$node['lft']},{$node['rgt']},$level); \n";
61         if($node['lft'] === 1){
62             // 遍歷新的樹
63             $i = 1;
64             $tree_id ++;
65         }
66     }
67 }
68 
69 pre_order($forest,1);

運行結果

C:\xampp\php\php.exe D:\www\php-all\sp.php
2 | B | 3 
5 | D | 6 
7 | E | 8 
4 | C | 9 
1 | A | 10 
2 | G | 3 
1 | F | 4 

將結果解析成圖片如下

我后續以這段代碼生成了SQL insert代碼。用來存儲該森林的SQL如下

 1 CREATE TABLE node3 (
 2   id INT AUTO_INCREMENT PRIMARY KEY ,
 3   tree_id INT NOT NULL COMMENT '為保證對某一棵的操作不影響森林中的其他書',
 4   name VARCHAR(12) NOT NULL ,
 5   num INT NOT NULL DEFAULT 0 COMMENT '節點下葉子的數量、節點權重(可認為分類下產品數量)',
 6   lft INT NOT NULL ,
 7   rgt INT NOT NULL ,
 8   level INT DEFAULT 0
 9 );
10 insert into node3 (tree_id, name, num, lft, rgt, level) VALUE (1,'B',7,2,3,2);
11 insert into node3 (tree_id, name, num, lft, rgt, level) VALUE (1,'D',1,5,6,3);
12 insert into node3 (tree_id, name, num, lft, rgt, level) VALUE (1,'E',2,7,8,3);
13 insert into node3 (tree_id, name, num, lft, rgt, level) VALUE (1,'C',3,4,9,2);
14 insert into node3 (tree_id, name, num, lft, rgt, level) VALUE (1,'A',10,1,10,1);
15 insert into node3 (tree_id, name, num, lft, rgt, level) VALUE (2,'G',2,2,3,2);
16 insert into node3 (tree_id, name, num, lft, rgt, level) VALUE (2,'F',2,1,4,1);

這里加入了一個tree_id字段,用來保證對一棵樹內的更新操作,不會影響到別的樹,有利於提高效率。

對該結構的操作可以非常復雜,這里說兩種基本的單元操作。

append操作,待加入節點不帶子節點

remove操作,待刪除節點沒有子節點

首先來看append操作,我想在節點C下添加一個M節點,如圖

仔細看可以發現,在已有一個節點下append一個節點M的話,M的左右值應該連續的,按照先序遍歷的順序,只需要將走在其后的節點的左右值分別+2,並且M節點的父節點的右值必然也要+2。下面以一個mysql function來實現append過程

 1 DROP FUNCTION IF EXISTS append_node;
 2 CREATE FUNCTION append_node(param_name VARCHAR(12), param_num INT, param_p_id INT, param_tree_id INT)
 3   returns INT
 4   BEGIN
 5     DECLARE p_lft INT;
 6     DECLARE p_rgt INT;
 7     DECLARE p_level INT;
 8     DECLARE ret INT;
 9 
10     SELECT lft,rgt,level INTO p_lft,p_rgt,p_level FROM node3 WHERE tree_id = param_tree_id AND id=param_p_id ;
11     # 比前一個節點左值大的需要加2
12     UPDATE node3 SET lft = lft + 2 WHERE tree_id = param_tree_id AND lft > p_lft;
13     # 按照先序遍歷規則,在一個節點M下添加節點之后,節點M的右值必然也要加2
14     UPDATE node3 SET rgt = rgt + 2 WHERE tree_id = param_tree_id AND rgt >= p_rgt;
15     INSERT INTO node3 (tree_id,name, num, lft, rgt, level)
16       VALUE (param_tree_id,param_name,param_num,p_lft + 1,p_rgt + 1,p_level + 1);
17     SELECT LAST_INSERT_ID() INTO ret;
18     RETURN ret;
19   END;
20 
21 # 在節點C下添加節點M,已知節點C的id為4,tree_id為1。
22 select append_node('M', 7, 4, 1);

除了append操作之外,還可以有insert操作,如下圖

其實insert操作可以看做是append 與 delete或者update結合的操作,不一定要一步到位,可以由單元操作來組成。

至於remove操作,則恰好是append操作的反向操作,需要將被刪除節點后面的節點的左右值-2。最后進行delete操作。另外,刪除要分兩種情況,就是子孫丟棄與子孫不丟棄。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM