楼主: newkid

[精华] 11GR2的递归WITH子查询(新增内容在第二页)

[复制链接]
论坛徽章:
69
生肖徽章2007版:羊
日期:2008-11-14 14:42:19复活蛋
日期:2011-08-06 08:59:05ITPUB十周年纪念徽章
日期:2011-11-01 16:19:412012新春纪念徽章
日期:2012-01-04 11:49:542012新春纪念徽章
日期:2012-02-13 15:13:202012新春纪念徽章
日期:2012-02-13 15:13:202012新春纪念徽章
日期:2012-02-13 15:13:202012新春纪念徽章
日期:2012-02-13 15:13:202012新春纪念徽章
日期:2012-02-13 15:13:20版主4段
日期:2012-05-15 15:24:11
21#
发表于 2010-5-17 09:43 | 只看该作者
很强劲!

使用道具 举报

回复
论坛徽章:
5
2009日食纪念
日期:2009-07-22 09:30:002010新春纪念徽章
日期:2010-03-01 11:19:072012新春纪念徽章
日期:2012-01-04 11:54:462013年新春福章
日期:2013-02-25 14:51:24优秀写手
日期:2013-12-25 06:00:13
22#
发表于 2010-5-17 10:05 | 只看该作者
关注一下。

使用道具 举报

回复
论坛徽章:
520
奥运会纪念徽章:垒球
日期:2008-09-15 01:28:12生肖徽章2007版:鸡
日期:2008-11-17 23:40:58生肖徽章2007版:马
日期:2008-11-18 05:09:48数据库板块每日发贴之星
日期:2008-11-29 01:01:02数据库板块每日发贴之星
日期:2008-12-05 01:01:03生肖徽章2007版:虎
日期:2008-12-10 07:47:462009新春纪念徽章
日期:2009-01-04 14:52:28数据库板块每日发贴之星
日期:2009-02-08 01:01:03生肖徽章2007版:蛇
日期:2009-03-09 22:18:532009日食纪念
日期:2009-07-22 09:30:00
23#
 楼主| 发表于 2010-5-19 00:53 | 只看该作者
下一个例子是我以前发过的首届NoCOUG国际SQL挑战赛:
http://www.itpub.net/thread-1216120-1-1.html

题目:
有一张表记录了骰子的六个面的面值极其出现概率:
CREATE TABLE die (
  face_id NUMBER(2) NOT NULL CHECK (face_id > 0) PRIMARY KEY,
  face_value NUMBER(2) NOT NULL CHECK (face_value > 0),
  probability NUMBER(10,10) NOT NULL CHECK (probability >=0 AND probability <= 1)
);

INSERT INTO die VALUES (1, 1, 1/6 + 1/12);
INSERT INTO die VALUES (2, 3, 1/6 + 1/12);
INSERT INTO die VALUES (3, 4, 1/6 + 1/12);
INSERT INTO die VALUES (4, 5, 1/6 - 1/12);
INSERT INTO die VALUES (5, 6, 1/6 - 1/12);
INSERT INTO die VALUES (6, 8, 1/6 - 1/12);

注意和传统的1-6骰子不同,概率也不是平均出现的。
现在要用SQL求出在掷N次后各个总面值及其概率。

投掷N次的总面值及其概率可以用这个表的N次笛卡尔积求出来,比如N=3:

SELECT d1.face_value+d2.face_value+d3.face_value
      ,SUM(d1.probability*d2.probability*d3.probability)
  FROM die d1,die d2,die d3
GROUP BY d1.face_value+d2.face_value+d3.face_value;

如果要用一个通用SQL满足不同的N,办法是用CONNECT BY来代替笛卡尔积:

VAR N NUMBER;
EXEC :N :=3;

WITH d AS (
SELECT ROWNUM AS id
      ,SYS_CONNECT_BY_PATH(face_value,'~')||'~' AS v
      ,SYS_CONNECT_BY_PATH(probability,'~')||'~' AS p
  FROM die
WHERE LEVEL=:N
CONNECT BY LEVEL<=:N
)
,d2 AS (
SELECT id
      ,SUM(SUBSTR(v,INSTR(V,'~',1,l)+1,INSTR(V,'~',1,l+1)-INSTR(V,'~',1,l)-1)) AS v
      ,EXP(SUM(LN(SUBSTR(p,INSTR(p,'~',1,l)+1,INSTR(p,'~',1,l+1)-INSTR(p,'~',1,l)-1)))) AS p
  FROM d
      ,(SELECT LEVEL as l FROM DUAL CONNECT BY LEVEL<=:N)
GROUP BY id
      )
SELECT v
      ,SUM(p) AS p
  FROM d2
GROUP BY v
ORDER BY 1;

可以看到,依然是用SYS_CONNECT_BY_PATH来拼接数据,拆开后用SUM求和,用对数反对数求积。

用新的WITH写法是这样的:

WITH t(v,p,lvl) AS (
   SELECT face_value  v
         ,probability p
         ,1
     FROM die
   UNION ALL
   SELECT d.face_value + t.v
         ,d.probability * t.p
         ,t.lvl+1
     FROM die d,t
    WHERE t.lvl<:N
    )
SELECT v
      ,SUM(p)
  FROM t
WHERE lvl = :N
GROUP BY v;

这里大大简化了求和求积的运算。但是美中不足的是产生了很多中间记录,每一层的数据是上一层的数量*6, 所以当N=3时在t里面总共会有:
6^1+6^2+6^3 = 258行,当N增加的时候效率急剧下降。
虽然中间行数很多,但是其不同的面值数量并不多。上述例子中最后只有20行。如果ORACLE允许我们这么做:

WITH t(v,p,lvl) AS (
   SELECT face_value  v
         ,probability p
         ,1
     FROM die
   UNION ALL
   SELECT d.face_value + t.v
         ,SUM(d.probability * t.p)
         ,MAX(t.lvl)+1
     FROM die d,t
    WHERE t.lvl<:N
    GROUP BY d.face_value + t.v
    )
SELECT *
  FROM t
WHERE lvl = :N;
即在每次递归的时候做一个聚合,那么中间结果将会大大减少,下一轮递归的效率也会高得多。
遗憾的是会报这么一个错:
  FROM t
       *
ERROR at line 15:
ORA-32486: unsupported operation in recursive branch of recursive WITH clause

文档中说,递归成员不可以包含以下元素:
1. DISTINCT 关键字或 GROUP BY 子句
2. model 子句
3. 聚合函数。而分析函数允许在SELECT中使用。
4. 引用了当前query_name的子查询
5. 把当前query_name作为右边的外连接。

1和3其实是一回事。这个限制使得我们无法在递归过程中通过聚合减少中间结果。3说可以用分析函数,但实际上我试不出来:
WITH t(n) AS (
  SELECT 1 FROM DUAL
  UNION ALL
  SELECT 1+MAX(n) OVER() FROM t
   WHERE n<3
)
SELECT * FROM t;

SELECT * FROM t
              *
ERROR at line 7:
ORA-32486: unsupported operation in recursive branch of recursive WITH clause

限制4造成的后果是:如果我已经找到了一个答案,无法告诉ORACLE停止遍历其他的枝条。
我期望的写法是:
WITH t(...) AS (
   SELECT ...
   UNION ALL
   SELECT ...
     FROM t,...
    WHERE ... AND NOT EXISTS (SELECT 1 FROM t WHERE 找到答案的条件) --- 这个子查询不允许出现t

哪一天这些限制能够去除,这个WITH将会好玩很多。

限制5说不可以用于外连接,但是我试了一下并不报错:
WITH t(v,p,lvl) AS (
   SELECT face_value  v
         ,probability p
         ,1
     FROM die
   UNION ALL
   SELECT d.face_value + t.v
         ,d.probability * t.p
         ,t.lvl+1
     FROM die d,t
    WHERE d.face_value = t.v(+)
          AND t.lvl<:N
    )
SELECT COUNT(*)
  FROM t;

反过来连也不报错:
WITH t(v,p,lvl) AS (
   SELECT face_value  v
         ,probability p
         ,1
     FROM die
   UNION ALL
   SELECT d.face_value + t.v
         ,d.probability * t.p
         ,t.lvl+1
     FROM die d,t
    WHERE d.face_value(+) = t.v
          AND t.lvl<:N
    )
SELECT COUNT(*)
  FROM t;

使用道具 举报

回复
论坛徽章:
520
奥运会纪念徽章:垒球
日期:2008-09-15 01:28:12生肖徽章2007版:鸡
日期:2008-11-17 23:40:58生肖徽章2007版:马
日期:2008-11-18 05:09:48数据库板块每日发贴之星
日期:2008-11-29 01:01:02数据库板块每日发贴之星
日期:2008-12-05 01:01:03生肖徽章2007版:虎
日期:2008-12-10 07:47:462009新春纪念徽章
日期:2009-01-04 14:52:28数据库板块每日发贴之星
日期:2009-02-08 01:01:03生肖徽章2007版:蛇
日期:2009-03-09 22:18:532009日食纪念
日期:2009-07-22 09:30:00
24#
 楼主| 发表于 2010-5-19 03:28 | 只看该作者
进制转换:
要转换的数字放在字符串STR中;源进制为FROM_BASE, 目标进制TO_BASE.
假设超过10的数字用A-Z来表示,也就是最大不超过36进制。

VAR STR VARCHAR2(10);
EXEC :STR := '1234WXYZ';

VAR FROM_BASE NUMBER;
EXEC :FROM_BASE :=36;

VAR TO_BASE NUMBER;
EXEC :TO_BASE :=10;

用CONNECT BY的做法:
WITH n1 AS (
  SELECT SUM((CASE WHEN c BETWEEN '0' AND '9' THEN TO_NUMBER(c) --- 求出每一位的值
                   ELSE ASCII(c)-ASCII('A')+10
              END)*POWER(:FROM_BASE,len-rn)    ----- 乘上这一位对应的权值
             ) AS the_num     ---- 最后得出这个需要转换的字符串的实际值
    FROM (SELECT SUBSTR(:STR,ROWNUM,1) c,LENGTH(:STR) len,ROWNUM rn  ----- 把每一位拆开
            FROM DUAL CONNECT BY ROWNUM<=LENGTH(:STR)
          )
)
,n2 AS (
  SELECT (CASE WHEN n <10 THEN TO_CHAR(n) ELSE CHR(ASCII('A')+n-10) END) AS digi   ---- 这一位值该用哪个字符来表示
        ,rn
    FROM (SELECT MOD(TRUNC(the_num/POWER(:TO_BASE,ROWNUM -1)),:TO_BASE) n   --- 求出该数字在转换后的进制中的每一位的值
                ,ROWNUM rn
            FROM n1
          CONNECT BY ROWNUM <= TRUNC(LOG(:TO_BASE,the_num))+1
         )
)
SELECT REPLACE(SYS_CONNECT_BY_PATH(digi,'*'),'*')   ---- 把转换后的每一位字符串起来
  FROM n2
WHERE rn=1
START WITH rn = (SELECT MAX(rn) FROM n2)
CONNECT BY rn = PRIOR rn -1;

REPLACE(SYS_CONNECT_BY_PATH(DIGI,'*'),'*')-
-------------------------------------------
82907382779

递归WITH的做法:
WITH n1(the_num,lvl,c) AS ( ---- 求值:循环对每一位求值,结果不断累积
  SELECT 0,1,SUBSTR(:STR,1,1) FROM DUAL
  UNION ALL
  SELECT the_num*:FROM_BASE+(CASE WHEN c BETWEEN '0' AND '9' THEN TO_NUMBER(c) --- 求出每一位的值
                                  ELSE ASCII(c)-ASCII('A')+10
                             END)
        ,lvl+1
        ,SUBSTR(:STR,lvl+1,1)
    FROM n1
   WHERE lvl<=LENGTH(:STR)
  )
,n2(the_num,n,the_str) AS ( --- 转换:循环对该数字取模,转换为字符串然后拼接
  SELECT TRUNC(the_num/:TO_BASE),MOD(the_num,:TO_BASE),'' FROM n1 WHERE lvl=LENGTH(:STR)+1
  UNION ALL
  SELECT (CASE WHEN the_num=0 THEN -1   --- 用负数来作为结束标记
               ELSE TRUNC(the_num/:TO_BASE)
          END)
        ,MOD(the_num,:TO_BASE)
        ,(CASE WHEN n <10 THEN TO_CHAR(n) ELSE CHR(ASCII('A')+n-10) END)||the_str
    FROM n2
   WHERE the_num>=0
  )
SELECT the_str FROM n2 WHERE the_num<0
;

THE_STR
------------------
82907382779

使用道具 举报

回复
论坛徽章:
3
ITPUB官方微博粉丝徽章
日期:2011-06-29 09:48:25ITPUB十周年纪念徽章
日期:2011-11-01 16:21:15秀才
日期:2016-02-18 09:23:46
25#
发表于 2010-5-20 14:24 | 只看该作者
顶一下。其实我觉得这种境界是要在兴趣支撑的前提下才能达到。比如很多玩魔兽的玩家游戏也能玩到很精。如果都能拿来做学问,高手也不少。newkid切记山外有山,虽然现在你的水平是公认的高,哈哈。

使用道具 举报

回复
论坛徽章:
211
国际米兰
日期:2010-01-11 10:26:28ITPUB评论家
日期:2007-11-04 01:35:51季节之章:春
日期:2011-04-03 16:30:30热刺
日期:2009-09-21 10:54:48天枰座
日期:2015-11-05 16:32:03月度论坛发贴之星
日期:2010-05-01 02:15:42生肖徽章:狗
日期:2006-10-01 00:29:23BLOG每周发帖之星
日期:2009-08-30 01:35:31BLOG每日发帖之星
日期:2009-08-28 01:01:02妮可·罗宾
日期:2016-10-19 10:45:04
26#
发表于 2010-8-4 00:11 | 只看该作者
mark

使用道具 举报

回复
论坛徽章:
1088
金色在线徽章
日期:2007-04-25 04:02:08金色在线徽章
日期:2007-06-29 04:02:43金色在线徽章
日期:2007-03-11 04:02:02在线时间
日期:2007-04-11 04:01:02在线时间
日期:2007-04-12 04:01:02在线时间
日期:2007-03-07 04:01:022008版在线时间
日期:2010-05-01 00:01:152008版在线时间
日期:2011-05-01 00:01:342008版在线时间
日期:2008-06-03 11:59:43ITPUB年度最佳技术原创精华奖
日期:2013-03-22 13:18:30
27#
发表于 2010-8-4 00:14 | 只看该作者
原帖由 xsaier 于 2010-5-20 14:24 发表
顶一下。其实我觉得这种境界是要在兴趣支撑的前提下才能达到。比如很多玩魔兽的玩家游戏也能玩到很精。如果都能拿来做学问,高手也不少。newkid切记山外有山,虽然现在你的水平是公认的高,哈哈。

newkid大哥的谦虚是我辈学习的榜样,他没有否认别的大山啊,而且他还宣传别的大山呢,你没有看到置顶的那帖子?发现了高人...

使用道具 举报

回复
论坛徽章:
58
生肖徽章2007版:马
日期:2009-11-06 23:12:33授权会员
日期:2013-01-10 14:38:592013年新春福章
日期:2013-02-25 14:51:24马自达
日期:2013-08-07 10:54:45红旗
日期:2013-08-09 13:48:48劳斯莱斯
日期:2013-09-12 15:56:37萤石
日期:2013-10-31 08:44:19优秀写手
日期:2013-12-18 09:29:13Jeep
日期:2014-01-14 10:53:432014年新春福章
日期:2014-02-18 16:43:09
28#
发表于 2011-4-22 11:54 | 只看该作者
好帖!!学习。

使用道具 举报

回复
论坛徽章:
3
ITPUB十周年纪念徽章
日期:2011-11-01 16:26:292012新春纪念徽章
日期:2012-01-04 11:57:56奥运会纪念徽章:跳水
日期:2012-06-13 13:46:02
29#
发表于 2012-2-20 10:28 | 只看该作者
都是大牛

使用道具 举报

回复
论坛徽章:
3
ITPUB十周年纪念徽章
日期:2011-11-01 16:26:292012新春纪念徽章
日期:2012-01-04 11:57:56奥运会纪念徽章:跳水
日期:2012-06-13 13:46:02
30#
发表于 2012-2-20 10:54 | 只看该作者
高人啊,大牛多了去了,值得学习的很多

使用道具 举报

回复

您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

TOP技术积分榜 社区积分榜 徽章 团队 统计 知识索引树 积分竞拍 文本模式 帮助
  ITPUB首页 | ITPUB论坛 | 数据库技术 | 企业信息化 | 开发技术 | 微软技术 | 软件工程与项目管理 | IBM技术园地 | 行业纵向讨论 | IT招聘 | IT文档
  ChinaUnix | ChinaUnix博客 | ChinaUnix论坛
CopyRight 1999-2011 itpub.net All Right Reserved. 北京盛拓优讯信息技术有限公司版权所有 联系我们 未成年人举报专区 
京ICP备16024965号-8  北京市公安局海淀分局网监中心备案编号:11010802021510 广播电视节目制作经营许可证:编号(京)字第1149号
  
快速回复 返回顶部 返回列表