Oracle開發技能提升之層次查詢全面解析

分類:技術 時間:2016-09-23

一、基本概念

對于層次查詢需要掌握:

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


ads
ads

相關文章
ads

相關文章

ad