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维度表获取名称,并进行汇总值的计算

This entry was posted in ORACLE and tagged , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *