去年作過一個項目,須要每日對上千個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操做。另外,刪除要分兩種狀況,就是子孫丟棄與子孫不丟棄。