首页 > Linux操作系统 > Linux操作系统 > ITPUB SQL大赛第四期
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/ ,如需转载,请注明出处,否则将追究法律责任。