在上篇博文中,我分析了手機上網流量分佈極度失衡的問題,並建議用“中位數”和“TOP1%貢獻率”等指標來避免“被平均”問題。
但是,這些指標並非直接就能得到,需要一些特別的技術實現,本篇主要闡述我是如何在Oracle中用PL/SQL來實現該項分析功能。
我從2005年開始應用這些指標進行分析和稽覈,由於當時並無統計中位數的MEDIAN函數(10g版本之後纔有),且TOP1%貢獻率的統計並無現成函數,所以就寫了一個專門的統計程序來完成該項功能,一直沿用至今。
我的實現思路是:該通用統計包在一個程序裏同時實現中位數、平均值、最大值、TOP1%貢獻率、TOP5%貢獻率等統計功能。基礎數據基於我自己整理的用戶寬表DW_ALL,以NTILE函數爲核心,先將用戶按指標一百等分,統計每類的上限和下限;然後在此中間結果基礎上計算所需的各項指標。
另外,爲了便於增加新的統計指標,所需統計的指標在參數表中定義,由程序自動解析。由於這個統計程序耗時較長,所以每個指標的統計結果先放入臨時表,然後再一起匯入正式的結果表,以實現統計過程的可並行性。
以下是關於這個程序的幾點說明:
1、用NTILE函數進行用戶分羣
該函數的主要功能是將用戶羣分成同等大小的幾類,比如“NTILE(100) OVER(ORDEER BY 出賬收入 DESC)”的結果就是將用戶按出賬收入從高到低分成100組,每個用戶所處的組的組號。
在對用戶分組的基礎上,統計好每個組的上限、下限、樣本個數和累計值,就能爲後續的指標計算提供基礎。
2、全省和分地區的指標須單獨統計
分析時,我們既需要分析全省的情況,也要比較各地區的情況。但是,全省的指標並不能從地區的指標中獲得,必須獨立統計。
3、同時完成多項指標的統計
目前我同時統計的指標有:個數,總和,最大值,均值,中位數,TOP1佔比,TOP5佔比,TOP10佔比,TOP20佔比,TOP30佔比,TOP40佔比,TOP50佔比,TOP60佔比,TOP70佔比,TOP80佔比,TOP90佔比,TOP1最小值,TOP5最小值,TOP10最小值,TOP20最小值,TOP30最小值,TOP40最小值,TOP50最小值,TOP60最小值,TOP70最小值,TOP80最小值,TOP90最小值。
4、通過參數化實現通用化
需要統計的指標在ETL_CFG_RATIO中定義,這個指標名稱可以隨便定義,關鍵是公式裏用到的字段必須跟DW_ALL寬表中的字段一致。
比如,指標名稱可以是“手機上網流量”,而公式卻是“GPRS忙時流量+GPRS閒時流量”。
參數表中的用戶範圍用來區分是統計全省還是地區,其值分“全省”和“地區”兩類。
該參數表結構如下:
CREATE TABLE ETL_CFG_RATIO(
序號 CHAR(2) NOT NULL,
指標名稱 VARCHAR2(20) NOT NULL,
用戶範圍 VARCHAR2(4) NOT NULL,
指標公式 VARCHAR2(100) ,
備註 VARCHAR2(20) ,
修改時間 DATE
)
5、通過臨時表實現並行彙總
由於該類彙總耗時較長,若有十多個指標的彙總,串行執行可能需要耗費40多個小時。爲了實現併發及減少程序衝突,我這邊將彙總結果先置於臨時表。
臨時表的結構完全相同,表名從TEMP_RATIO_INDEX01到30,當然也可以根據需要進行擴展。參數表中的序號與表名的末兩位相對應,比如02序號中指標的TEMP_RATIO_INDEX02。
該類表結構如下:
CREATE TABLE TEMP_RATIO_INDEX01(
月份 CHAR(6) NOT NULL,
指標名稱 VARCHAR2(40) NOT NULL,
用戶範圍 VARCHAR2(10) NOT NULL,
地區名稱 VARCHAR2(6) NOT NULL,
品牌名稱 VARCHAR2(16) NOT NULL,
分組序號 NUMBER(4,0) NOT NULL,
個數 NUMBER(10,0) NOT NULL,
總和 NUMBER(18,2) NOT NULL,
最小值 NUMBER(12,2) NOT NULL,
最大值 NUMBER(12,2) NOT NULL,
備註 VARCHAR2(20)
)
6、最終結果彙總到兩張表中
當分指標的彙總都統計完畢並存儲在臨時表中以後,爲了分析和使用方便,最後我將這些彙總都整到兩張彙總表中。其中,STAT_ALL_USER_RATIO1是對臨時表的簡單合併,而STAT_ALL_USER_RATIO2表則存儲計算好的“中位數”和“TOP1%貢獻率”等指標。
這兩種表結構如下:
CREATE TABLE STAT_ALL_USER_RATIO1(
月份 CHAR(6) NOT NULL,
指標名稱 VARCHAR2(20) NOT NULL,
用戶範圍 VARCHAR2(10) NOT NULL,
地區名稱 VARCHAR2(4) NOT NULL,
品牌名稱 VARCHAR2(12) NOT NULL,
分組序號 NUMBER(4,0) NOT NULL,
個數 NUMBER(10,0) NOT NULL,
總和 NUMBER(18,2) NOT NULL,
最小值 NUMBER(12,2) NOT NULL,
最大值 NUMBER(12,2) NOT NULL,
備註 VARCHAR2(20)
)
CREATE TABLE STAT_ALL_USER_RATIO2(
月份 CHAR(6) NOT NULL,
指標名稱 VARCHAR2(20) NOT NULL,
用戶範圍 VARCHAR2(4) NOT NULL,
地區名稱 VARCHAR2(4) NOT NULL,
個數 NUMBER(10,0) NOT NULL,
總和 NUMBER(18,2) NOT NULL,
最大值 NUMBER(12,2) NOT NULL,
均值 NUMBER(12,2) NOT NULL,
中位數 NUMBER(12,2) NOT NULL,
TOP1佔比 NUMBER(6,4) NOT NULL,
TOP5佔比 NUMBER(6,4) NOT NULL,
TOP10佔比 NUMBER(6,4) NOT NULL,
TOP20佔比 NUMBER(6,4) NOT NULL,
TOP30佔比 NUMBER(6,4) NOT NULL,
TOP40佔比 NUMBER(6,4) NOT NULL,
TOP50佔比 NUMBER(6,4) NOT NULL,
TOP60佔比 NUMBER(6,4) NOT NULL,
TOP70佔比 NUMBER(6,4) NOT NULL,
TOP80佔比 NUMBER(6,4) NOT NULL,
TOP90佔比 NUMBER(6,4) NOT NULL,
TOP1最小值 NUMBER(12,2) NOT NULL,
TOP5最小值 NUMBER(12,2) NOT NULL,
TOP10最小值 NUMBER(12,2) NOT NULL,
TOP20最小值 NUMBER(12,2) NOT NULL,
TOP30最小值 NUMBER(12,2) NOT NULL,
TOP40最小值 NUMBER(12,2) NOT NULL,
TOP50最小值 NUMBER(12,2) NOT NULL,
TOP60最小值 NUMBER(12,2) NOT NULL,
TOP70最小值 NUMBER(12,2) NOT NULL,
TOP80最小值 NUMBER(12,2) NOT NULL,
TOP90最小值 NUMBER(12,2) NOT NULL,
備註 VARCHAR2(20)
)
=====================================================================
相關代碼如下,並不複雜,熟悉PL/SQL開發的人應能讀懂
====================================================================
/* =============================================================== *
GET_RATIO_DTL: 按某個指標統計用戶的100等分分佈
* =============================================================== */
PROCEDURE GET_RATIO_DTL(p_月份 CHAR,p_目的表 CHAR,p_指標名稱 CHAR,p_指標公式 CHAR,p_用戶範圍 CHAR DEFAULT '*') IS
v_stat VARCHAR2(4000);
v_SQL VARCHAR2(4000);
BEGIN
v_stat :='
INSERT INTO #目的表
SELECT ''#月份'',''#指標名稱'',''#用戶範圍'',地區代碼,DECODE(品牌類型,1,1,4,4,6),分組序號,
count(1),SUM(指標值),min(指標值),max(指標值),null
FROM
(SELECT /*+ PARALLEL(T,5) */
NTILE(100) OVER (ORDER BY #指標公式 DESC) 分組序號,
地區代碼,品牌類型, #指標公式 指標值
FROM DW_ALL@DW T
WHERE 月份 =''#月份'' #條件 AND #指標公式>0
)
GROUP BY 分組序號,地區代碼,DECODE(品牌類型,1,1,4,4,6)';
v_stat := REPLACE(v_stat,'#月份',p_月份);
v_stat := REPLACE(v_stat,'#目的表',p_目的表);
v_stat := REPLACE(v_stat,'#指標名稱',p_指標名稱);
v_stat := REPLACE(v_stat,'#指標公式',NVL(p_指標公式,p_指標名稱));
IF p_用戶範圍 IN('全省','*') THEN
EXECUTE IMMEDIATE REPLACE(REPLACE(v_stat,'#條件',''),'#用戶範圍','全省');
END IF;
IF p_用戶範圍 IN('地區','*') THEN
v_SQL := REPLACE(v_stat,'#用戶範圍','地區');
FOR v_地區代碼 IN 570..580 LOOP
EXECUTE IMMEDIATE REPLACE(v_SQL,'#條件',' AND 地區代碼='''||v_地區代碼||''' ');
END LOOP;
END IF;
END GET_RATIO_DTL;
/* =============================================================== *
GET_RATIO100:統計用戶100等分分佈
* =============================================================== */
PROCEDURE GET_RATIO100(p_月份 CHAR,p_序號 CHAR) IS
v_指標名稱 ETL_CFG_RATIO.指標名稱%TYPE;
v_用戶範圍 ETL_CFG_RATIO.用戶範圍%TYPE;
v_指標公式 ETL_CFG_RATIO.指標公式%TYPE;
BEGIN
SELECT 指標名稱,用戶範圍,NVL(指標公式,指標名稱) 指標公式
INTO v_指標名稱,v_用戶範圍,v_指標公式
FROM ETL_CFG_RATIO
WHERE 序號 = p_序號;
GET_RATIO_DTL(p_月份,'TEMP_RATIO_INDEX'||p_序號,v_指標名稱,v_指標公式,v_用戶範圍);
COMMIT;
END GET_RATIO100;
/* =============================================================== *
GET_RATIO1:合併用戶分佈彙總
* =============================================================== */
PROCEDURE GET_RATIO1(p_月份 CHAR) IS
v_SQL VARCHAR2(2000);
BEGIN
-- 檢查數據源是否全
FOR rec IN (SELECT * FROM ETL_CFG_RATIO ORDER BY 序號 DESC) LOOP
IF ETL_TOOL.IS_EMPTY('TEMP_RATIO_INDEX'||rec.序號,p_月份) THEN
RETURN;
END IF;
END LOOP;
-- 合併彙總
v_SQL := 'INSERT INTO STAT_ALL_USER_RATIO1 SELECT * FROM TEMP_RATIO_INDEX#序號';
FOR rec IN (SELECT * FROM ETL_CFG_RATIO ORDER BY 序號 DESC) LOOP
EXECUTE IMMEDIATE REPLACE(v_SQL,'#序號',rec.序號);
END LOOP;
COMMIT;
END GET_RATIO1;
/* =============================================================== *
GET_RATIO2:統計用戶分佈關鍵指標
* =============================================================== */
PROCEDURE GET_RATIO2(p_月份 CHAR) IS
BEGIN
DELETE STAT_ALL_USER_RATIO2 WHERE 月份 = p_月份;
INSERT INTO STAT_ALL_USER_RATIO2
SELECT p_月份,指標名稱,用戶範圍,DECODE(用戶範圍,'地區',地區名稱,'全省') 地區名稱,
SUM(個數),SUM(總和),MAX(最大值),ROUND(SUM(總和)/SUM(個數),2) 均值,
ROUND(SUM(DECODE(分組序號,50,總和,0))/SUM(DECODE(分組序號,50,個數,0)),2) 中位數,
SUM(DECODE(分組序號,1,總和,0))/SUM(總和) TOP1佔比,
SUM(DECODE(SIGN(6-分組序號),1,總和,0))/SUM(總和) TOP5佔比,
SUM(DECODE(SIGN(11-分組序號),1,總和,0))/SUM(總和) TOP10佔比,
SUM(DECODE(SIGN(21-分組序號),1,總和,0))/SUM(總和) TOP20佔比,
SUM(DECODE(SIGN(31-分組序號),1,總和,0))/SUM(總和) TOP30佔比,
SUM(DECODE(SIGN(41-分組序號),1,總和,0))/SUM(總和) TOP40佔比,
SUM(DECODE(SIGN(51-分組序號),1,總和,0))/SUM(總和) TOP50佔比,
SUM(DECODE(SIGN(61-分組序號),1,總和,0))/SUM(總和) TOP60佔比,
SUM(DECODE(SIGN(71-分組序號),1,總和,0))/SUM(總和) TOP70佔比,
SUM(DECODE(SIGN(81-分組序號),1,總和,0))/SUM(總和) TOP80佔比,
SUM(DECODE(SIGN(91-分組序號),1,總和,0))/SUM(總和) TOP90佔比,
MIN(DECODE(分組序號,1,最小值,NULL)) TOP1最小值,
MIN(DECODE(分組序號,6,最小值,NULL)) TOP5最小值,
MIN(DECODE(分組序號,11,最小值,NULL)) TOP10最小值,
MIN(DECODE(分組序號,21,最小值,NULL)) TOP20最小值,
MIN(DECODE(分組序號,31,最小值,NULL)) TOP30最小值,
MIN(DECODE(分組序號,41,最小值,NULL)) TOP40最小值,
MIN(DECODE(分組序號,51,最小值,NULL)) TOP50最小值,
MIN(DECODE(分組序號,61,最小值,NULL)) TOP60最小值,
MIN(DECODE(分組序號,71,最小值,NULL)) TOP70最小值,
MIN(DECODE(分組序號,81,最小值,NULL)) TOP80最小值,
MIN(DECODE(分組序號,91,最小值,NULL)) TOP90最小值,
NULL
FROM STAT_ALL_USER_RATIO1
WHERE 月份 = p_月份
GROUP BY 指標名稱,用戶範圍,DECODE(用戶範圍,'地區',地區名稱,'全省')
ORDER BY 指標名稱,用戶範圍,地區名稱;
COMMIT;
END GET_RATIO2;