一、基本概念
對于層次查詢需要掌握:
1)理解層次查詢的基本概念,識別需求中何時要用到層次查詢的能力。
2)建立和格式化樹形報表(tree report)。
3)修剪樹形結構的節點(node)和枝(branches)。
4)掌握層次查詢中的各種關鍵字和10G新特性。
關鍵詞:tree,root(根),node,leaf(葉子),branch(樹枝,分支)
-
子句:START WITH、CONNECT BY、10g的ORDER SIBLINGS BY
-
操作符:PRIOR、10g的CONNECT_BY_ROOT
-
偽列:LEVEL,10g的CONNECT_BY_ISCYCLE、CONNECT_BY_ISLEAF
-
參數或關鍵字:10g的NOCYCLE
-
函數:SYS_CONNECT_BY_PATH
解決問題:數據構造、連續數問題、線路規劃問題、格式化樹形結構、字符串合并等。
本節例子來源于表s_emp,表結構和數據如下:
看上面的表s_emp,使用層次查詢,我們可以獲得一張表基于層次關系的數據集合。Oracle是一 種關系型數據庫,在表中不可能以層次的關系存放數據。但是我們可以通過一定的規則,使用tree walking (樹的遍歷或樹的查找) 來獲得層次關系的數據。Hierarical query是一種獲得樹的層析關系報表的方法。
樹形結構的數據集合,存在于我們日常生活中的很多地方,比如考慮一個家族關系,有長輩,長輩下面有子女,子女下面還可以有子女,這轉化為層次或等級關系就是:根節點只有一個,下面有子節點,子節點下面還有子節點,這樣組成了一棵樹。 (有時候,根節點root不一定只有一個,嚴格意義上說,這種情況不是一個嚴格的樹)
當一種層次關系是存在一個表的行中,那么這種層次是可以獲得的。例如,我們看s_emp表,對于title:VP,我們知道這些行中都包含manager_id=1,也就是說,這些行屬于id=1的雇員的下屬雇員,那么有title=vp又可以獲得一系列的層次,而這些層次的跟則是id=1這個雇員。由此,得到一棵樹形結構數據集合。
層次樹或等級樹,在譬如 家族關系,育種關系,組織管理,產品裝配,人類進化,科學研究 等領 廣泛應用。
下面我們就根據s_emp這張表,根據職位大小來描述一個樹形結構圖。如圖:
(只顯示部分樹形結構)
樹形結構的父子關系,你可以控制:
1)遍歷樹的方向,是自上而下,還是自下而上。
2)確定層次的開始點(root)的位置。
層次查詢語句正是從這兩個方面來確定的,start with確定開始點,connect by確定遍歷的方向。
二、層次查詢介紹
gt; gt; gt; gt;
1、語法:
層次查詢是通過start with和connect by子句標識的。
1)其中level關鍵字是可選的,表示等級,表示root,2表示root的child,其他相同的規則。 它可以在where,connect by,select里存在。
2)where條件限制了查詢返回的行,但是不影響層次關系,不滿足條件的節點不返回,但是這個不滿足條件的節點的下層child不受影響。因為where是在start with和connect by之后執行的(join是在connect by之前做的)。
3)start with是表示開始節點(root節點),對于一個真實的層次關系,必須要有這個子句,但是不是必須的, 沒有start with表示每行都是根然后遍歷。
4)connect by prior是指定父子關系,選擇子節點,其中prior的位置不一定要在connect by之后, 對于一個真實的層次關系,這也是必須的,不能省略。
5)一般層次查詢不能直接order by,會破壞層次,但是10g可以使用ORDER SIBLINGS BY。
gt; gt; gt; gt;
2、遍歷樹
-
Start with 子句:
首先必須確定startpoint,通過start with子句,后面加條件,這個條件是任何合法的條件表達式。
Start with確定將哪行作為root,如果沒有start with,則每行都當作root,然后查找其后代,這不是一個真實的查詢。 start with后面可以使用子查詢 ,如果有where條件,則會截斷層次中的相關滿足條件的節點,但是不影響整個層次結構。 可以帶多個條件。
對于s_emp,從root title=president開始,語句如下:
select level,id,manager_id,last_name,title from s_emp
start with manager_id is null
connect by prior id=manager_id;
-
Connect by 子句:
Connect by與prior確定一個層次查詢的條件和遍歷的方向(prior確定)。
Connect by prior column_1=column_2; (其中prior表示前一個節點的意思,可以在connect by等號的前后,列之前,也可以放到select中的列之前)。
Connect by也可以帶多個條件,比如 connect by prior id=manager_id and idgt;10;
connect by也可以帶level之類的,再比如connect_by_isleaf,connect_by_root,有的在低版本比如10.1上可能報錯,高版本沒有問題,如connect_by_root。
-
遍歷方向:
1 )自頂向下遍歷: 就是先由根節點,然后遍歷子節點。column_1表示父key,column_2表示子key。即這種情況下: connect by prior 父key=子key表示自頂向下,等同于connect by 子key=prior 父key.
例如:
select level,id,manager_id,last_name, title from s_emp
start with manager_id=2
connect by id=prior manager_id;--自下而上遍歷
2 )自底向上遍歷: 就是先由最底層的子節點,遍歷一直找到根節點。與上面的相反。
Connect by之后不能有子查詢,但是可以加其他條件,比如加上and id !=2等。這句話則會截斷樹枝,如果id=2的這個節點下面有很多子孫后代,則全部截斷不顯示。比如下面的句子:
select level,id,manager_id,last_name,title from s_emp
start with title=(select title from s_emp where manager_id is null)
connect by prior id=manager_id and id!=2;
不來不加上id!=2,共有25條記錄,現在加上這個條件只有9條記錄了,因為id=2的后代包括自己共有16條記錄,全部被截斷。
注意connect by中用level修剪的問題: 在connect by里修剪效率比where過濾好,因為不用全部遞歸,注意level條件。
SELECT ID,manager_id,LEVEL
FROM s_emp
START WITH manager_id IS NULL
CONNECT BY PRIOR ID = manager_id
AND LEVEL lt;=3
;
第1次遞歸level=2,滿足條件,則繼續下次遞歸到3結束,如果是levelgt;1也可以,levelgt;=2也可以,相當于沒有加level,但是levelgt;3則第1次遞歸不滿足,也就是返回根了。level=3同樣,要從原理上搞清楚遞歸。
gt; gt; gt; gt;
3、使用level和lpad格式化報表
Level是層次查詢的一個偽列,如果有level,必須有connect by,start with可以沒有。Lpad是在一個string的左邊添加一定長度的字符,并且滿足中間的參數長度要求,不滿足自動添加。例如現在的需求是,輸出s_emp等級報表,root節點的last_name不變,比如第2等級,也就是level=2的前面加兩個’_’符號,level=3的前面加4個。這樣我們可以得到一個公式就是:
Lpad(last_name,length(last_name) (level*2)-2,’_’),lpad 對中文處理有點問題,用lenthb
可以得出下面的語句:
select level,id,manager_id,lpad(last_name,length(last_name) (level*2)-2,'_'),title,prior last_name from s_emp
start with manager_id is null
connect by prior id=manager_id;
select中的prior last_name是輸出其父親的last_name.這個語句 執行的結果如下:
再如:
select level,id,manager_id,lpad('|--'||last_name,length('|--' ||last_name) (level*5-5),' '),title,prior last_name from s_emp
start with manager_id is null
connect by prior id=manager_id;
-- 因為第2個沒有用level和括號拼湊,所以改為-2,顯示層次數
select level,id,manager_id,lpad('|('||level||')--'||last_name,length('|--' ||last_name) (level*5-2),' '),title,prior last_name from s_emp
start with manager_id is null
connect by prior id=manager_id;
select level,id,manager_id,lpad('--'||level||'--'||last_name,length(last_name) (level*5-2),' '),title,prior last_name from s_emp
start with manager_id is null
connect by prior id=manager_id;
gt; gt; gt; gt;
4、修剪branches
上面已經提到,where子句會將節點刪除,但是 其后代不會受到影響 ,connect by 中加上條件會將滿足條件的 整個樹枝包括后代都刪除 。要注意, 如果是connect by之后加條件正好條件選到根,那么結果和沒有加一樣 ,對于結果的過濾,如果能加在connect by之后,則盡量加在connect by之后,從而盡早過濾數據,提高效率lt;connect by是發生在where之前的gt;。
如圖所示:
gt; gt; gt; gt;
5、層次查詢限制
1)層次查詢from之后如果是table,只能是一個table,不能有join。
2)from之后如果是view,則view不能是帶join的。
3)使用order by子句,order子句是在等級層次做完之后開始的,所以對于層次查詢來說沒有什么意義,除非特別關注level,獲得某行在層次中的深度,但是這兩種都會破壞層次。見5.3增強特性中的使用siblings排序。
4)在start with中表達式可以有子查詢,但是connect by中不能有子查詢。
以上是10g之前的限制,10g之后可以使用帶join的表和視圖,connect by中可以使用子查詢。
理解關鍵操作:
層次查詢的關鍵操作組件就是start with,connect bystart with確定樹的開始節點,如果這個語句確定的開始節點有多個(沒有start with語句,那么每行都是一個根節點),那么根據層次查詢的算法規則,會有多個符合start with條件的節點作為不同樹的根節點,然后以每個根節點為開始搜索點,根據connect by條件來搜索符合條件的子節點,當所有符合connect by條件的根節點都找完其子節點,層次查詢結束。
層次查詢是在同一層where條件之前執行的,這個要注意,所以where條件不會破壞層次查詢的節點所屬根和層次(level),where只是簡單的起到過濾結果的作用。這個容易出錯,不要以為where寫在from后connect by前就認為where先執行,那是不正確的,看下面結果(scott下的表):
-- 最普通的層次查詢,根節點mgr is null確定只有一個,empno=7839為root
SQLgt; select empno,mgr,sys_connect_by_path(mgr,'-gt;') ptree from emp
2start with mgr is null
3connect by prior empno = mgr;
EMPNO MGR PTREE
----- ----- ---------------------------------------------------------------------------
7839 -gt;
75667839 -gt;-gt;7839
79027566 -gt;-gt;7839-gt;7566
73697902 -gt;-gt;7839-gt;7566-gt;7902
76987839 -gt;-gt;7839
74997698 -gt;-gt;7839-gt;7698
75217698 -gt;-gt;7839-gt;7698
76547698 -gt;-gt;7839-gt;7698
78447698 -gt;-gt;7839-gt;7698
79007698 -gt;-gt;7839-gt;7698
77827839 -gt;-gt;7839
79347782 -gt;-gt;7839-gt;7782
12 rows selected
--看start with增加了個or條件,那么現在的根就有兩個,一個是上面的,另一個是mgr=7566,因此,下面的結果包含了:
--上面的結果,另外多了棵樹的根是7566的,第1和第2行
--你的sql如果把start with中的flag=0卻掉就是這種情況了,暫且不討論你的connect by中flag=0去掉的情況
SQLgt; select empno,mgr,sys_connect_by_path(mgr,'-gt;') ptree from emp
2start with mgr is null or mgr =7566
3connect by prior empno = mgr;
EMPNO MGR PTREE
----- ----- ------------------------------------------------------------------------
79027566 -gt;7566
73697902 -gt;7566-gt;7902
7839 -gt;
75667839 -gt;-gt;7839
79027566 -gt;-gt;7839-gt;7566
73697902 -gt;-gt;7839-gt;7566-gt;7902
76987839 -gt;-gt;7839
74997698 -gt;-gt;7839-gt;7698
75217698 -gt;-gt;7839-gt;7698
76547698 -gt;-gt;7839-gt;7698
78447698 -gt;-gt;7839-gt;7698
79007698 -gt;-gt;7839-gt;7698
77827839 -gt;-gt;7839
79347782 -gt;-gt;7839-gt;7782
14 rows selected
--看增加where條件,只會的兩行結果,但是mrg=7566所屬的層次和所屬的樹都是沒有變的,這相當于從兩棵樹上剪下兩棵樹枝,又叫樹的修剪。
--可以看出where是在connect by之后起作用的,如果在之前起作用,就一個節點,何來樹呢?看計劃也可以的。
SQLgt; select empno,mgr,sys_connect_by_path(mgr,'-gt;') ptree from emp
2where mgr=7566
3start with mgr is null or mgr =7566
4connect by prior empno = mgr
5;
EMPNO MGR PTREE
----- ----- --------------------------------------------------------------------
79027566 -gt;7566
79027566 -gt;-gt;7839-gt;7566
gt; gt; gt; gt;
6、應用
1 )查詢每個等級上節點的數目
先查看總共有幾個等級:
select count(distinct level) -- max(level)也可以
from s_emp
start with manager_id is null
connect by prior id=manager_id;
要查看每個等級上有多少個節點,只要按等級分組,并統計節點的數目即可,可以這樣寫:
select level,count(last_name)
from s_emp
start with manager_id is null
connect by prior id=manager_id
group by level;
2 )格式化報表
詳見:第三小節.使用level和lpad格式化報表
3 )查看等級關系
有一個常見的需求,比如給定一個具體的emp看是否對某個emp有管理權,也就是從給定的節點尋找,看其子樹節點中能否找到這個節點。如果找到,返回,找不到,no rows returned.
比如對于s_emp表,從根節點,也就是manager_id is null的開始找,看first_name=’ Elena’是否被它管理,語句如下:
select level,a.* from
s_emp a
where first_name='Elena' –被管理的節點
start with manager_id is null –開始節點
connect by prior id=manager_id;
4 )刪除子樹
比如有這樣的需求,現在要裁員,將某個部門的員工包括經理全部裁掉,那么可以使用樹形查詢作為子查詢實現這個功能。
將id為2的員工管理的所有員工包括自己刪除。因為要全部裁掉了。那么語句如下:
delete from s_emp where id in(
select id from
s_emp a
start with id=2 –從id=2的員工開始查找其子節點,把整棵樹刪除
connect by prior id=manager_id);
5 )找出每個部門的經理
這個需求,我們可以從樹中查找,也就是對于每個部門選最高等級節點。可以使用connect by后加條件過濾branches的方法。
select level,a.* from
s_emp a
start with manager_id is null
connect by prior id=manager_id and dept_id !=prior dept_id;--當前行的dept_id不等于父親的dept_id,即每個子樹中選最高等級節點
6 )查詢一個組織中最高的幾個等級
用where level條件過濾
select level,a.* from
s_emp a
where level lt;=2 –查找前兩個等級
start with manager_id is null
connect by prior id=manager_id and dept_id !=prior dept_id;
改進:將levellt;=2加在connect by之后,盡早刪除不必要的分支,提高效率。
select level,a.* from
s_emp a
start with manager_id is null
connect by prior id=manager_id and dept_id !=prior dept_id and levellt;=2;
7 )合計層次
有兩個需求,一是對一個指定的子樹subtree做累加計算salary,一是將每行都作為root節點,然后對屬于這個節點的所有葉子點累加計算salary(如果不包含根節點,可以使用10g的CONNECT_BY_ISLEAF=1只求子節點)。
第一種很簡單,求下sum就可以了,語句:
select sum(salary) from
s_emp a
start with id=2—比如從id=2開始
connect by prior id=manager_id;
第2個需求,需要用到第1個,對每個root節點求這個樹的累加值,然后內部層次查詢的開始節點從外層查詢獲得。
select last_name,salary,(
select sum(salary) from
s_emp
start with id=a.id --讓每個節點都成為root
connect by prior id=manager_id) sumsalary
from s_emp a;
SELECT id,SUM(salary)
FROM(
select salary,connect_by_root ID ID from
s_emp
connect by prior id=manager_id
)
GROUP BY ID;
實例: 有數據表結構如下,只有葉子節點有數據。
idparentId name amount
1 成本
2 1 工資
3 2 基本工資 1000
4 2 獎金 200
5 1 保險 400
現在想統計處父節點合計數,如下:
1 成本 1600 //2 5
2 工資 1200 //3 4
3 基本工資 1000
4 獎金 200
5 保險 400
with tmp as (
select 1 as id , null as parentid , '成本' as name , null as amount from dual union all
select 2,1 , '工資', null from dual union all
select 3,2 , '基本工資', 1000 from dual union all
select 4,2 , '獎金' , 200 from dual union all
select 5,1 , '保險' , 400 from dual
)
SELECT root_id,SUM(amount)
FROM (select CONNECT_BY_ROOT(id) root_id,amount
from tmp
WHERE CONNECT_BY_ISLEAF=1
CONNECT BY PRIOR id = parentid
)
GROUP BY root_id;
--因為根節點沒有amount,所以不需要去掉,如果有amount,而不需要包含根節點,則用10G的CONNECT_BY_ISLEAF=1只查子節點。
with tmp as (
select 1 as id , null as parentid , '成本' as name , null as amount from dual union all
select 2,1 , '工資', null from dual union all
select 3,2 , '基本工資', 1000 from dual union all
select 4,2 , '獎金' , 200 from dual union all
select 5,1 , '保險' , 400 from dual
)
SELECT id,parentid,name,
( SELECT SUM(amount) FROM
tmp a
-- WHERE CONNECT_BY_ISLEAF=1
START WITH a.id=b.id
CONNECT BY PRIOR a.id=a.parentid
) sum_sal
FROM
tmp b
ORDER BY 1;
---9i可以使用SYS_CONNECT_BY_PATH然后取第1個節點---
將CONNECT_BY_ROOT用下面的代替:
substr(sys_connect_by_path(id,'/'),2,
decode(instr(sys_connect_by_path(id,'/'),'/',1,2),
0,length(sys_connect_by_path(id,'/'))-1,
instr(sys_connect_by_path(id,'/'),'/',1,2)- instr(sys_connect_by_path(id,'/'),'/',1,1)-1 )
) root_id
8 )找出指定層次中的葉子節點
Leaf( 葉子)就是沒有子孫的孤立節點。 Oracle 10g提供了一個簡單的 connect_by_isleaf =1, 0表示非葉子節點。
select level,id,manager_id,last_name, title from s_emp
where connect_by_isleaf=1 –表示查詢葉子節點
start with manager_id=2
connect by prior id=manager_id;
下面的方法也可以,NOT EXISTS表示找到的沒有子孫的節點,這種方法在10g之前比較好。
SELECT LEVEL,id,manager_id,last_name, title
FROM s_emp a
WHERE NOT EXISTS (SELECT 1 FROM s_emp b WHERE b.manager_id = a.id)
START WITH manager_id =2 CONNECT BY PRIOR id=manager_id;
注意:level不可以前面加表名。
其他:
Connect by與rownum的聯合使用,比如給定兩個日期,查詢中間所有的日期,按月遞增。
SELECT to_date('2008-10-1', 'YYYY-MM-DD') ROWNUM - 1
FROM dual
CONNECT BY rownum lt;= to_date('2008-10-5', 'YYYY-MM-DD') -
to_date('2008-10-1', 'YYYY-MM-DD') 1;
獲取01到99
select case when length(rownum)=1 then to_char('0')||rownum else to_char(rownum) end
from dual
connect by rownumlt;=99;
select lpad(rownum,2,'0') from dual connect by rownumlt;=99;
9 )在合并行上使用
比如:
create table t_test (l_pro_id varchar2(16), r_pro_id varchar2(16));
INSERT INTO t_test VALUES(1,10);
INSERT INTO t_test VALUES(1,11);
INSERT INTO t_test VALUES(1,12);
INSERT INTO t_test VALUES(1,13);
INSERT INTO t_test VALUES(1,14);
INSERT INTO t_test VALUES(1,15);
INSERT INTO t_test VALUES(1,16);
INSERT INTO t_test VALUES(1,17);
INSERT INTO t_test VALUES(2,2);
INSERT INTO t_test VALUES(2,3);
INSERT INTO t_test VALUES(2,4);
INSERT INTO t_test VALUES(2,5);
INSERT INTO t_test VALUES(2,6);
INSERT INTO t_test VALUES(2,7);
INSERT INTO t_test VALUES(2,8);
INSERT INTO t_test VALUES(2,9);
COMMIT;
要求結果
l_pro_id r_pro_list
--------------------------------
1 10,11,12,13,14,15,16,17
2 2,3,4,5,6,7,8,9
可以使用wmsys.wm_concat,也可以使用層次查詢,這里用層次查詢。
select l_pro_id,ltrim(max(sys_connect_by_path(r_pro_id,',')),',') --需要剔除第1個逗號
from
(select l_pro_id,r_pro_id,row_number() over(partition by l_pro_id order by r_pro_id) rn from t_test)
start with rn =1
connect by prior rn = rn-1 and prior l_pro_id=l_pro_id
group by l_pro_id
order by l_pro_id;
10 )找任意兩節點之間的最大路徑
drop table t;
create table t(a number,b number);
select * from t for update;
insert into t values(1,2);
insert into t values(2,3);
insert into t values(3,4);
insert into t values(4,5);
insert into t values(1,5);
insert into t values(5,9);
insert into t values(1,8);
insert into t values(7,3);
insert into t values(8,9);
commit;
-- 用sys_connect_by_path,下面的是從根找到葉子
select '1'||sys_connect_by_path(b,'-gt;') path from t
where connect_by_isleaf=1
start with a=1
connect by prior b = a;
1-gt;2-gt;3-gt;4-gt;5-gt;9
1-gt;5-gt;9
1-gt;8-gt;9
--這個找任意開始到結束節點,注意如果有循環的要加nocycle,10g才有
select *
from (select ltrim(sys_connect_by_path(a, '-gt;') || '-gt;' || b, '-gt;') r
from t x
start with x.a = 1
connect by prior x.b = x.a)
where instr(r, '9') gt; 0
三、增強特性
gt; gt; gt; gt;
1、SYS_CONNECT_BY_PATH
Oracle 9i提供了sys_connect_by_path(column,char),其中column是 字符型或能自動轉換成字符型 的列名。它的主要目的就是將父節點到當前節點的”path”按照指定的模式展現出現。 這個函數只能使用在層次查詢中。
例如,要求將s_emp表中的層次關系展現出來,并且將last_name按照’=gt;’展現。如root,則是=gt;root_last_name, level=2的就是=gt;root_last_name=gt;level_2_last_name,并且利用lpad格式化報表。語句是:
s elect last_name,
level,
id,
lpad(' ', level * 2 - 1) || sys_connect_by_path(last_name, '=gt;') –前面按層次加空格,--并且后面加上路徑
from s_emp
start with manager_id is null
connect by prior id = manager_id;
結果如圖所示:
下面的是oracle10g新增特性。
gt; gt; gt; gt;
2、CONNECT_BY_ISLEAF偽列
在oracle9i的時候,查找指定root下的葉子節點,是很復雜的,oracle10g引入了一個新的偽列,connect_by_isleaf,如果行的值為0表示不是葉子節點,1表示是葉子節點。
找出s_emp中找出manager_id=2開始的行為root,表示葉子節點和非葉子節點,那么語句如下:
----找根節點為a,對應的所有葉子節點。
gt; gt; gt; gt;
3、CONNECT_BY_ISCYCLE偽列和NOCYCLE關鍵字
如果從root節點開始找其子孫,找到一行,結果發生和祖先互為子孫的情況,則發生循環,Oracle會報ORA-01436: CONNECT BY loop in user data,在9i中只能將發生死循環的不加入到樹中或刪除,在10g中可以用nocycle關鍵字加在connect by之后,避免循環的參加查詢操作。并且通過connect_by_iscycle得到哪個節點發生循環(也就是onnect_by_iscycle偽列只能與nocycle連用)。0表示未發生循環,1表示發生了循環,如:
結果是:
gt; gt; gt; gt;
4、CONNECT_BY_ROOT
Oracle10g新增connect_by_root,用在列名之前表示此行的根節點的相同列名的值 (這個和PRIOR一樣,是層次查詢的操作符) 。如:
select connect_by_root last_name root_last_name, connect_by_root id root_id,
id,last_name,manager_id
from s_emp
start with manager_id is null
connect by prior id=manager_id;
結果為:
9i 辦法:用SYS_CONNECT_BY_PATH, 然后用INSTR, SUBSTR把第一截解析出來。
上面的可以改寫為:
//選第2次出現分割符的位置,然后截取,因為根節點只有一個分隔符,所以加個decode.
gt; gt; gt; gt;
5、使用SIBLINGS關鍵字排序
前面說了,對于層次查詢如果用order by排序,比如order by last_name則是先做完層次獲得level,然后按last_name排序,這樣破壞了層次,比如特別關注某行的深度,按level排序,也是會破壞層次的。
在oracle10g中,增加了siblings關鍵字的排序。
語法:order siblings by lt;expregt;
它會保護層次,并且在每個等級中按expre排序。
select level,
id,last_name,manager_id
from s_emp
start with manager_id is null
connect by prior id=manager_id
order siblings by last_name;
結果如圖:
9i 辦法:
Tags:
文章來源:http://dbaplus.cn/news-10-676-1.html