使用connect by進行級聯查詢
connect by可以用於級聯查詢,常用於對具有樹狀結構的記錄查詢某一節點的所有子孫節點或所有祖輩節點。
來看一個示例,現假設我們擁有一個菜單表t_menu,其中只有三個字段:id、name和parent_id。它們是具有父子關係的,最頂級的菜單對應的parent_id爲0。現假設我們擁有如下記錄:
id |
name |
parent_id |
1 |
菜單01 |
0 |
2 |
菜單02 |
0 |
3 |
菜單03 |
0 |
4 |
菜單0101 |
1 |
5 |
菜單0102 |
1 |
6 |
菜單0103 |
1 |
7 |
菜單010101 |
4 |
8 |
菜單010201 |
5 |
9 |
菜單010301 |
6 |
10 |
菜單0201 |
2 |
11 |
菜單0202 |
2 |
12 |
菜單020101 |
10 |
13 |
菜單020102 |
10 |
14 |
菜單020103 |
10 |
15 |
菜單0301 |
3 |
16 |
菜單0302 |
3 |
17 |
菜單030201 |
16 |
18 |
菜單030202 |
16 |
19 |
菜單030203 |
16 |
如果這個時候我們需要查詢“菜單01”以及其下所有的子孫菜單應該怎麼辦呢?如果使用connect by的話這將會非常簡單,使用如下SQL語句就可以達到對應的效果。
select * from t_menu connect by parent_id=prior id start with id=1;
connect by是需要跟start with一起使用的。connect by後跟的是連接條件,在connect by後接的條件通常都需要使用關鍵字“prior”,可以簡單的把它理解爲上一級,所以上述例子中“connect by parent_id=prior id”就表示連接條件爲parent_id等於上級的id,查找到下一級記錄後又會找parent_id等於下一級記錄的id的記錄,而prior對應的最頂層的記錄就是通過start with來確定的,start with後接對應的篩選條件,表示最頂層的記錄是哪些,最頂層的記錄可以有多個,比如我想查找“菜單01”下的子孫菜單,但是不包括“菜單01”本身,那麼我就可以使用如下的SQL語句進行查找,此時“start with parent_id=1”對應的記錄就會有多條。
select * from t_menu connect by parent_id=prior id start with parent_id=1;
對應的結果爲:
id |
name |
parent_id |
4 |
菜單0101 |
1 |
5 |
菜單0102 |
1 |
6 |
菜單0103 |
1 |
7 |
菜單010101 |
4 |
8 |
菜單010201 |
5 |
9 |
菜單010301 |
6 |
此外,如果我們想查找“菜單010101”對應的祖輩菜單也非常簡單,如下SQL就可以實現該功能,即從“菜單010101”的父菜單(對應id爲4)開始查找。
select * from t_menu connect by id=prior parent_id start with id=4;
對應的結果爲:
id |
name |
parent_id |
1 |
菜單01 |
0 |
4 |
菜單0101 |
1 |
level
使用connect by時我們可以使用內置的類似於rownum的一個叫level的僞列,該列表示當前記錄相對於start with記錄的一個層級,start with記錄的level爲1。如上面的兩條SQL語句,如果加上level的話對應的結果將是這樣的。
select level,t.* from t_menu t connect by parent_id=prior id start with parent_id=1;
對應的結果爲:
level |
id |
name |
parent_id |
1 |
4 |
菜單0101 |
1 |
1 |
5 |
菜單0102 |
1 |
1 |
6 |
菜單0103 |
1 |
2 |
7 |
菜單010101 |
4 |
2 |
8 |
菜單010201 |
5 |
2 |
9 |
菜單010301 |
6 |
select level,t.* from t_menu t connect by id=prior parent_id start with id=4;
對應的結果爲:
level |
id |
name |
parent_id |
2 |
1 |
菜單01 |
0 |
1 |
4 |
菜單0101 |
1 |
有了level後,我們就可以對查詢的level做一個限制,比如只查從最頂層開始向下兩級的菜單。
select level,t.* from t_menu t where level<3 connect by prior id= parent_id start with parent_id=0;
從上述SQL我們可以看到where條件是直接跟在from之後的,使用connect by時我們的where條件不是在connect by之前對數據進行過濾的,而是在connect by之後纔對所有的數據進行過濾的,這一點跟使用分組語句group by時是不一樣的,group by是先通過where對需要分組的數據進行過濾後再通過group by來分組的。
nocycle和connect_by_iscycle
如果我們的記錄中存在循環的父子關係,則使用connect by進行查詢時會拋出異常,如A->B、B->C、C->A這樣的記錄。解決辦法是在connect by語句後加上“nocycle”,表示不循環查詢,如:
select * from t_menu connect by nocycle prior id=parent_id start with parent_id=0;
使用nocycle後對於A->B、B->C、C->A這樣的記錄會通過查詢B,然後通過B查詢C,再通過C查詢A時發現已經循環了,就不再查詢了,即在C這條記錄這裏循環了。在對存在循環記錄的查詢中我們也可以通過“connect_by_iscycle”找到是哪一條記錄循環了,“connect_by_iscycle”也是一個僞列,其必須和nocycle一起使用。僞列“connect_by_iscycle”對應的值有0和1,如果某一條記錄的connect_by_iscycle對應的值爲1則表示從該條記錄這裏開始循環了。如下是一個使用connect_by_iscycle的示例。
select connect_by_iscycle,t.* from t_menu t connect by nocycle prior id=parent_id start with parent_id=0;
connect_by_isleaf
connect_by_isleaf也是一個僞列,其表示對應的記錄是否是一個葉子節點,即在進行connect by時不能通過該記錄找到下一條記錄。其對應的值有0和1,0表示非葉子節點,1表示是葉子節點。如我只想找出是葉子節點的菜單時對應的SQL可以這樣寫:
select connect_by_isleaf,t.* from t_menu t where connect_by_isleaf=1 connect by prior id=parent_id start with parent_id=0;
connect_by_root
connect_by_root表示根節點,即某一條記錄所對應的最頂級的記錄,其用法跟prior類似,後面也需要跟一個字段名。如下面示例可以查詢所有葉子節點菜單的最頂級菜單和上級菜單的名稱。
select connect_by_root name as root_name, prior name as prior_name,t.* from t_menu t where connect_by_isleaf=1 connect by prior id=parent_id start with parent_id=0;
對應上表的記錄,在上述SQL中查詢出來的結果應該如下所示:
root_name |
prior_name |
id |
name |
parent_id |
菜單01 |
菜單0101 |
7 |
菜單010101 |
4 |
菜單01 |
菜單0102 |
8 |
菜單010201 |
5 |
菜單01 |
菜單0103 |
9 |
菜單010301 |
6 |
菜單02 |
菜單02 |
11 |
菜單0202 |
2 |
菜單02 |
菜單0201 |
12 |
菜單020101 |
10 |
菜單02 |
菜單0201 |
13 |
菜單020102 |
10 |
菜單02 |
菜單0201 |
14 |
菜單020103 |
10 |
菜單03 |
菜單03 |
15 |
菜單0301 |
3 |
菜單03 |
菜單0302 |
17 |
菜單030201 |
16 |
菜單03 |
菜單0302 |
18 |
菜單030202 |
16 |
菜單03 |
菜單0302 |
19 |
菜單030203 |
16 |
sys_connect_by_path
sys_connect_by_path(column,delimiter)可以用來展示以指定column和分隔符delimiter表示從根節點到當前節點的路徑。以下SQL用來查詢id爲2的菜單下葉子節點的信息,包括以字段name和分隔符“>”表示的其對應的根節點的路徑。
select sys_connect_by_path(name, '>') as connect_path,t.* from t_menu t where connect_by_isleaf=1 connect by prior id=parent_id start with id=2;
對應結果如下所示:
connect_path |
id |
name |
parent_id |
>菜單02>菜單0202 |
11 |
菜單0202 |
2 |
>菜單02>菜單0202>菜單020101 |
12 |
菜單020101 |
10 |
>菜單02>菜單0202>菜單020102 |
13 |
菜單020102 |
10 |
>菜單02>菜單0202>菜單020103 |
14 |
菜單020103 |
10 |
排序order
可以使用order by對connect by之後的結果進行排序,此時order by需放在最末端,而不像where篩選那樣直接定義在from之後。如需對connect by之後的結果按id進行排序,則可以使用如下SQL語句:
select t.* from t_menu t connect by parent_id=prior id start with parent_id=0 order by id;
除了傳統的針對查詢結果的排序外,connect by語句還支持對同一父節點下的子節點進行排序,這是通過order siblings by來定義的。如我們需要查詢id爲2的菜單下的所有子孫菜單,然後對具有同一父節點的菜單按id進行倒序排列,則我們的SQL語句可以如下定義:
select t.* from t_menu t connect by parent_id=prior id start with id=2 order siblings by id desc;
對應的結果會是這樣子:
id |
name |
parent_id |
2 |
菜單02 |
0 |
11 |
菜單0202 |
2 |
10 |
菜單0201 |
2 |
14 |
菜單020103 |
10 |
13 |
菜單020102 |
10 |
12 |
菜單020101 |
10 |
如上表所示,我們可以看到“菜單0201”和“菜單0202”具有相同的父節點“菜單02”,它們按照id進行倒序排列,所有“菜單0202”在“菜單0201”之前,同樣“菜單020101”、“菜單020102”和“菜單020103”具有相同的父節點“菜單0201”,所以它們也是按照id的倒序排列。
一次針對connect by的查詢優化
有這麼一個需求:表A表示分類,表B表示任務模板,A與B是一對多的關係,每一個任務模板都屬於一個特定的分類,在表B中用字段a表示所屬的分類。分類存在父子關係,子分類的parent_id對應父分類的id。現假設需要統計id爲1的分類及其子分類下存在的任務模板數量。對應SQL如下:
select count(1) from B b,(select id from A connect by prior id=parent_id start with id=1) a where a.id=b.a;
現假設擁有另外一個表C,其表示任務實例,一個任務模板B可以擁有n個任務實例B,即B跟C之間是一對多的關係。任務實例C通過字段b關聯任務模板B,另外任務實例C擁有一個字段status表示任務實例的具體狀態。現假設需要統計id爲1的分類及其子分類下各狀態的任務實例數量。對應SQL如下:
select c.status,count(1) from B b,(select id from A connect by prior id=parent_id start with id=1) a, C c where a.id=b.a and b.id=c.b group by c.status;
在A表數據量1000,B表數據量20000,C表數據量5000,id爲1的分類下屬的子孫分類數量爲100的情況下第一條SQL的查詢速度可以在0.1秒左右完成,而第二條SQL需要將近10秒才能完成。把查詢id爲1的分類下子孫分類的id的SQL語句“selectidfrom A connectbypriorid=parent_id startwithid=1”單獨查詢的速度也可以在0.1秒內完成。通常對於這種數量級別的三表查詢都是可以在0.1秒內完成的,爲此心想第二條SQL應該是受了子查詢中connect by的影響。後來決定把分類的子查詢直接作爲B的in條件進行查詢,如下所示:
select c.status,count(1) from B b, C c where b.a in(select id from A connect by prior id=parent_id start with id=1) and b.id=c.b group by c.status;
其查詢效果是一樣的,心想應該還是connect by影響到了,既然單獨使用connect by查詢id爲1的分類的子孫分類的id只需要不到0.1秒,那何不在程序裏面先將id爲1的分類的子孫分類id查詢出來,再作爲B、C聯合查詢的in條件,如:
select c.status,count(1) from B b, C c where b.a in(...) and b.id=c.b group by c.status;
結果查詢結果也可以在0.1秒內完成。