一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

11GR2的遞歸WITH子查詢(新增內(nèi)容在第二頁(yè))

 ctbtcol 2014-11-13

WITH子查詢也稱(chēng)為CTE (Common Table Expression),是ANSI SQL-99標(biāo)準(zhǔn)的一部分。ORACLE從9i開(kāi)始引入WITH子查詢,把它被稱(chēng)作SUBQUERY FACTORING(分解子查詢)。

WITH子查詢的作用類(lèi)似于內(nèi)聯(lián)視圖(INLINE VIEW)。內(nèi)聯(lián)視圖的定義寫(xiě)作SQL的FROM 后面,只能夠引用一次;而WITH子查詢需要在引用之前先定義,一旦定義了在整個(gè)查詢的后續(xù)部分就可以按名稱(chēng)來(lái)反復(fù)引用,從這點(diǎn)來(lái)看又很像臨時(shí)表。

從版本11GR2開(kāi)始,ORACLE支持遞歸的WITH, 即允許在WITH子查詢的定義中對(duì)自身引用。這不是什么新鮮事,其他數(shù)據(jù)庫(kù)如DB2, Firebird, Microsoft SQL Server, PostgreSQL 都先于ORACLE支持這一特性。但對(duì)于ORACLE用戶來(lái)說(shuō),這一遞歸特性還是很令人期待的,利用它可以輕易實(shí)現(xiàn)以往做不到的、或者很難做到的許多新功能。這一章我們就來(lái)探索這一令人興奮的新特性,并把它和以往的實(shí)現(xiàn)手段(主要是CONNECT BY層次查詢)作比較。

我們先來(lái)看看這個(gè)遞歸WITH子查詢的語(yǔ)法:
WITH
①  query_name ([c_alias [, c_alias]...])
②  AS (subquery)
③  [search_clause]
④  [cycle_clause]
⑤  [,query_name ([c_alias [, c_alias]...]) AS (subquery) [search_clause] [cycle_clause]]...  

①這是子查詢的名稱(chēng),和以往不同的是,必須在括號(hào)中把這個(gè)子查詢的所有列名寫(xiě)出來(lái)。
②AS后面的subquery就是查詢語(yǔ)句,遞歸部分就寫(xiě)在這里。
③遍歷順序子句,可以指定深度優(yōu)先或廣度優(yōu)先遍歷順序。
④循環(huán)子句,用于中止遍歷中出現(xiàn)的死循環(huán)。
⑤如果還有其他遞歸子查詢,定義同上。

subquery部分由兩個(gè)成員組成:anchor member(錨點(diǎn)成員) 和 recursive member(遞歸成員)。它們之間必須用union all聯(lián)合起來(lái),anchor member 必須寫(xiě)在recursive member前面。
anchor member用來(lái)定位遞歸的入口,錨點(diǎn)成員是一個(gè)SELECT語(yǔ)句,它不可以包含自身名稱(chēng)(query_name)。這相當(dāng)于CONNECT BY查詢中的START WITH,典型寫(xiě)法就是:
SELECT ... FROM 要遍歷的表 WHERE ... (起始條件)

遞歸成員也是一個(gè)SELECT語(yǔ)句,用于定義上下級(jí)的關(guān)系,它必須包含自身名稱(chēng)(即query_name),而且僅僅只能引用一次。遞歸正是體現(xiàn)在對(duì)于自身的引用。典型的做法就是把query_name和其他表(一般來(lái)說(shuō)就是你要遍歷的表)做一個(gè)連接,連接條件表明了上下級(jí)的關(guān)系。必須注意,在這個(gè)query_name中,并不是截止目前為止的所有數(shù)據(jù)都是可見(jiàn)的,可見(jiàn)的只是上次遞歸新加入的最近的一層數(shù)據(jù)。對(duì)query_name列的引用相當(dāng)于CONNECT BY中的PRIOR操作符。當(dāng)找不到滿足條件的下級(jí),遍歷就會(huì)停止;如果你還有其他的遞歸出口條件,也可以一起寫(xiě)在WHERE中,當(dāng)WHERE不滿足時(shí),遍歷就會(huì)停止,這就是在遍歷樹(shù)、圖時(shí)候的剪枝操作。越早停止則效率越高。

這個(gè)遞歸成員就是程序員發(fā)揮創(chuàng)造力的地方,以往在CONNECT BY中做不到的事情,比如沿路徑求和、求積等運(yùn)算,現(xiàn)在都輕而易舉。而SYS_CONNECT_BY_PATH也很容易用字符串拼接'||'來(lái)實(shí)現(xiàn)。

搜索子句(search_clause)和循環(huán)子句(cycle_clause)我們后面的例子中會(huì)見(jiàn)到。

下面我們就來(lái)看看遞歸WITH子查詢的用法實(shí)例。

例1:
先來(lái)一個(gè)簡(jiǎn)單例子,從scott/tiger的emp表來(lái)查找上下級(jí)關(guān)系:

傳統(tǒng)的CONNECT BY寫(xiě)法:
SELECT empno
      ,ename
      ,job
      ,mgr
      ,deptno
      ,level
      ,SYS_CONNECT_BY_PATH(ename,'\') AS path
      ,CONNECT_BY_ROOT(ename) AS top_manager
  FROM EMP
START WITH mgr IS NULL -- mgr列為空,表示沒(méi)有上級(jí),該員工已經(jīng)是最高級(jí)別。這是層次查詢的起點(diǎn)
CONNECT BY PRIOR empno= mgr;

新的遞歸WITH寫(xiě)法:
WITH T(empno, ename, job, mgr, deptno, the_level, path,top_manager) AS ( ---- 必須把結(jié)構(gòu)寫(xiě)出來(lái)
   SELECT empno, ename, job, mgr, deptno  ---- 先寫(xiě)錨點(diǎn)查詢,用START WITH的條件
         ,1 AS the_level    ---- 遞歸起點(diǎn),第一層
         ,'\'||ename        ---- 路徑的第一截
         ,ename AS top_manager ---- 原來(lái)的CONNECT_BY_ROOT
     FROM EMP
    WHERE mgr IS NULL ---- 原來(lái)的START WITH條件
   UNION ALL  ---- 下面是遞歸部分
   SELECT e.empno, e.ename, e.job, e.mgr, e.deptno  ---- 要加入的新一層數(shù)據(jù),來(lái)自要遍歷的emp表
         ,1 + t.the_level             ---- 遞歸層次,在原來(lái)的基礎(chǔ)上加1。這相當(dāng)于CONNECT BY查詢中的LEVEL偽列
         ,t.path||'\'||e.ename        ---- 把新的一截路徑拼上去
         ,t.top_manager               ---- 直接繼承原來(lái)的數(shù)據(jù),因?yàn)槊總€(gè)路徑的根節(jié)點(diǎn)只有一個(gè)
     FROM t, emp e                    ---- 典型寫(xiě)法,把子查詢本身和要遍歷的表作一個(gè)連接
    WHERE t.empno = e.mgr             ---- 原來(lái)的CONNECT BY條件
) ---- WITH定義結(jié)束
SELECT * FROM T
;

查詢結(jié)果:
EMPNO ENAME      JOB          MGR  DEPTNO  THE_LEVEL PATH                       TOP_MANAGE
------ ---------- --------- ------ ------- ---------- -------------------------- ----------
  7839 KING       PRESIDENT             10          1 \KING                      KING
  7566 JONES      MANAGER     7839      20          2 \KING\JONES                KING
  7698 BLAKE      MANAGER     7839      30          2 \KING\BLAKE                KING
  7782 CLARK      MANAGER     7839      10          2 \KING\CLARK                KING
  7499 ALLEN      SALESMAN    7698      30          3 \KING\BLAKE\ALLEN          KING
  7521 WARD       SALESMAN    7698      30          3 \KING\BLAKE\WARD           KING
  7654 MARTIN     SALESMAN    7698      30          3 \KING\BLAKE\MARTIN         KING
  7788 SCOTT      ANALYST     7566      20          3 \KING\JONES\SCOTT          KING
  7844 TURNER     SALESMAN    7698      30          3 \KING\BLAKE\TURNER         KING
  7900 JAMES      CLERK       7698      30          3 \KING\BLAKE\JAMES          KING
  7902 FORD       ANALYST     7566      20          3 \KING\JONES\FORD           KING
  7934 MILLER     CLERK       7782      10          3 \KING\CLARK\MILLER         KING
  7369 SMITH      CLERK       7902      20          4 \KING\JONES\FORD\SMITH     KING
  7876 ADAMS      CLERK       7788      20          4 \KING\JONES\SCOTT\ADAMS    KING

14 rows selected.   

從結(jié)果集的THE_LEVEL和PATH列可以清楚地看到數(shù)據(jù)是如何被一層一層疊加上去的。

例2:
構(gòu)造等差數(shù)列:

CONNECT BY寫(xiě)法:
這是一個(gè)非常特殊的用法,因?yàn)闆](méi)有上下級(jí)關(guān)系,只有遍歷的終止條件。像這類(lèi)CONNECT BY我強(qiáng)烈推薦在只有一行的結(jié)果集上運(yùn)行(比如FROM DUAL, 比如從一個(gè)聚合后的子查詢),在多行的集合上運(yùn)行比較難以控制,頭腦必須很清醒。

(以下ROWNUM全部可以改成 LEVEL,效果一樣):
SELECT ROWNUM n
      ,ROWNUM*2 n2
      ,DATE '2010-1-1'+ROWNUM-1 dt
      ,ADD_MONTHS(DATE '2010-1-1', ROWNUM-1) mon
  FROM DUAL
CONNECT BY ROWNUM<=10;

結(jié)果:
         N         N2 DT          MON        
---------- ---------- ----------- -----------
         1          2 2010-01-01  2010-01-01
         2          4 2010-01-02  2010-02-01
         3          6 2010-01-03  2010-03-01
         4          8 2010-01-04  2010-04-01
         5         10 2010-01-05  2010-05-01
         6         12 2010-01-06  2010-06-01
         7         14 2010-01-07  2010-07-01
         8         16 2010-01-08  2010-08-01
         9         18 2010-01-09  2010-09-01
        10         20 2010-01-10  2010-10-01

10 rows selected.

這個(gè)簡(jiǎn)潔優(yōu)雅的寫(xiě)法最早由Mikito Harakiri(從名字看是個(gè)日本人)在asktom網(wǎng)站(http://asktom.oracle.com)發(fā)表,現(xiàn)在已經(jīng)風(fēng)靡全世界的ORACLE社區(qū)。在這個(gè)方法被發(fā)現(xiàn)之前,一般采用的是從一個(gè)大的集合(表或視圖)中獲取ROWNUM的方法:

SELECT ROWNUM n, ROWNUM*2 n2, DATE '2010-1-1'+ROWNUM-1 dt, ADD_MONTHS(DATE '2010-1-1', ROWNUM-1) mon
  FROM ALL_OBJECTS ---- ALL_OBJECTS是個(gè)很大的系統(tǒng)視圖,它包含的行數(shù)足夠滿足一般的序列構(gòu)造
WHERE ROWNUM<=10;


下面嘗試用遞歸WITH的寫(xiě)法:
WITH t(n,n2,dt,mon) AS (
  SELECT 1, 2,TO_DATE('2010-1-1','YYYY-MM-DD'),TO_DATE('2010-1-1','YYYY-MM-DD') FROM DUAL --- 先構(gòu)造第一個(gè)
  UNION ALL
  SELECT t.n+1   ---- 遞增1
        ,t.n2+2  ---- 遞增2
        ,dt+1    ---- 下一日
        ,ADD_MONTHS(mon,1)  ---- 下個(gè)月
    FROM t      ---- 沒(méi)有任何連接,因?yàn)椴恍枰袛?shù)據(jù)都可以從錨點(diǎn)成員中衍生出來(lái)
   WHERE t.n<10
  )
SELECT * FROM T;

一切都按規(guī)矩來(lái),竟然還是出錯(cuò)了:
        ,ADD_MONTHS(mon,1)  ---- 下個(gè)月
         *
ERROR at line 6:
ORA-01790: expression must have same datatype as corresponding expression

改為字符串型看看:
WITH t(n,n2,dt,mon) AS (
  SELECT 1, 2,'2010-01-01','2010-01-01' FROM DUAL  ---- 用字符串來(lái)表示日期
  UNION ALL
  SELECT t.n+1   ---- 遞增1
        ,t.n2+2  ---- 遞增2
        ,TO_CHAR(TO_DATE(t.dt,'YYYY-MM-DD')+1,'YYYY-MM-DD')   ---- 先轉(zhuǎn)換為日期型,計(jì)算后換回字符串型
        ,TO_CHAR(ADD_MONTHS(TO_DATE(t.mon,'YYYY-MM-DD'),1),'YYYY-MM-DD')  ---- 計(jì)算下個(gè)月,方法同上
    FROM t
   WHERE t.n<10
  )
SELECT * FROM T;

我很驚奇地看到這個(gè)結(jié)果:
         N         N2 DT         MON
---------- ---------- ---------- ----------
         1          2 2010-01-01 2010-01-01
         2          4 2009-12-31 2010-02-01  ----- DT竟然是遞減的!
         3          6 2009-12-30 2010-03-01
         4          8 2009-12-29 2010-04-01
         5         10 2009-12-28 2010-05-01
         6         12 2009-12-27 2010-06-01
         7         14 2009-12-26 2010-07-01
         8         16 2009-12-25 2010-08-01
         9         18 2009-12-24 2010-09-01
        10         20 2009-12-23 2010-10-01

10 rows selected.

這是ORACEL 11.2.0.1.0版本的BUG,后續(xù)版本應(yīng)該會(huì)改正。

沒(méi)辦法,只好想其他招數(shù)繞過(guò)去:
WITH t(n) AS (
  SELECT 1 FROM DUAL --- 先構(gòu)造第一個(gè)
  UNION ALL
  SELECT t.n+1  ---- 僅僅是整數(shù)序列
    FROM t      
   WHERE t.n<10
  )
SELECT n
      ,n*2 n2
      ,DATE '2010-1-1'+n-1 dt   ---- 在最終的查詢中進(jìn)行日期運(yùn)算
      ,ADD_MONTHS(DATE '2010-1-1', n-1) mon
  FROM T;
  
這下子對(duì)了:
         N         N2 DT          MON
---------- ---------- ----------- -----------
         1          2 2010-01-01  2010-01-01
         2          4 2010-01-02  2010-02-01
         3          6 2010-01-03  2010-03-01
         4          8 2010-01-04  2010-04-01
         5         10 2010-01-05  2010-05-01
         6         12 2010-01-06  2010-06-01
         7         14 2010-01-07  2010-07-01
         8         16 2010-01-08  2010-08-01
         9         18 2010-01-09  2010-09-01
        10         20 2010-01-10  2010-10-01

10 rows selected.

看來(lái)對(duì)日期的運(yùn)算有BUG。解決辦法就是先構(gòu)造整數(shù)序列,然后在最終的查詢中再利用這個(gè)整數(shù)序列來(lái)構(gòu)造日期序列。


從一個(gè)單行結(jié)果集CONNECT BY的例子:
SELECT ROWNUM rn,cnt
FROM (SELECT COUNT(*) cnt FROM emp)   ---- 經(jīng)過(guò)聚合的只有一行的結(jié)果集
CONNECT BY ROWNUM<=cnt;

結(jié)果:
        RN        CNT
---------- ----------
         1         14
         2         14
         3         14
         4         14
         5         14
         6         14
         7         14
         8         14
         9         14
        10         14
        11         14
        12         14
        13         14
        14         14

14 rows selected.

遞歸WITH寫(xiě)法:
WITH t(n,cnt) AS (
  SELECT 1,COUNT(*) cnt FROM EMP --- 先構(gòu)造第一個(gè)
  UNION ALL
  SELECT t.n+1  ---- 遞增1
        ,t.cnt  ---- 這個(gè)cnt列不做任何修改,從第一層得來(lái)
    FROM t      ---- 沒(méi)有任何連接,因?yàn)椴恍枰?br>    WHERE t.n<t.cnt  ---- 在這里看到cnt的作用,就是用于終止遍歷
  )
SELECT * FROM t;

結(jié)果同上(略)。

例3:
獨(dú)立事件的排列組合:一個(gè)布袋中裝有數(shù)量相同的四種顏色的小球。隨機(jī)從布袋中取四次,每次取完都放回去?,F(xiàn)在問(wèn)四次結(jié)果總顏色數(shù)等于3的概率是多少?

傳統(tǒng)的CONNECT BY寫(xiě)法:
WITH t AS (
SELECT ROWNUM rn   -- 先構(gòu)造一個(gè)1,2,3,4的結(jié)果集,每個(gè)rn表示一種顏色
  FROM DUAL
CONNECT BY ROWNUM<=4
)
,t2 AS ( ---- 集合t2模擬獨(dú)立取四次的動(dòng)作,最終結(jié)果會(huì)有4*4*4*4=256行
SELECT ROWNUM id   ---- 構(gòu)造唯一ID供下面拆分用
      ,REPLACE(SYS_CONNECT_BY_PATH(rn,'@'),'@') path     ---- 用一個(gè)特殊字符@來(lái)作分隔符, 并在最后用REPLACE把它去除
      ,COUNT(*) OVER() cnt    ---- 利用分析函數(shù)算出總行數(shù)并把它作為一個(gè)列返回
  FROM t ---- 這個(gè)是有四行的集合
WHERE LEVEL=4       ---- 我們需要的僅僅是最后一層的結(jié)果。在PATH里面已經(jīng)包含了取四次的所有結(jié)果組合
CONNECT BY LEVEL<=4  ---- 沒(méi)有任何條件,前后都是獨(dú)立的
)
,t3 AS ( ---- 集合t3把t2中的PATH包含的顏色組合拆開(kāi)為四行
SELECT id,cnt,SUBSTR(PATH,rn,1) color
  FROM t2,t  ---- 笛卡兒積,用于把t2中的一行變?yōu)樗男?br>   )
SELECT COUNT(COUNT(*))/MAX(cnt) AS prob
  FROM t3
GROUP BY id,cnt
HAVING COUNT(DISTINCT color)=3   --- 每一個(gè)id中包含三種顏色
;

結(jié)果:
      PROB
----------
     .5625

這個(gè)例子展示了CONNECT BY來(lái)模擬排列組合的技巧。每一層遍歷表示一次抽取的動(dòng)作,因?yàn)槊看味际峭耆?dú)立的,在CONNECT BY 里面僅僅限制了抽取次數(shù)(遍歷層數(shù))而沒(méi)有其他條件。SYS_CONNECT_BY_PATH可以把截至當(dāng)前為止所訪問(wèn)到的各層次的數(shù)據(jù)串起來(lái),在LEVEL=N就包含了前N層的排列組合情況。你可以用這個(gè)查詢來(lái)看看中間生成的結(jié)果集t2:

WITH t AS (
SELECT ROWNUM rn   -- 先構(gòu)造一個(gè)1,2,3,4的結(jié)果集,每個(gè)rn表示一種顏色
  FROM DUAL
CONNECT BY ROWNUM<=4
)
,t2 AS ( ---- 集合t2模擬獨(dú)立取四次的動(dòng)作,最終結(jié)果會(huì)有4*4*4*4=256行
SELECT ROWNUM id   ---- 構(gòu)造唯一ID供下面拆分用
      ,REPLACE(SYS_CONNECT_BY_PATH(rn,'@'),'@') path     ---- 用一個(gè)特殊字符@來(lái)作分隔符, 并在最后用REPLACE把它去除
      ,COUNT(*) OVER() cnt    ---- 利用分析函數(shù)算出總行數(shù)并把它作為一個(gè)列返回
  FROM t ---- 這個(gè)是有四行的集合
WHERE LEVEL=4       ---- 我們需要的僅僅是最后一層的結(jié)果。在PATH里面已經(jīng)包含了取四次的所有結(jié)果組合
CONNECT BY LEVEL<=4  ---- 沒(méi)有任何條件,前后都是獨(dú)立的
)
SELECT * FROM t2;

        ID PATH              CNT
---------- ---------- ----------
         1 1111              256
         2 1112              256
         3 1113              256
         4 1114              256
         5 1121              256
         6 1122              256
         7 1123              256
         8 1124              256
         9 1131              256
        10 1132              256
        11 1133              256
......(其余結(jié)果略)

256 rows selected.

由此看到PATH列已經(jīng)包含了四次抽取的所有可能結(jié)果,每個(gè)結(jié)果都被賦予一個(gè)唯一的編號(hào)ID。

如果你好奇的話可以看看下一步的結(jié)果集t3:
WITH t AS (
SELECT ROWNUM rn   -- 先構(gòu)造一個(gè)1,2,3,4的結(jié)果集,每個(gè)rn表示一種顏色
  FROM DUAL
CONNECT BY ROWNUM<=4
)
,t2 AS ( ---- 集合t2模擬獨(dú)立取四次的動(dòng)作,最終結(jié)果會(huì)有4*4*4*4=256行
SELECT ROWNUM id   ---- 構(gòu)造唯一ID供下面拆分用
      ,REPLACE(SYS_CONNECT_BY_PATH(rn,'@'),'@') path     ---- 用一個(gè)特殊字符@來(lái)作分隔符, 并在最后用REPLACE把它去除
      ,COUNT(*) OVER() cnt    ---- 利用分析函數(shù)算出總行數(shù)并把它作為一個(gè)列返回
  FROM t ---- 這個(gè)是有四行的集合
WHERE LEVEL=4       ---- 我們需要的僅僅是最后一層的結(jié)果。在PATH里面已經(jīng)包含了取四次的所有結(jié)果組合
CONNECT BY LEVEL<=4  ---- 沒(méi)有任何條件,前后都是獨(dú)立的
)
,t3 AS ( ---- 集合t3把t2中的PATH包含的顏色組合拆開(kāi)為四行
SELECT id,cnt,SUBSTR(PATH,rn,1) color
  FROM t2,t  ---- 笛卡兒積,用于把t2中的一行變?yōu)樗男?br>   )
SELECT * FROM t3;

        ID        CNT COLO
---------- ---------- ----
         1        256 1
         1        256 1
         1        256 1
         1        256 1
         2        256 1
         2        256 1
         2        256 1
         2        256 2
         3        256 1
         3        256 1
         3        256 1
         3        256 3
         4        256 1
         4        256 1
         4        256 1
         4        256 4
......(其余結(jié)果略)

1024 rows selected.
可以看到t2集合中的每一行都被拆成了四行,這是為了后面的聚合運(yùn)算。

最后看看算概率的主查詢:
SELECT COUNT(COUNT(*))/MAX(cnt) AS prob
  FROM t3
GROUP BY id,cnt
HAVING COUNT(DISTINCT color)=3;

COUNT(DISTINCT color)可以算出每個(gè)ID中包含不重復(fù)的顏色數(shù)目,放在HAVING中過(guò)濾了數(shù)目不為3的那些ID。

GROUP BY id,cnt 表示按照id來(lái)分組。因?yàn)樗行械腸nt都是一樣的(都等于256),我們?cè)诜纸M加入它并不會(huì)改變分組的結(jié)果,加入cnt的目的是為了在查詢中引用。
最后的連續(xù)兩層COUNT函數(shù)的意思是要把分組結(jié)果再聚合為一行,算出滿足條件的id的行數(shù)。除以cnt就得到了我們要的概率。

本例是一個(gè)在多行的結(jié)果集上進(jìn)行無(wú)條件遍歷的例子,前面說(shuō)過(guò)了要特別小心,因?yàn)闆](méi)有上下級(jí)關(guān)系,隨著層數(shù)遞增,數(shù)據(jù)量的增長(zhǎng)十分可觀。


遞歸WITH寫(xiě)法:

WITH T AS (
SELECT ROWNUM rn   -- 還是先構(gòu)造一個(gè)1,2,3,4的結(jié)果集
  FROM DUAL
CONNECT BY ROWNUM<=4
)
,t2(distinct_colors,lvl) AS (   --- 兩個(gè)列:所有不重復(fù)顏色,層次
   SELECT '\'||rn,1  ---- 第一層就是最基礎(chǔ)的四種顏色的表
     FROM t
   UNION ALL
   SELECT CASE WHEN INSTR(t2.distinct_colors||'\','\'||t.rn||'\')=0 --- 這個(gè)顏色沒(méi)有出現(xiàn)過(guò)
                    THEN t2.distinct_colors||'\'||t.rn              --- 拼上去
               ELSE t2.distinct_colors   ---- 顏色已經(jīng)出現(xiàn),保持原來(lái)的
          END  
         ,t2.lvl+1    --- 層數(shù)遞增
     FROM t, t2
    WHERE t2.lvl<4 --- 遞歸出口的條件:次數(shù)達(dá)到限制
)
SELECT COUNT(CASE WHEN LENGTH(distinct_colors) - LENGTH(REPLACE(distinct_colors,'\'))=3 THEN 1 END) --- 出現(xiàn)三個(gè)斜杠
       /COUNT(*)
FROM t2
WHERE lvl=4   ---- 同CONNECT BY類(lèi)似,我們只需觀察最后一層的數(shù)據(jù),在這里面已經(jīng)包含了所有層次的顏色
;

在遞歸WITH子查詢t2中,我們看到它用了一個(gè)CASE表達(dá)式把以前沒(méi)出現(xiàn)過(guò)的顏色拼接到distinct_colors中。這個(gè)CASE是遞歸WITH的妙處,用SYS_CONNECT_BY_PATH沒(méi)辦法做到有條件的拼接。

而最后在計(jì)算顏色數(shù)的時(shí)候用了一個(gè)技巧,把顏色數(shù)轉(zhuǎn)換為斜杠的個(gè)數(shù),因?yàn)槲覀儤?gòu)造數(shù)據(jù)的時(shí)候每種顏色前面都帶一個(gè)斜杠。為了求出字符串中某字符出現(xiàn)的次數(shù),我們用了這樣的辦法:
先求出字符串的總長(zhǎng)度;
用REPLACE函數(shù)從串中去除這個(gè)字符,然后再求一次長(zhǎng)度;
兩個(gè)長(zhǎng)度之差就是被去除的字符個(gè)數(shù)。
CASE函數(shù)把出現(xiàn)滿足條件的標(biāo)記置為1,不滿足則為NULL, 那么再套一個(gè)COUNT函數(shù)就能算出滿足條件的行數(shù),因?yàn)镹ULL是不被COUNT計(jì)入的。
COUNT和CASE的嵌套使用,也是在聚合運(yùn)算中常用的技巧。

這個(gè)顏色數(shù)的計(jì)算,我們也可以在遞歸的過(guò)程中進(jìn)行有條件累加,這樣最后就可以直接使用:
WITH T AS (
SELECT ROWNUM rn   -- 還是先構(gòu)造一個(gè)1,2,3,4的結(jié)果集
  FROM DUAL
CONNECT BY ROWNUM<=4
)
,t2(distinct_colors,lvl,distinct_colors_cnt) AS (   --- 兩個(gè)列:所有不重復(fù)顏色,層次,不重復(fù)的顏色數(shù)
   SELECT '\'||rn,1,1  ---- 第一層就是最基礎(chǔ)的四種顏色的表
     FROM t
   UNION ALL
   SELECT CASE WHEN INSTR(t2.distinct_colors||'\','\'||t.rn||'\')=0 --- 這個(gè)顏色沒(méi)有出現(xiàn)過(guò)
                    THEN t2.distinct_colors||'\'||t.rn              --- 拼上去
               ELSE t2.distinct_colors   ---- 顏色已經(jīng)出現(xiàn),保持原來(lái)的
          END  
         ,t2.lvl+1    --- 層數(shù)遞增
         ,CASE WHEN INSTR(t2.distinct_colors||'\','\'||t.rn||'\')=0 --- 這個(gè)顏色沒(méi)有出現(xiàn)過(guò)
                    THEN t2.distinct_colors_cnt + 1                  --- 顏色數(shù)累加
               ELSE t2.distinct_colors_cnt   ---- 顏色已經(jīng)出現(xiàn),數(shù)目不變
          END  
     FROM t, t2
    WHERE t2.lvl<4 --- 遞歸出口的條件:次數(shù)達(dá)到限制
)
SELECT COUNT(CASE WHEN distinct_colors_cnt=3 THEN 1 END) --- 出現(xiàn)三個(gè)斜杠
       /COUNT(*)
FROM t2
WHERE lvl=4   ---- 同CONNECT BY類(lèi)似,我們只需觀察最后一層的數(shù)據(jù),在這里面已經(jīng)包含了所有層次的顏色
;



例4:
構(gòu)造一個(gè)二階等差數(shù)列:這個(gè)數(shù)列的各項(xiàng)之差是一個(gè)等差數(shù)列
比如:1,3,6,10,15,21,...     
   
用CONNECT BY:

SELECT LEVEL, SUM(LEVEL) OVER(ORDER BY LEVEL) n
  FROM DUAL
CONNECT BY LEVEL<=10;

結(jié)果:
     LEVEL          N
---------- ----------
         1          1
         2          3
         3          6
         4         10
         5         15
         6         21
         7         28
         8         36
         9         45
        10         55

10 rows selected.

因?yàn)橹挥幸粭l路徑,所以用分析函數(shù)SUM很輕易做到了。

遞歸WITH寫(xiě)法:

WITH t(lvl,n) AS (
  SELECT 1,1 FROM DUAL --- 先構(gòu)造第一個(gè)
  UNION ALL
  SELECT t.lvl+1, t.lvl+1+t.n  ---- n的增幅本身是一個(gè)等差數(shù)列,即新的t.lvl
    FROM t      ---- 沒(méi)有任何連接,因?yàn)椴恍枰?br>    WHERE t.lvl<10  ---- 找到10個(gè)就停止
  )
SELECT * FROM T;

結(jié)果:
       LVL          N
---------- ----------
         1          1
         2          3
         3          6
         4         10
         5         15
         6         21
         7         28
         8         36
         9         45
        10         55
10 rows selected.

例5:
構(gòu)造斐波那契數(shù)列: 指的是這樣一個(gè)數(shù)列, 從第三項(xiàng)開(kāi)始,每一項(xiàng)都等于前兩項(xiàng)之和。
1,1,2,3,5,8,13,21,......

傳統(tǒng)的CONNECT BY方法做不出來(lái),但是用10G以上所支持的MODEL可以輕松構(gòu)造:
SELECT rn,n
FROM (SELECT ROWNUM rn FROM DUAL CONNECT BY ROWNUM<=10)
MODEL RETURN UPDATED ROWS
   DIMENSION BY (rn)
   MEASURES (1 n)
   RULES (
     n[any] order by rn=DECODE(cv(rn),1,1,2,1, n[cv()-2]+n[cv()-1]) ---- 用DECODE構(gòu)造最初的兩個(gè),其余的則賦值為最近兩項(xiàng)之和
   )
/

        RN          N
---------- ----------
         1          1
         2          1
         3          2
         4          3
         5          5
         6          8
         7         13
         8         21
         9         34
        10         55

10 rows selected.

用遞歸WITH的寫(xiě)法:

WITH t(n,last_n,cnt) AS (
  SELECT 1,0,1 FROM DUAL --- 先構(gòu)造第一個(gè)
  UNION ALL
  SELECT t.n+t.last_n, t.n, t.cnt+1  ---- 前兩項(xiàng)之和
    FROM t      ---- 沒(méi)有任何連接,因?yàn)椴恍枰?br>    WHERE t.cnt<10  ---- 找到10個(gè)就停止
  )
SELECT n FROM T;

         N
----------
         1
         1
         2
         3
         5
         8
        13
        21
        34
        55

10 rows selected.

例6:
排列組合:

從5個(gè)數(shù)中取3個(gè)的所有組合C(3,5):

CONNECT BY寫(xiě)法:
SELECT SYS_CONNECT_BY_PATH(rn, ',') xmlpath
FROM (SELECT ROWNUM RN FROM DUAL CONNECT BY LEVEL<6)
WHERE LEVEL=3
CONNECT BY rn<PRIOR rn AND LEVEL<=3    ---- 強(qiáng)行按降序排序,這樣就排除了其他相同的、只是順序不同的組合
;

XMLPATH
--------------
,5,4,3
,5,4,2
,5,4,1
,5,3,2
,5,3,1
,5,2,1
,4,3,2
,4,3,1
,4,2,1
,3,2,1
        
遞歸WITH寫(xiě)法:
WITH t AS (
   SELECT ROWNUM RN FROM DUAL CONNECT BY LEVEL<6
   )
,t2(rn,xmlpath,lvl) AS (  ---- 三個(gè)列:當(dāng)前節(jié)點(diǎn)值,路徑,層數(shù)
SELECT rn,','||rn,1 FROM t ---- 先構(gòu)造錨點(diǎn)成員的基礎(chǔ)數(shù)據(jù),就是上面生成的6行數(shù)據(jù)的集合
UNION ALL
SELECT t.rn,t2.xmlpath||','||t.rn,t2.lvl+1  --- 把當(dāng)前節(jié)點(diǎn)拼接入路徑,層數(shù)則遞增
   FROM t2, t
  WHERE t2.rn<t.rn AND t2.lvl<3
)
SELECT xmlpath FROM t2 WHERE lvl=3;

XMLPATH
-----------
,1,2,3
,1,2,4
,1,2,5
,1,3,4
,1,3,5
,1,4,5
,2,3,4
,2,3,5
,2,4,5
,3,4,5

10 rows selected.

如果要的不是組合而是排列,比如P(3,5)可以這么寫(xiě):

SELECT SYS_CONNECT_BY_PATH(rn, ',') xmlpath
FROM (SELECT ROWNUM rn FROM DUAL CONNECT BY LEVEL<6)
WHERE LEVEL=3
CONNECT BY NOCYCLE rn<>PRIOR rn AND LEVEL<=3;

XMLPATH
----------
,1,2,3
,1,2,4
,1,2,5
,1,3,2
,1,3,4
,1,3,5
,1,4,2
,1,4,3
,1,4,5
,1,5,2
,1,5,3
,1,5,4
,2,1,3
,2,1,4
......(其余結(jié)果略)

60 rows selected.

和剛才的組合寫(xiě)法相比,rn<PRIOR rn變成了NOCYCLE rn<>PRIOR rn, 這表示只要rn沒(méi)出現(xiàn)過(guò)就行,我們要的是所有的排列順序而不僅僅是降序。注意這里面的NOCYCLE, 這個(gè)是10G上才有的。

如果不寫(xiě)這個(gè)NOCYCLE會(huì)怎么樣?

SELECT SYS_CONNECT_BY_PATH(rn, ',') xmlpath
FROM (SELECT ROWNUM rn FROM DUAL CONNECT BY LEVEL<6)
WHERE LEVEL=3
CONNECT BY rn<>PRIOR rn AND LEVEL<=3;

ERROR:
ORA-01436: CONNECT BY loop in user data

可以看到,這個(gè)NOCYCLE是很重要的,ORACLE不允許遍歷順序中出現(xiàn)循環(huán)。

在遞歸WITH中,NOCYCLE的寫(xiě)法:
WITH t AS (
   SELECT ROWNUM RN FROM DUAL CONNECT BY LEVEL<6
   )
,T2(rn,xmlpath,lvl) AS (  ---- 三個(gè)列:當(dāng)前節(jié)點(diǎn)值,路徑,層數(shù)
SELECT rn,','||rn,1 FROM t ---- 先構(gòu)造錨點(diǎn)成員的基礎(chǔ)數(shù)據(jù),就是上面生成的6行數(shù)據(jù)的集合
UNION ALL
SELECT t.rn,t2.xmlpath||','||t.rn,t2.lvl+1  --- 把當(dāng)前節(jié)點(diǎn)拼接入路徑,層數(shù)則遞增
   FROM t2, t
  WHERE t2.rn<>t.rn AND t2.lvl<3
)
CYCLE rn SET cycle_flag TO 'Y' DEFAULT 'N'   ---- 這個(gè)cycle_flag是自己定義的偽列名和值,可以起到CONNECT_BY_ISCYCLE同樣的作用
SELECT xmlpath FROM t2 WHERE lvl=3 AND cycle_flag='N';

結(jié)果:
XMLPATH
-----------
,1,2,3
,1,2,4
,1,2,5
,1,3,2
,1,3,4
,1,3,5
,1,4,2
,1,4,3
,1,4,5
,1,5,2
,1,5,3
,1,5,4
,2,1,3
,2,1,4
,2,1,5
......(其余結(jié)果略)

60 rows selected.

在這里我們看到了前面例子中沒(méi)出現(xiàn)過(guò)的循環(huán)子句(CYCLE CLAUSE)。循環(huán)子句的語(yǔ)法如下:
① CYCLE c_alias [, c_alias]...   ---- 用哪些列來(lái)判斷是否發(fā)生CYCLE。一般來(lái)說(shuō)就是要遍歷的集合能夠唯一標(biāo)識(shí)出每行的那些列,例如主鍵。
②  SET cycle_mark_c_alias TO cycle_value  ---- cycle_mark_c_alias如果發(fā)生CYCLE了要設(shè)置什么值,一般就是Y/N, 1/0 來(lái)表示是否CYCLE
③  DEFAULT no_cycle_value                 ---- 沒(méi)有CYCLE要設(shè)什么值


① 用哪些列來(lái)判斷是否發(fā)生CYCLE。一般來(lái)說(shuō)就是要遍歷的集合能夠唯一標(biāo)識(shí)出每行的那些列,例如主鍵。
② cycle_mark_c_alias是用來(lái)存放CYCLE標(biāo)記的列名。在本例中我們?yōu)樗∶鹀ycle_flag。
   cycle_value是當(dāng)發(fā)生了循環(huán)(下一個(gè)節(jié)點(diǎn)已經(jīng)被遍歷過(guò))的時(shí)候,要把什么值存到cycle_mark_c_alias列中。
   因?yàn)槭遣紶栔?,我們一般用Y/N, 1/0來(lái)表示。在本例中用的是'Y'。
③ 在沒(méi)有循環(huán)的情況下要賦予cycle_mark_c_alia什么值。在本例中用的是'N'。

定義了循環(huán)子句之后,ORACLE碰到循環(huán)就會(huì)自動(dòng)停止并為你指定的cycle_mark_c_alias列賦予一個(gè)標(biāo)記。在隨后的SELECT中可以用這個(gè)標(biāo)記來(lái)過(guò)濾數(shù)據(jù)。

如果不寫(xiě)這個(gè)循環(huán)子句會(huì)怎么樣?

WITH t AS (
   SELECT ROWNUM RN FROM DUAL CONNECT BY LEVEL<6
   )
,T2(rn,xmlpath,lvl) AS (  ---- 三個(gè)列:當(dāng)前節(jié)點(diǎn)值,路徑,層數(shù)
SELECT rn,','||rn,1 FROM t ---- 先構(gòu)造錨點(diǎn)成員的基礎(chǔ)數(shù)據(jù),就是上面生成的6行數(shù)據(jù)的集合
UNION ALL
SELECT t.rn,t2.xmlpath||','||t.rn,t2.lvl+1  --- 把當(dāng)前節(jié)點(diǎn)拼接入路徑,層數(shù)則遞增
   FROM t2, t
  WHERE t2.rn<>t.rn
)
SELECT xmlpath FROM t2 WHERE lvl=3;

WITH t AS (
*
ERROR at line 1:
ORA-32044: cycle detected while executing recursive WITH query

假如在上述SQL的WHERE中加上層數(shù)限制,則錯(cuò)誤不會(huì)出現(xiàn),但是數(shù)據(jù)中有重復(fù)節(jié)點(diǎn):
WITH t AS (
   SELECT ROWNUM RN FROM DUAL CONNECT BY LEVEL<6
   )
,T2(rn,xmlpath,lvl) AS (  ---- 三個(gè)列:當(dāng)前節(jié)點(diǎn)值,路徑,層數(shù)
SELECT rn,','||rn,1 FROM t ---- 先構(gòu)造錨點(diǎn)成員的基礎(chǔ)數(shù)據(jù),就是上面生成的6行數(shù)據(jù)的集合
UNION ALL
SELECT t.rn,t2.xmlpath||','||t.rn,t2.lvl+1  --- 把當(dāng)前節(jié)點(diǎn)拼接入路徑,層數(shù)則遞增
   FROM t2, t
  WHERE t2.rn<>t.rn AND lvl<3
)
SELECT xmlpath FROM t2 WHERE lvl=3;

結(jié)果:
XMLPATH
------------
,1,2,1   ------ 可以看到1被訪問(wèn)了兩次
,1,2,3
,1,2,4
,1,2,5
,1,3,1
,1,3,2
,1,3,4
,1,3,5
,1,4,1
,1,4,2
,1,4,3
,1,4,5
,1,5,1
,1,5,2
......(其余結(jié)果略)

80 rows selected.

那么這個(gè)問(wèn)題不用循環(huán)子句能否解決呢?其實(shí)我們?cè)谇懊娴睦?中已經(jīng)用到了一個(gè)技巧,就是在遞歸的部分加上一個(gè)條件,僅當(dāng)該節(jié)點(diǎn)未出現(xiàn)過(guò)時(shí)才加入:
WITH t AS (
   SELECT ROWNUM RN FROM DUAL CONNECT BY LEVEL<6
   )
,T2(rn,xmlpath,lvl) AS (  ---- 三個(gè)列:當(dāng)前節(jié)點(diǎn)值,路徑,層數(shù)
SELECT rn,','||rn,1 FROM t ---- 先構(gòu)造錨點(diǎn)成員的基礎(chǔ)數(shù)據(jù),就是上面生成的6行數(shù)據(jù)的集合
UNION ALL
SELECT t.rn,t2.xmlpath||','||t.rn,t2.lvl+1  --- 把當(dāng)前節(jié)點(diǎn)拼接入路徑,層數(shù)則遞增
   FROM t2, t
  WHERE t2.rn<>t.rn AND t2.lvl<3
        AND INSTR(t2.xmlpath,t.rn)=0  ------- 新加入的節(jié)點(diǎn)必須是未出現(xiàn)過(guò)的
)
SELECT xmlpath FROM t2 WHERE lvl=3;


例7:
求遍歷樹(shù)中的葉子節(jié)點(diǎn)。

在例1中我們看到一個(gè)求出所有員工的上級(jí)關(guān)系的層次查詢?,F(xiàn)在假設(shè)要把最下級(jí)的員工求出來(lái):

CONNECT BY寫(xiě)法:
SELECT empno, ename, job, deptno,LEVEL, SYS_CONNECT_BY_PATH(ename,'\') as path
  FROM EMP
WHERE CONNECT_BY_ISLEAF=1  ----- 葉子節(jié)點(diǎn)
START WITH mgr IS NULL -- mgr列為空,表示沒(méi)有上級(jí),該員工已經(jīng)是最高級(jí)別。這是層次查詢的起點(diǎn)
CONNECT BY PRIOR empno= mgr;

結(jié)果:
EMPNO ENAME      JOB       DEPTNO LEVEL  PATH
-----------------------------------------------------------------
7876  ADAMS      CLERK     20     4      \KING\JONES\SCOTT\ADAMS
7369  SMITH      CLERK     20     4      \KING\JONES\FORD\SMITH
7499  ALLEN      SALESMAN  30     3      \KING\BLAKE\ALLEN
7521  WARD       SALESMAN  30     3      \KING\BLAKE\WARD
7654  MARTIN     SALESMAN  30     3      \KING\BLAKE\MARTIN
7844  TURNER     SALESMAN  30     3      \KING\BLAKE\TURNER
7900  JAMES      CLERK     30     3      \KING\BLAKE\JAMES
7934  MILLER     CLERK     10     3      \KING\CLARK\MILLER

CONNECT_BY_ISLEAF是10G以上提供的,它是一個(gè)偽列,當(dāng)CONNECT_BY_ISLEAF=1時(shí)表示該行為葉子節(jié)點(diǎn)。
在9i中沒(méi)有CONNECT_BY_ISLEAF這個(gè)偽列,我們可以根據(jù)葉子節(jié)點(diǎn)的定義寫(xiě)一個(gè)相關(guān)子查詢:
SELECT empno, ename, job, deptno,LEVEL, SYS_CONNECT_BY_PATH(ename,'\') as path
  FROM EMP e
WHERE NOT EXISTS (SELECT NULL FROM emp WHERE mgr = e.empno)
START WITH mgr IS NULL
CONNECT BY PRIOR empno= mgr;

在這里,NOT EXISTS相關(guān)子查詢的意思即不存在下一級(jí)數(shù)據(jù),這正是葉子節(jié)點(diǎn)的定義。


在遞歸WITH子查詢中實(shí)現(xiàn)CONNECT_BY_ISLEAF的方法:

WITH T(empno, ename, mgr, the_level, path) AS ( ---- 必須把結(jié)構(gòu)寫(xiě)出來(lái)
   SELECT empno, ename, mgr  ---- 先寫(xiě)錨點(diǎn)查詢,用START WITH的條件
         ,1 AS the_level    ---- 遞歸起點(diǎn),第一層
         ,'\'||ename        ---- 路徑的第一截
     FROM EMP
    WHERE mgr IS NULL ---- 原來(lái)的START WITH條件
   UNION ALL  ---- 下面是遞歸部分
   SELECT e.empno, e.ename, e.mgr     ---- 要加入的新一層數(shù)據(jù),來(lái)自要遍歷的emp表
         ,1 + t.the_level             ---- 遞歸層次,在原來(lái)的基礎(chǔ)上加1
         ,t.path||'\'||e.ename        ---- 把新的一截路徑拼上去
     FROM t, emp e                    ---- 典型寫(xiě)法,把子查詢本身和要遍歷的表作一個(gè)連接
    WHERE t.empno = e.mgr             ---- 原來(lái)的CONNECT BY條件
)
SEARCH DEPTH FIRST BY mgr SET seq   --- 指定深度優(yōu)先, 按順序生成序號(hào)seq
SELECT * FROM (
SELECT T.*
      ,(CASE WHEN the_level < LEAD(the_level) OVER (ORDER BY seq) THEN 0 ELSE 1 END) is_leaf ---- 如果層數(shù)不增加則為葉子節(jié)點(diǎn)。
  FROM T
  )
WHERE is_leaf=1
;

EMPNO ENAME          MGR  THE_LEVEL PATH                                SEQ    IS_LEAF
------ ---------- ------- ---------- ---------------------------- ---------- ----------
  7876 ADAMS         7788          4 \KING\JONES\SCOTT\ADAMS               4          1
  7369 SMITH         7902          4 \KING\JONES\FORD\SMITH                6          1
  7499 ALLEN         7698          3 \KING\BLAKE\ALLEN                     8          1
  7521 WARD          7698          3 \KING\BLAKE\WARD                      9          1
  7654 MARTIN        7698          3 \KING\BLAKE\MARTIN                   10          1
  7844 TURNER        7698          3 \KING\BLAKE\TURNER                   11          1
  7900 JAMES         7698          3 \KING\BLAKE\JAMES                    12          1
  7934 MILLER        7782          3 \KING\CLARK\MILLER                   14          1

8 rows selected.

seq 是整個(gè)結(jié)果集按照深度優(yōu)先遍歷順序排序后的序號(hào)。按照深度優(yōu)先的定義,每個(gè)節(jié)點(diǎn)都要比它的上一節(jié)點(diǎn)更深一層,除非已經(jīng)到底(也就是葉子節(jié)點(diǎn))。因此如果層數(shù)沒(méi)有增加,則我們知道碰到了葉子節(jié)點(diǎn),這就是最后一個(gè)CASE表達(dá)式的依據(jù)。

這個(gè)例子中出現(xiàn)了前面沒(méi)有的搜索子句(SEARCH CLAUSE)。

SEARCH子句的語(yǔ)法:

SEARCH
①   { DEPTH FIRST BY c_alias [, c_alias]...
        [ ASC | DESC ]
        [ NULLS FIRST | NULLS LAST ]
②   | BREADTH FIRST BY c_alias [, c_alias]...
        [ ASC | DESC ]
        [ NULLS FIRST | NULLS LAST ]
     }
③    SET ordering_column

①DEPTH FIRST表示按深度優(yōu)先的順序來(lái)輸出。BY后面的列名及其升降序、空值放置順序指明了在深度優(yōu)先的前提下,同一層次的數(shù)據(jù)的排序情況。這和原來(lái)CONNECT BY查詢中的ORDER SIBLING BY子句是一樣的。
②BREADTH FIRST表示按廣度優(yōu)先的順序來(lái)輸出。BY后面的列名及其升降序、空值放置順序指明了在廣度優(yōu)先的前提下,同一層次的數(shù)據(jù)的排序情況。
③列名ordering_column用于存放排序后的序號(hào),是一個(gè)從1開(kāi)始的連續(xù)正整數(shù)。后續(xù)的查詢中可以利用這個(gè)列得知某行數(shù)據(jù)在整個(gè)結(jié)果集中的位置。

那么這個(gè)搜索子句是不是可以指定ORACLE的搜索順序呢?我們從遞歸WITH的寫(xiě)法來(lái)看,數(shù)據(jù)是一層一層疊加上去的,也就是廣度優(yōu)先算法。
為了證明這一點(diǎn),我們可以寫(xiě)一個(gè)自定義函數(shù),根據(jù)輸出的順序來(lái)判斷ORACLE的搜索順序:

CREATE OR REPLACE FUNCTION f_get_mgr (
       p_empno    IN NUMBER   ---- 當(dāng)前員工編號(hào)
      ,p_mgr      IN NUMBER   ---- 當(dāng)前員工的經(jīng)理編號(hào)
      ,p_last_emp IN NUMBER   ---- 上一層員工的編號(hào)
      ,p_level    IN NUMBER   ---- 當(dāng)前員工的層數(shù)
      )
RETURN NUMBER
AS
BEGIN
   IF p_last_emp = p_mgr THEN  
      ---- 假如發(fā)生匹配,證明這個(gè)節(jié)點(diǎn)將被加入,這時(shí)我們輸出當(dāng)前員工的編號(hào)。從屏幕的輸出就可以判斷節(jié)點(diǎn)被加入的順序
      ---- 此輸出可以觀察函數(shù)被調(diào)用的順序
      DBMS_OUTPUT.PUT_LINE('level='||p_level||' empno='||p_empno);
   END IF;
   RETURN p_mgr;
END f_get_mgr;
/

把查詢修改一下:
WITH T(empno, ename, mgr, the_level, path) AS (
   SELECT empno, ename, mgr
         ,1 AS the_level   
         ,'\'||ename        
     FROM EMP
    WHERE mgr IS NULL
   UNION ALL  
   SELECT e.empno, e.ename, e.mgr  
         ,1 + t.the_level         
         ,t.path||'\'||e.ename     
     FROM t, emp e
    WHERE t.empno = f_get_mgr(e.empno,e.mgr,t.empno, t.the_level+1) ----此函數(shù)會(huì)返回經(jīng)理編號(hào),并在屏幕上輸出員工編號(hào)
)
SEARCH DEPTH FIRST BY mgr SET seq  ---- 強(qiáng)行指定深度優(yōu)先順序
SELECT * FROM T
;

輸出:              
   EMPNO ENAME         MGR  THE_LEVEL PATH                              SEQ
-------- ---------- ------ ---------- -------------------------- ----------
    7839 KING                       1 \KING                               1
    7566 JONES        7839          2 \KING\JONES                         2
    7788 SCOTT        7566          3 \KING\JONES\SCOTT                   3
    7876 ADAMS        7788          4 \KING\JONES\SCOTT\ADAMS             4
    7902 FORD         7566          3 \KING\JONES\FORD                    5
    7369 SMITH        7902          4 \KING\JONES\FORD\SMITH              6
    7698 BLAKE        7839          2 \KING\BLAKE                         7
    7499 ALLEN        7698          3 \KING\BLAKE\ALLEN                   8
    7521 WARD         7698          3 \KING\BLAKE\WARD                    9
    7654 MARTIN       7698          3 \KING\BLAKE\MARTIN                 10
    7844 TURNER       7698          3 \KING\BLAKE\TURNER                 11
    7900 JAMES        7698          3 \KING\BLAKE\JAMES                  12
    7782 CLARK        7839          2 \KING\CLARK                        13
    7934 MILLER       7782          3 \KING\CLARK\MILLER                 14

14 rows selected.

level=2 empno=7566
level=2 empno=7698
level=2 empno=7782
level=3 empno=7788
level=3 empno=7902
level=3 empno=7499
level=3 empno=7521
level=3 empno=7654
level=3 empno=7844
level=3 empno=7900
level=3 empno=7934
level=4 empno=7876
level=4 empno=7369

盡管前半部分輸出已經(jīng)按照深度優(yōu)先順序排列,但從后半部分的輸出來(lái)看,節(jié)點(diǎn)是逐層被加入的,可見(jiàn)遍歷還是按廣度優(yōu)先順序來(lái)的。
如果我們把查詢改為廣度優(yōu)先,就和函數(shù)調(diào)用順序一致了:

WITH T(empno, ename, mgr, the_level, path) AS (
   SELECT empno, ename, mgr
         ,1 AS the_level   
         ,'\'||ename        
     FROM EMP
    WHERE mgr IS NULL
   UNION ALL  
   SELECT e.empno, e.ename, e.mgr  
         ,1 + t.the_level         
         ,t.path||'\'||e.ename     
     FROM t, emp e
    WHERE t.empno = f_get_mgr(e.empno,e.mgr,t.empno, t.the_level+1) ----此函數(shù)會(huì)返回經(jīng)理編號(hào),并在屏幕上輸出員工編號(hào)
)
SEARCH BREADTH FIRST BY mgr SET seq  ---- 強(qiáng)行指定廣度優(yōu)先順序
SELECT * FROM T
;

輸出:
  EMPNO ENAME         MGR  THE_LEVEL PATH                          SEQ
------- ---------- ------ ---------- -------------------------- ------
   7839 KING                       1 \KING                           1
   7566 JONES        7839          2 \KING\JONES                     2
   7698 BLAKE        7839          2 \KING\BLAKE                     3
   7782 CLARK        7839          2 \KING\CLARK                     4
   7788 SCOTT        7566          3 \KING\JONES\SCOTT               5
   7902 FORD         7566          3 \KING\JONES\FORD                6
   7499 ALLEN        7698          3 \KING\BLAKE\ALLEN               7
   7521 WARD         7698          3 \KING\BLAKE\WARD                8
   7654 MARTIN       7698          3 \KING\BLAKE\MARTIN              9
   7844 TURNER       7698          3 \KING\BLAKE\TURNER             10
   7900 JAMES        7698          3 \KING\BLAKE\JAMES              11
   7934 MILLER       7782          3 \KING\CLARK\MILLER             12
   7876 ADAMS        7788          4 \KING\JONES\SCOTT\ADAMS        13
   7369 SMITH        7902          4 \KING\JONES\FORD\SMITH         14

14 rows selected.

level=2 empno=7566
level=2 empno=7698
level=2 empno=7782
level=3 empno=7788
level=3 empno=7902
level=3 empno=7499
level=3 empno=7521
level=3 empno=7654
level=3 empno=7844
level=3 empno=7900
level=3 empno=7934
level=4 empno=7876
level=4 empno=7369

以往也有人在9i中用這個(gè)方法制造is_leaf標(biāo)記,但是該方法依賴(lài)于CONNECT BY的遍歷順序生成的ROWNUM,而ORACLE并沒(méi)有保障ROWNUM一定會(huì)按深度優(yōu)先的順序生成?,F(xiàn)在遞歸WITH子句支持遍歷輸出順序的顯式指定,就可以放心使用了。

--------------------------------------------------------

例8:

下面的例子來(lái)自我的一個(gè)老貼:
http://www./thread-1037629-1-1.html

機(jī)票路線問(wèn)題:假設(shè)有一張表存儲(chǔ)了起飛、到達(dá)機(jī)場(chǎng)以及這段路線的票價(jià),現(xiàn)在要求出從城市1到城市2的所有飛行線路及其總價(jià)格。

測(cè)試環(huán)境設(shè)置:
CREATE TABLE fares   ---- 票價(jià)
(
    depart VARCHAR2(3),   --- 起飛機(jī)場(chǎng)
    arrive VARCHAR2(3),   --- 到達(dá)機(jī)場(chǎng)
    price  NUMBER
);

測(cè)試數(shù)據(jù):
INSERT INTO FARES VALUES('BJ','SH',500);
INSERT INTO FARES VALUES('SH','GZ',1500);
INSERT INTO FARES VALUES('BJ','GZ',1800);
INSERT INTO FARES VALUES('GZ','BJ',1600);
insert into fares values('GZ','SH',1300);
INSERT INTO FARES VALUES('BJ','SZ',100);
INSERT INTO FARES VALUES('SZ','GZ',110);

COMMIT;

用CONNECT BY求出所有北京到上海的線路,轉(zhuǎn)機(jī)不超過(guò)10次:

WITH f1 AS (
SELECT LEVEL lvl
      ,SYS_CONNECT_BY_PATH(depart,'/') ---- 用SYS_CONNECT_BY_PATH把所有的出發(fā)機(jī)場(chǎng)拼接起來(lái)。
       ||'/'||arrive||'/'  AS path     ---- 在右邊拼上最后一段旅程的到達(dá)機(jī)場(chǎng),構(gòu)成完整的路徑。最后添加的斜杠是為了下面解析方便。
  FROM fares
WHERE arrive = 'SH'      ---- 目的地是上海
START WITH depart = 'BJ' ---- 從北京出發(fā)
CONNECT BY NOCYCLE depart=prior arrive   ---- 每段路線首位銜接
            AND 'BJ'<>arrive   ----- 目的地不能是北京,否則就繞回去了
            AND LEVEL <= 11    ----- 轉(zhuǎn)機(jī)不超過(guò)10次
)
SELECT * FROM f1;

輸出:
       LVL PATH
---------- ------------------------------------------
         2 /BJ/GZ/SH/
         1 /BJ/SH/
         3 /BJ/SZ/GZ/SH/

可以看到有三條飛行路線,LEVEL表示了路線分為幾段(轉(zhuǎn)機(jī)次數(shù))。
現(xiàn)在路線有了,接下來(lái)就得算總價(jià)格。這就意味這必須和fares做一個(gè)連接,路線的每一段都必須拆成起飛、到達(dá)機(jī)場(chǎng)這樣的格式。
為了把一行拆成多行,我們必須構(gòu)造一個(gè)自然數(shù)集(從1到最大LEVEL)連接上去;而每段路線的起始點(diǎn)可以用字符串函數(shù)INSTR, SUBSTR函數(shù)解析出來(lái)。

WITH f1 AS (
SELECT LEVEL lvl
      ,SYS_CONNECT_BY_PATH(depart,'/') ---- 用SYS_CONNECT_BY_PATH把所有的出發(fā)機(jī)場(chǎng)拼接起來(lái)。
       ||'/'||arrive||'/'  AS path     ---- 在右邊拼上最后一段旅程的到達(dá)機(jī)場(chǎng),構(gòu)成完整的路徑。最后添加的斜杠是為了下面解析方便。
  FROM fares
WHERE arrive = 'SH'      ---- 目的地是上海
START WITH depart = 'BJ' ---- 從北京出發(fā)
CONNECT BY NOCYCLE depart=prior arrive   ---- 每段路線首位銜接
            AND 'BJ'<>arrive   ----- 目的地不能是北京,否則就繞回去了
            AND LEVEL <= 11    ----- 轉(zhuǎn)機(jī)不超過(guò)10次
)
,f2 AS (
SELECT path
      ,SUBSTR(path, INSTR(path,'/',1,n  )+1, INSTR(path,'/',1,n+1)-INSTR(path,'/',1,n  )-1) AS depart
      ,SUBSTR(path, INSTR(path,'/',1,n+1)+1, INSTR(path,'/',1,n+2)-INSTR(path,'/',1,n+1)-1) AS arrive
FROM f1
     ,(SELECT LEVEL AS n    ------- 利用CONNECT BY構(gòu)造出一個(gè)自然數(shù)集合
         FROM DUAL
        CONNECT BY LEVEL<=(SELECT MAX(lvl) FROM f1) ---- 所需要的總行數(shù)不超過(guò)集合f1里面的最多的路線段數(shù)
       ) r
WHERE r.n<=lvl  ------ 每一行包含的路線段數(shù)為lvl, 相對(duì)于這一行我們需要拆成行數(shù)為lvl,即從集合r中取出1~lvl行來(lái)進(jìn)行連接。
)
SELECT * FROM f2
ORDER BY 1;


輸出:
PATH                       DEPART     ARRIVE
-------------------------- ---------- ----------
/BJ/GZ/SH/                 GZ         SH
/BJ/GZ/SH/                 BJ         GZ
/BJ/SH/                    BJ         SH
/BJ/SZ/GZ/SH/              SZ         GZ
/BJ/SZ/GZ/SH/              GZ         SH
/BJ/SZ/GZ/SH/              BJ         SZ

6 rows selected.

至此我們不僅擁有了所有北京到上海的飛行線路,而且這條線路的組成也都分解出來(lái)了。接下來(lái)的事情就是和fares表做一個(gè)連接求出票價(jià),然后按PATH來(lái)做分組聚合求出線路總成本:

WITH f1 AS (
SELECT LEVEL lvl
      ,SYS_CONNECT_BY_PATH(depart,'/') ---- 用SYS_CONNECT_BY_PATH把所有的出發(fā)機(jī)場(chǎng)拼接起來(lái)。
       ||'/'||arrive||'/'  AS path     ---- 在右邊拼上最后一段旅程的到達(dá)機(jī)場(chǎng),構(gòu)成完整的路徑。最后添加的斜杠是為了下面解析方便。
  FROM fares
WHERE arrive = 'SH'      ---- 目的地是上海
START WITH depart = 'BJ' ---- 從北京出發(fā)
CONNECT BY NOCYCLE depart=prior arrive   ---- 每段路線首位銜接
            AND 'BJ'<>arrive   ----- 目的地不能是北京,否則就繞回去了
            AND LEVEL <= 11    ----- 轉(zhuǎn)機(jī)不超過(guò)10次
)
,f2 AS (
SELECT path
      ,SUBSTR(path, INSTR(path,'/',1,n  )+1, INSTR(path,'/',1,n+1)-INSTR(path,'/',1,n  )-1) AS depart
      ,SUBSTR(path, INSTR(path,'/',1,n+1)+1, INSTR(path,'/',1,n+2)-INSTR(path,'/',1,n+1)-1) AS arrive
FROM f1
     ,(SELECT LEVEL AS n    ------- 利用CONNECT BY構(gòu)造出一個(gè)自然數(shù)集合
         FROM DUAL
        CONNECT BY LEVEL<=(SELECT MAX(lvl) FROM f1) ---- 所需要的總行數(shù)不超過(guò)集合f1里面的最多的路線段數(shù)
       ) r
WHERE r.n<=lvl  ------ 每一行包含的路線段數(shù)為lvl, 相對(duì)于這一行我們需要拆成行數(shù)為lvl,即從集合r中取出1~lvl行來(lái)進(jìn)行連接。
)
SELECT SUM(price) AS cost,path
  FROM f2,fares
WHERE f2.depart=fares.depart AND f2.arrive = fares.arrive
GROUP BY path
ORDER BY cost;

結(jié)果:
      COST PATH
---------- ------------------------
       500 /BJ/SH/
      1510 /BJ/SZ/GZ/SH/
      3100 /BJ/GZ/SH/

另外也有一個(gè)稍微簡(jiǎn)化的寫(xiě)法。在拼接路徑的時(shí)候,我們可以把價(jià)格也拼起來(lái),最后只要分解開(kāi)再求和就可以了,無(wú)需和fares表再做連接:

WITH f1 AS (
SELECT LEVEL lvl
      ,SYS_CONNECT_BY_PATH(depart,'/') ---- 用SYS_CONNECT_BY_PATH把所有的出發(fā)機(jī)場(chǎng)拼接起來(lái)。
       ||'/'||arrive||'/'  AS path     ---- 在右邊拼上最后一段旅程的到達(dá)機(jī)場(chǎng),構(gòu)成完整的路徑。最后添加的斜杠是為了下面解析方便。
      ,SYS_CONNECT_BY_PATH(price,'/')||'/'  AS price_path ---- 用SYS_CONNECT_BY_PATH把所有的票價(jià)拼接起來(lái)。
  FROM fares
WHERE arrive = 'SH'      ---- 目的地是上海
START WITH depart = 'BJ' ---- 從北京出發(fā)
CONNECT BY NOCYCLE depart=prior arrive   ---- 每段路線首位銜接
            AND 'BJ'<>arrive   ----- 目的地不能是北京,否則就繞回去了
            AND LEVEL <= 11    ----- 轉(zhuǎn)機(jī)不超過(guò)10次
)
,f2 AS (
SELECT path
      ,SUBSTR(price_path, INSTR(price_path,'/',1,n)+1, INSTR(price_path,'/',1,n+1)-INSTR(price_path,'/',1,n)-1) AS price
FROM f1
     ,(SELECT LEVEL AS n    ------- 利用CONNECT BY構(gòu)造出一個(gè)自然數(shù)集合
         FROM DUAL
        CONNECT BY LEVEL<=(SELECT MAX(lvl) FROM f1) ---- 所需要的總行數(shù)不超過(guò)集合f1里面的最多的路線段數(shù)
       ) r
WHERE r.n<=lvl  ------ 每一行包含的路線段數(shù)為lvl, 相對(duì)于這一行我們需要拆成行數(shù)為lvl,即從集合r中取出1~lvl行來(lái)進(jìn)行連接。
)
SELECT SUM(price) AS cost,path
  FROM f2
GROUP BY path
ORDER BY cost;

上述用CONNECT BY求出總票價(jià)的方法十分繁瑣,因?yàn)镾YS_CONNECT_BY_PATH必須先分開(kāi)再聚合。

遞歸WITH的方法:沿路徑求和的實(shí)現(xiàn)非常方便。

WITH T (depart,arrive,path,cost,lvl) AS (
SELECT  depart  ---- 構(gòu)造第一層數(shù)據(jù):從起點(diǎn)城市出發(fā)
       ,arrive
       ,'/'||depart AS PATH
       ,price
       ,1
   FROM fares
  WHERE depart = 'BJ'  ---- 起點(diǎn)是北京
UNION ALL  ------- 遞歸部分:把銜接的新一段路程拼接進(jìn)去
SELECT f.depart
      ,f.arrive
      ,t.path||'/'||f.depart    ----- 把新的路段的起點(diǎn)機(jī)場(chǎng)拼接上去
      ,t.cost + f.price         ----- 把新的路段的票價(jià)累加到總成本中。這是遞歸WITH最強(qiáng)大的地方。
      ,t.lvl+1                  ----- 層數(shù)遞增
  FROM t,fares f
WHERE f.depart=t.arrive        ----- 遞歸條件:起飛機(jī)場(chǎng)是上一段的到達(dá)機(jī)場(chǎng)
       AND 'BJ'<>f.arrive       ----- 目的地不能是北京,否則就繞回去了
       AND t.arrive<>'SH'       ----- 遞歸終止條件:如果上一段終點(diǎn)已經(jīng)是上海,沒(méi)必要繼續(xù)遍歷了
       AND t.cost + f.price <5000 ------- 控制總成本在5000以內(nèi),否則停止遍歷。這個(gè)剪枝功能是CONNECT BY做不到的。
       AND lvl<=10   -------- 控制轉(zhuǎn)機(jī)次數(shù),轉(zhuǎn)機(jī)不超過(guò)10次
       AND INSTR(t.path,'/'||f.depart)=0  ------ 新一段路程的出發(fā)機(jī)場(chǎng)在路徑中未出現(xiàn)過(guò)。相當(dāng)于CONNECT BY的NOCYCLE功能,或是遞歸WITH中的CYCLE子句。
)
SELECT t.path||'/'||t.arrive path  ---- 在右邊拼上最后一段旅程的到達(dá)機(jī)場(chǎng),構(gòu)成完整的路徑。
      ,t.cost
FROM T WHERE arrive='SH';

PATH                      COST
------------------- ----------
/BJ/SH                     500
/BJ/GZ/SH                 3100
/BJ/SZ/GZ/SH              1510

例8:
沿路徑求乘積應(yīng)用例子: BOM

BOM(Bill Of Material)是一個(gè)ERP的相關(guān)術(shù)語(yǔ),指的是物料清單,又稱(chēng)材料表或配方料表,它反映生產(chǎn)產(chǎn)品與其組件的數(shù)量和從屬關(guān)系。

復(fù)雜的版本見(jiàn):
http://www./thread-1233081-3-1.html

這里是一個(gè)簡(jiǎn)化版。

測(cè)試環(huán)境設(shè)置:
CREATE TABLE BOM (
       item_no VARCHAR2(10)      ---- 部件編號(hào)
      ,part_no VARCHAR2(10)      ---- 構(gòu)成該部件的子部件編號(hào)
      ,qty NUMBER                ---- 構(gòu)成該部件的子部件數(shù)量
      );

測(cè)試數(shù)據(jù):
INSERT INTO BOM VALUES ('A','B',3);
INSERT INTO BOM VALUES ('A','C',2);
INSERT INTO BOM VALUES ('A','D',4);
INSERT INTO BOM VALUES ('B','E',2);
INSERT INTO BOM VALUES ('B','F',3);
INSERT INTO BOM VALUES ('D','G',6);
INSERT INTO BOM VALUES ('D','H',5);
INSERT INTO BOM VALUES ('E','I',3);
COMMIT;

例子數(shù)據(jù)中部件A由3個(gè)B, 2個(gè)C和4個(gè)D組成,一個(gè)B由2個(gè)E和3個(gè)F組成,等等。

現(xiàn)在要求組成A的所有零部件及其數(shù)量。

用CONNECT BY的辦法十分困難。和上例類(lèi)似,首先要求出樹(shù)的遍歷路徑,然后再把路徑拆開(kāi)為多行,求出每段的數(shù)量構(gòu)成;最后再做聚合運(yùn)算。因?yàn)闆](méi)有現(xiàn)成的聚合求積函數(shù),必須轉(zhuǎn)換為對(duì)數(shù),求出對(duì)數(shù)之和,再取反對(duì)數(shù)。

WITH t AS (
SELECT ROWNUM id ----- 為每行分配一個(gè)唯一的id, 用于隨后的記錄拆分和聚合
      ,part_no
      ,SYS_CONNECT_BY_PATH(qty,',') PATH    ----- 把每一段的組成數(shù)量拼接起來(lái)
      ,LEVEL lvl
  FROM bom
START WITH item_no='A'
CONNECT BY item_no = PRIOR part_no
)
,t2 AS (
SELECT id
      ,part_no
      ,EXP(SUM(LN(REGEXP_SUBSTR(path,'[^,]+',1,rn)) ------ 用規(guī)則表達(dá)式辨識(shí)出分隔符(逗號(hào)),把每一段數(shù)量qty求出來(lái);用LN函數(shù)取自然對(duì)數(shù);用SUM求和;用EXP指數(shù)函數(shù)求出乘積
              )
          ) qty
  FROM t,(SELECT ROWNUM rn   ------- 利用CONNECT BY構(gòu)造出一個(gè)自然數(shù)集合
           FROM (SELECT MAX(lvl) lvl FROM t)
          CONNECT BY ROWNUM<=lvl
         ) r
WHERE rn <= lvl   ------ 每一行包含的段數(shù)為lvl, 相對(duì)于這一行我們需要拆成行數(shù)為lvl,即從集合r中取出1~lvl行來(lái)進(jìn)行連接。
GROUP BY id,part_no
)
SELECT part_no,SUM(qty) from t2 GROUP BY part_no;

結(jié)果:
PART_NO      SUM(QTY)
---------- ----------
H                  20
I                  18
D                   4
B                   3
C                   2
F                   9
E                   6
G                  24

8 rows selected.

用遞歸WITH的辦法可謂賞心悅目:
WITH t(part_no,qty) AS (
SELECT part_no  ---- 取出第一層數(shù)據(jù),即需要計(jì)算的部件A
      ,qty
  FROM bom
WHERE item_no='A'
UNION ALL
SELECT b.part_no
      ,t.qty*b.qty  ---- 新加入的節(jié)點(diǎn)乘上已有的乘積,就是這條路徑的總乘積
  FROM t, bom b
WHERE b.item_no = t.part_no
)
SELECT part_no,SUM(qty) from t GROUP BY part_no;
  
結(jié)果:
PART_NO      SUM(QTY)
---------- ----------
H                  20
I                  18
D                   4
B                   3
C                   2
E                   6
F                   9
G                  24

8 rows selected.

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買(mǎi)等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類(lèi)似文章 更多

    亚洲视频一区二区久久久| 又大又长又粗又猛国产精品| 日本免费一本一二区三区| 中文人妻精品一区二区三区四区 | 久久福利视频这里有精品| 国产成人免费高潮激情电| 亚洲婷婷开心色四房播播| 尹人大香蕉一级片免费看| 二区久久久国产av色| 欧美精品激情视频一区| 亚洲精品国产主播一区| 欧洲日韩精品一区二区三区| 国产一区二区三中文字幕| 欧美熟妇一区二区在线| 亚洲一二三四区免费视频| 丰满少妇被粗大猛烈进出视频| 日本在线不卡高清欧美| 国产一区二区不卡在线视频| 国语对白刺激高潮在线视频| 国产av精品一区二区| 青草草在线视频免费视频| 麻豆剧果冻传媒一二三区| 91免费一区二区三区| 五月婷日韩中文字幕四虎| 激情视频在线视频在线视频| 国产成人精品久久二区二区| 国产精品视频第一第二区| 午夜色午夜视频之日本| 欧美中文日韩一区久久| 国产又粗又长又爽又猛的视频| 东京不热免费观看日本| 亚洲男人的天堂就去爱| 国产亚洲中文日韩欧美综合网| 国产在线小视频你懂的| 日韩不卡一区二区在线| 国产又大又猛又粗又长又爽| 成人综合网视频在线观看| 亚洲欧美天堂精品在线| 国产亚洲欧美日韩国亚语| 欧美日韩国产综合在线| 国产又粗又深又猛又爽又黄|