ITPub博客

首页 > Linux操作系统 > Linux操作系统 > ITPUB SQL大赛第四期

ITPUB SQL大赛第四期

原创 Linux操作系统 作者:yangtingkun 时间:2012-09-27 23:46:46 0 删除 编辑

SQL大赛第四期解法的最终答案。

第四期题目参考:http://www.itpub.net/thread-1411495-1-1.html

版主newkid点评参考:http://www.itpub.net/thread-1417576-1-1.html

 

 

最近整理BLOG文章时发现,这篇文章当时没有贴出来。由于当时发现最后一题出现了错误,且手头的事情比较多,本打算搞清楚之后再把结果贴出来,没想到后来忘记了。虽然已经过去了一年了,不过信守自己的承诺,还是把最终结果贴出来。

我的代码为:

WITH DAY AS
(
     SELECT TO_DATE(:P_START_DATE, 'YYYYMMDD') + ROWNUM - 1 DAY
     FROM DUAL
     CONNECT BY ROWNUM <= TO_DATE(:P_END_DATE, 'YYYYMMDD') - TO_DATE(:P_START_DATE, 'YYYYMMDD') + 1
),
COMPANY_INFO AS
(
     SELECT A.USER_ID, SERVICE_ID, B.COMPANY_ID,
         GREATEST(START_DATE, TO_DATE(:P_START_DATE, 'YYYYMMDD')) START_DATE,
         LEAST(END_DATE, TO_DATE(:P_END_DATE, 'YYYYMMDD')) END_DATE
     FROM SERVICE_USAGE A, SERVICE_USERS B
     WHERE A.USER_ID = B.USER_ID
     AND START_DATE <= TO_DATE(:P_END_DATE, 'YYYYMMDD')
     AND END_DATE >= TO_DATE(:P_START_DATE, 'YYYYMMDD')
     AND TO_DATE(:P_END_DATE, 'YYYYMMDD') - TO_DATE(:P_START_DATE, 'YYYYMMDD') >= 60
),
COMPANY_DAY1 AS
(
     SELECT SERVICE_ID, COMPANY_ID, DAY, COUNT(DISTINCT USER_ID) CN
     FROM DAY D, COMPANY_INFO C
     WHERE D.DAY BETWEEN C.START_DATE AND C.END_DATE
     GROUP BY SERVICE_ID, COMPANY_ID, DAY
),
COMPANY_DAY2 AS
(
     SELECT SERVICE_ID, COMPANY_ID, DAY, COUNT(DISTINCT A.USER_ID) CN
     FROM
     (
         SELECT USER_ID, SERVICE_ID, START_DATE, END_DATE, DAY
         FROM SERVICE_USAGE
         WHERE START_DATE <= TO_DATE(:P_END_DATE, 'YYYYMMDD')
         AND END_DATE >= TO_DATE(:P_START_DATE, 'YYYYMMDD')
         AND TO_DATE(:P_END_DATE, 'YYYYMMDD') - TO_DATE(:P_START_DATE, 'YYYYMMDD') < 60
         MODEL UNIQUE SINGLE REFERENCE
         PARTITION BY (USER_ID, SERVICE_ID,
              GREATEST(START_DATE, TO_DATE(:P_START_DATE, 'YYYYMMDD')) START_DATE,
              LEAST(END_DATE, TO_DATE(:P_END_DATE, 'YYYYMMDD')) END_DATE)
         DIMENSION BY (CAST(NULL AS DATE) DAY)
         MEASURES (0 V)
         RULES
         (
         V[
         FOR DAY
              FROM TO_DATE(:P_START_DATE, 'YYYYMMDD')
              TO TO_DATE(:P_END_DATE, 'YYYYMMDD')
              INCREMENT 1
         ] = 1
         )
     ) A, SERVICE_USERS B
     WHERE A.USER_ID = B.USER_ID
     AND DAY BETWEEN START_DATE AND END_DATE
     GROUP BY SERVICE_ID, COMPANY_ID, DAY
),
COMPANY_DAY AS
(
     SELECT SERVICE_ID, COMPANY_ID, DAY, CN
     FROM COMPANY_DAY1
     UNION ALL
     SELECT SERVICE_ID, COMPANY_ID, DAY, CN
     FROM COMPANY_DAY2
)
SELECT R.COMPANY_ID, COMPANY_NAME, R.SERVICE_ID, SERVICE_NAME, FEE1, FEE2,
     FEE3, FEE4, NVL(FEE1, 0) + NVL(FEE2, 0) + NVL(FEE3, 0) + NVL(FEE4, 0) TOTAL_FEE
FROM
(
     SELECT CD.SERVICE_ID, CD.COMPANY_ID, CATEGORY_ID, RATE*CN
     FROM COMPANY_DAY CD, SERVICE_RATES SR
     WHERE CD.SERVICE_ID = SR.SERVICE_ID
     AND CD.COMPANY_ID = SR.COMPANY_ID
     AND CN BETWEEN USER_COUNT_MIN AND USER_COUNT_MAX
)
PIVOT
(
     SUM(RATE) FOR CATEGORY_ID IN (1 FEE1, 2 FEE2, 3 FEE3, 4 FEE4)
) R, SERVICES S, COMPANIES C
WHERE R.SERVICE_ID = S.SERVICE_ID
AND R.COMPANY_ID = C.COMPANY_ID
ORDER BY 1, 3;

这是改正之后的代码,与之前错误的代码相比,在SELECT CD.SERVICE_ID, CD.COMPANY_ID, CATEGORY_ID, RATE这一行,将RATE改为了RATE*CN。题目里面其实对这个问题进行了说明,不过后续在处理SQL的过程中漏掉了,于是导致了SQL的错误。

下面对SQL的实现进行简单的说明:

整个SQL由两大部分组成,第一个部分通过构造日期的方式获取每天每个SERVICE_ID和COMPANY_ID有多少不同的用户使用。第一部分的功能都通过WITH子查询方式实现,第二部分主要是利用11G的PIVOT实现列到行的转换。

出于性能方面的考虑,第一部分的逻辑通过等价的两种方式实现:分别是利用CONNECT BY方式构造日期和利用MODEL语句来构造日期,这两种方式分别对应COMPANY_DAY1和COMPANY_DAY2两个WITH语句。

在我的测试环境中,当:P_START_DATE与:P_END_DATE的时间间隔小于2个月的时候,第二种MODEL方式,也就是COMPANY_DAY2的效率更高,而当:P_START_DATE与:P_END_DATE的时间间隔较长,比如大于1年,则第一种方式,也就是COMPANY_DAY1的效率要远高于第二种方式。由于第一种方式线性更好,因此在不确定数据分布的情况下,将二者的阈值设置为60天,在两个WITH中加上了互斥的常量条件,分别为日期间隔大于等于60和间隔小于60。通过UNION ALL将两个WITH查询合并,为第二部分构造了统一的接口COMPANY_DAY,使得SQL根据输入间隔的不同自动选择更优的代码执行。

子查询COMPANY_DAY1的思路很简单:DAY子查询用于构造输入变量:P_START_DATE和:P_END_DATE之间所有的日期信息。关联SERVICE_USAGE和SERVICE_USERS表,获取COMPANY_ID和SERVICE_ID的对应关系,同时获取对应的时间范围,开始时间由SERVICE_USAGE中START_DATE和输入变量:P_START_DATE二者中大的值确定,结束时间由SERVICE_USAGE中END_DATE和输入变量:P_END_DATE中小的确定。

通过DAY子查询和COMPANY_INFO关联并聚集,使用COUNT(DISTINCT USER_ID)是为了避免同一个用户一天内多次使用该服务而被重复计算。同样用户配置使用服务的日期区间的重叠问题也会被DISTINCT消除掉。子查询COMPANY_DAY2的思路其实和COMPANY_DAY1没有区别,只不过利用了MODEL来自动生成日期维度,由于无论是利用FROM TO方式还是利用子查询方式,都无法带入当前记录的START_DATE和END_DATE。因此只能使用输入的变量:P_START_DATE和:P_END_DATE构造日期维度,随后构造的维度DAY和记录的START_DATE和END_DATE过滤数据,随后的思路和COMPANY_DAY1相同。

COMPANY_DAY主要是提供一个统一接口,关联COMPANY_DAY和SERVICE_RATES表,根据每天的人数和SERVICE_RATES的人数范围进行匹配,汇总对应的RATE并利用PIVOT进行行转列操作,最后关联SERVICES和COMPANIES维度表获取名称,并进行汇总值的计算

 

 

来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/4227/viewspace-751984/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论
暂无介绍

注册时间:2007-12-29

  • 博文量
    1964
  • 访问量
    11076238