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

去年作過一個項目,須要每日對上千個Android內存泄漏(OOM)時core dump出的hprof文件進行分析,但願藉助海量數據來快速定位內存泄漏的緣由。最終的分析結果是一個類森林,由於時隔較遠,只找到下面這個截圖了。php

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

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

 

鄰接列表

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

改方法的SQL以下app

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節點。spa

該結構SQL表示以下:3d

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字段rest

插入測試數據code

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操做。另外,刪除要分兩種狀況,就是子孫丟棄與子孫不丟棄。

相關文章
相關標籤/搜索