事件編號 | 發生日期 | 發生機構 | 業務條線 | 損失事件類型 | 損失金額 |
LE0001 | 2010-5-11 | 分行1 | B4.商業銀行 | E1.內部欺詐 | 200 |
LE0002 | 2010-6-12 | 分行2 | B5.支付和結算 | E1.內部欺詐 | 400 |
LE0003 | 2010-7-14 | 分行3 | B4.商業銀行 | E2.外部欺詐 | 300 |
LE0004 | 2010-8-15 | 分行4 | B5.支付和結算 | E2.外部欺詐 | 600 |
LE0005 | 2010-9-16 | 分行5 | B4.商業銀行 | E1.內部欺詐 | 500 |
LE0006 | 2010-10-18 | 分行6 | B5.支付和結算 | E1.內部欺詐 | 200 |
… |
業務條線 | E1.內部欺詐 | E2.外部欺詐 | E3.就業制度和工作場所安全事件 | E4.客戶、產品和業務活動事件 | E5.實物資產的損壞 | E6.信息科技系統事件 | E7.執行、交割和流程管理事件 |
B1.公司金融 | 0 | 0 | 200 | 0 | 0 | 0 | 0 |
B2.交易和銷售 | 0 | 0 | 0 | 400 | 0 | 0 | 0 |
B3.零售銀行 | 0 | 0 | 0 | 0 | 300 | 0 | 0 |
B4.商業銀行 | 200 | 800 | 0 | 0 | 0 | 0 | 0 |
B5.支付和結算 | 600 | 600 | 0 | 0 | 0 | 0 | 0 |
B6.代理服務 | 0 | 0 | 0 | 0 | 0 | 600 | 0 |
B7.資產管理 | 0 | 0 | 0 | 0 | 0 | 0 | 500 |
B8.零售經紀 | 0 | 0 | 200 | 0 | 0 | 0 | 0 |
SELECT group_col ,nvl(sum(decode(to_char(column_col),'E1.內部欺詐',value_col)),0.0)"E1.內部欺詐" ,nvl(sum(decode(to_char(column_col),'E2.外部欺詐',value_col)),0.0)"E2.外部欺詐" ,nvl(sum(decode(to_char(column_col),'E3.就業制度和工作場所安全事件',value_col)),0.0)"E3.就業制度和工作場所安全事件" ,nvl(sum(decode(to_char(column_col),'E4.客戶、產品和業務活動事件',value_col)),0.0)"E4.客戶、產品和業務活動事件" ,nvl(sum(decode(to_char(column_col),'E5.實物資產的損壞',value_col)),0.0)"E5.實物資產的損壞" ,nvl(sum(decode(to_char(column_col),'E6.信息科技系統事件',value_col)),0.0)"E6.信息科技系統事件" ,nvl(sum(decode(to_char(column_col),'E7.執行、交割和流程管理事件',value_col)),0.0)"E7.執行、交割和流程管理事件" FROM Ts WHERE 1=1 GROUP BY group_col
也可以使用case when …then…end語句來代替上式中的decode函數。再此不另附編碼。 引種方案是最直接的解決方案。其優點是直觀。缺點是使用了太多的硬編碼,看起來非常的繁瑣,特別是當column_col的值較多或需要使用多個column_col對應的value_col進行計算時,重復性的硬編碼將會在代碼中多次出現。不論從可讀性、可維護性和擴展性上來講,這都不能稱為一個好的解決方案。 如果能有什么方法直接將column_col的值轉置成列名,然后在SQL直接按列名取對應的統計值就好了。那樣的話,生成報表的代碼就可以寫成類似這樣的代碼: SELECT group_col,E1.內部欺詐,E2.外部欺詐 FROM (…) 有沒有這樣的方法呢? 3.TABLE 能直接按列名訪問的,可以是TABLE,可以是VIEW,也可以是CURSOR。 先看TABLE。 使用Table,有兩種思路,一種是批量時使用Tt。由於不想因為增實體表而增大系統的復雜性,此種思路暫不考慮。 另一種思路是在SQL中生成臨時表,生成報表后,在SQL中將臨時表刪除。這種思路帶來的問題是如何解決臨時表的命名沖突或數據一致性問題。 對於臨時表的命名沖突,可以根據查詢的條件組成臨時表名,但這樣會造成生成很多臨時表的混亂局面,也無法避免兩個查詢條件一致時的並發問題。出於此種考慮,放棄掉使用臨時表的想法。 4.VIEW 再看VIEW。 View是基於一個表或多個表或視圖的邏輯表。建立View不會帶來物理存儲上的混亂。看起來是個不錯的選擇。但在解決此問題時,並不適合使用View,原因如下: 通用性問題:如果為這個需求建立了視圖,那么再出現一個類似的需求是否也要建立一個視圖?而這樣的需求可以說是無窮盡的。只對表1,就可以提出很多種類似於表2的查詢統計。如按機構、業務條線統計損失金額,再如按機構、損失事件類型統計損失金額等,可以按時間、機構、業務條線、損失事件類型、金額范圍等多個維護的兩兩組合。 命名問題:如果使用臨時生成的View,存在命名沖突的問題。 對於臨時生成View,在技術上似乎可行,示例見代碼2:代碼2:使用臨時視圖
WITH t0 AS( SELECT create_v('v_test') FROM dual --生成臨時視圖v_test ) ,t1 AS ( SELECT * FROM v_test --使用視圖v_test ) , t2 AS( SELECT delete_v('v_test') FROM dual --刪除視圖v_test ) SELECT * FROM t1, t2;
事件編號 | 發生日期 | 發生機構 | 業務條線 | 損失事件類型 | 損失金額 |
LE0001 | 2010-5-11 | 分行1 | B4.商業銀行 | E1.內部欺詐 | 200 |
LE0002 | 2010-6-12 | 分行2 | B5.支付和結算 | E1.內部欺詐 | 400 |
LE0003 | 2010-7-14 | 分行3 | B4.商業銀行 | E2.外部欺詐 | 300 |
LE0004 | 2010-8-15 | 分行4 | B5.支付和結算 | E2.外部欺詐 | 600 |
LE0005 | 2010-9-16 | 分行5 | B4.商業銀行 | E1.內部欺詐 | 500 |
LE0006 | 2010-10-18 | 分行6 | B5.支付和結算 | E1.內部欺詐 | 200 |
… |
業務條線 | E1.內部欺詐 | E2.外部欺詐 | E3.就業制度和工作場所安全事件 | E4.客戶、產品和業務活動事件 | E5.實物資產的損壞 | E6.信息科技系統事件 | E7.執行、交割和流程管理事件 |
B1.公司金融 | 0 | 0 | 200 | 0 | 0 | 0 | 0 |
B2.交易和銷售 | 0 | 0 | 0 | 400 | 0 | 0 | 0 |
B3.零售銀行 | 0 | 0 | 0 | 0 | 300 | 0 | 0 |
B4.商業銀行 | 200 | 800 | 0 | 0 | 0 | 0 | 0 |
B5.支付和結算 | 600 | 600 | 0 | 0 | 0 | 0 | 0 |
B6.代理服務 | 0 | 0 | 0 | 0 | 0 | 600 | 0 |
B7.資產管理 | 0 | 0 | 0 | 0 | 0 | 0 | 500 |
B8.零售經紀 | 0 | 0 | 200 | 0 | 0 | 0 | 0 |
也可以使用case when …then…end語句來代替上式中的decode函數。再此不另附編碼。 引種方案是最直接的解決方案。其優點是直觀。缺點是使用了太多的硬編碼,看起來非常的繁瑣,特別是當column_col的值較多或需要使用多個column_col對應的value_col進行計算時,重復性的硬編碼將會在代碼中多次出現。不論從可讀性、可維護性和擴展性上來講,這都不能稱為一個好的解決方案。 如果能有什么方法直接將column_col的值轉置成列名,然后在SQL直接按列名取對應的統計值就好了。那樣的話,生成報表的代碼就可以寫成類似這樣的代碼: SELECT group_col,E1.內部欺詐,E2.外部欺詐 FROM (…) 有沒有這樣的方法呢? 3.TABLE 能直接按列名訪問的,可以是TABLE,可以是VIEW,也可以是CURSOR。 先看TABLE。 使用Table,有兩種思路,一種是批量時使用Tt。由於不想因為增實體表而增大系統的復雜性,此種思路暫不考慮。 另一種思路是在SQL中生成臨時表,生成報表后,在SQL中將臨時表刪除。這種思路帶來的問題是如何解決臨時表的命名沖突或數據一致性問題。 對於臨時表的命名沖突,可以根據查詢的條件組成臨時表名,但這樣會造成生成很多臨時表的混亂局面,也無法避免兩個查詢條件一致時的並發問題。出於此種考慮,放棄掉使用臨時表的想法。 4.VIEW 再看VIEW。 View是基於一個表或多個表或視圖的邏輯表。建立View不會帶來物理存儲上的混亂。看起來是個不錯的選擇。但在解決此問題時,並不適合使用View,原因如下: 通用性問題:如果為這個需求建立了視圖,那么再出現一個類似的需求是否也要建立一個視圖?而這樣的需求可以說是無窮盡的。只對表1,就可以提出很多種類似於表2的查詢統計。如按機構、業務條線統計損失金額,再如按機構、損失事件類型統計損失金額等,可以按時間、機構、業務條線、損失事件類型、金額范圍等多個維護的兩兩組合。 命名問題:如果使用臨時生成的View,存在命名沖突的問題。 對於臨時生成View,在技術上似乎可行,示例見代碼2:代碼2:使用臨時視圖 WITH t0 AS( SELECT create_v('v_test') FROM dual --生成臨時視圖v_test ) ,t1 AS ( SELECT * FROM v_test --使用視圖v_test ) , t2 AS( SELECT delete_v('v_test') FROM dual --刪除視圖v_test ) SELECT * FROM t1, t2; 以上代碼試圖在t0段中調用create_v函數生成臨時視圖v_test,在t1段中使用該視圖,在t2段中刪除視圖。 但代碼2中至少存在兩外錯誤:一是如果v_test是create_v()函數生成的臨時視圖,則t1段時無法直接使用;二是如果v_test視圖早就存在,create_v()函數只是修改,則執行時報ORA-14552: cannot perform a DDL, commit or rollback inside a query or DML,即違反了“在查詢或數據管理語言中不能執行數據定義語言、提交或回滾”的約束。 由此看來,使用VIEW並非可行之選。 5.CURSOR Cursor又叫游標,其作用是用於臨時存儲從數據庫中提取的數據。 考慮到封裝性,可以將用一個函數動態生成代碼1,然后用代碼1生成游標,並返回這個游標給外層的SQL,期望外層的SQL能能象使用table那樣使用返回的游標。 Oracle中有三種Cursor,分別是隱式Cursor、顯式Cursor和Ref Cursor(動態Cursor)。其中Ref Cursor可以作為函數參數或函數返回值進行傳遞,這將成為首先考慮的途徑。Oracle還提供了DBMS_SQL包,該包中提供了更象是類似於C或Java等高級編程語言中的Cursor及操作,這也給我們提供了一種選擇。 不管使用哪種Cursor,首要的任務是編制動態生成代碼1的函數,在這里命名為GETNVSQL(),含義為GET Name Value SQL。 5.1GETNVSQL函數 GETNVSQL函數的功能為動態生成代碼1。執行分以下兩個步驟進行: 步驟一:生成列標簽cursor。 步驟二:對於列標簽cursor每一行,組裝成decode()語句。 步驟三:組裝上SELECT 、WHERE子句、GROUP子句和ORDER子句。 考慮到通用性,將表名、分組列、轉置列、值列、聚合函數名都設計為函數輸入參數。為限制選擇數據、排序方式、缺值處理,將選擇條件、行列排序和空值處理也設計為函數輸入參數。完整的GETNVSQL函數參見代碼3。
代碼3:GETNVSQL函數 CREATE OR REPLACE FUNCTION GETNVSQL(TABNAME IN VARCHAR2, -- 需要進行行轉列操作的表名; GROUP_COL IN VARCHAR2, -- 查詢結果要按某列或某些列分組的字段名; COLUMN_COL IN VARCHAR2, -- 要從行轉成列的字段; VALUE_COL IN VARCHAR2, -- 需要聚合的值字段; AGGREGATE_FUNC IN VARCHAR2 DEFAULT 'max', -- 選用的聚合函數,可選; CONDITIONS IN VARCHAR2 DEFAULT ' 1 = 1', -- 條件 COLORDER IN VARCHAR2 DEFAULT NULL, -- 行轉列后列的排序,可選; ROWORDER IN VARCHAR2 DEFAULT NULL, -- 行轉列后記錄的排序,可選; WHEN_VALUE_NULL IN VARCHAR2 DEFAULT NULL) -- 空值處理,可選。 RETURN VARCHAR2 AS SQLSTR VARCHAR2(32767) := 'SELECT ' || GROUP_COL || ' '; C1 SYS_REFCURSOR; ValueStr VARCHAR2(100); BEGIN OPEN C1 FOR 'SELECT distinct ' || COLUMN_COL || ' FROM ' || TABNAME || ' WHERE ' || CONDITIONS || CASE WHEN COLORDER IS NOT NULL THEN ' ORDER BY ' || COLORDER END; LOOP FETCH C1 INTO ValueStr; EXIT WHEN C1%NOTFOUND; SQLSTR := SQLSTR || CHR(10) || ',' || CASE WHEN WHEN_VALUE_NULL IS NOT NULL THEN 'nvl(' END || AGGREGATE_FUNC || '(decode(to_char(' || COLUMN_COL || '),''' || ValueStr || ''',' || VALUE_COL || '))' || CASE WHEN WHEN_VALUE_NULL IS NOT NULL THEN CHR(44) || WHEN_VALUE_NULL || CHR(41) END || '"' || ValueStr || '"'; END LOOP; CLOSE C1; SQLSTR := SQLSTR || ' FROM ' || TABNAME || ' WHERE ' || CONDITIONS || ' GROUP BY ' || GROUP_COL || CASE WHEN ROWORDER IS NOT NULL THEN ' ORDER BY ' || ROWORDER END; RETURN(SQLSTR); END GETNVSQL;
用以下代碼對GETNVSQL函數進行測試:
SELECT getnvsql('Ts', 'group_col', 'column_col','value_col', 'sum', '1=1', 'column_col', 'group_col', ..
得到GETNVSQL函數生成的代碼同代碼1一致,完成了生成標簽和統計值的功能。 5.2 Ref Cursor Ref Cursor提供了在函數間傳遞游標的可能性。對Ref Cursor的使用一般按Open、Fetch和Close三個步驟進行。 在函數中可以利用代碼3可以動態生成代碼1,可以為代碼1利用Open生成Ref Cursor。但是這個RefCursor如何能在SQL中直接使用呢?又在什么時候執行Close以釋放Ref Cursor占用的資源呢?經過多種嘗試,也沒有找到在SQL中直接使用Ref Cursor的辦法。 5.3 DBMS_SQL 再看DBMS_SQL。DBMS_SQL包提供了動態定義Cursor、動態生成Cursor、動態進行變量綁定等功能,使用方法更像是用C或Java等高級語言操作數據庫游標的方式。DBMS_SQL包中所用的Cursor更像是一個文件指針,其類型是int型的。其使用一般按Open、Parse、Describe_Columns、Define_Columns,Fetch_Rows和Close等步驟進行。DBMS_SQL給了開發者更多的自由來操縱Cursor。 然而,這里的Cursor也無法直接提供給SQL使得。 要想把Cursor提供給SQL象TABLE那樣使用,只能再經過包裝。 6.TABLE函數 Oracle提供了TABLE函數,可以接收自定義數組參數,返回值可以被視為表或視圖一樣進行訪問。 使用TABLE函數一般按以下步驟進行: 步驟一:定義行數據類型; 步驟二:定義行數據類型的表類型; 步驟三:生成表類型的數據; 步驟四:在SQL中FROM子句中使用TABLE(表類型數據)進行調用。 其中步驟一、二決定了步驟三、四的設計,是解決問題的關鍵。設計什么樣的行數據類型合適呢? 6.1類型定義 我們可以按Tt的結構來設計行數據類型。但帶來的問題,每一個種應用有不同的列標簽,我們不可能窮舉所有可能的結構(見4.VIEW中的討論)。如何能定義一種可以包含這些可能的結構的結構呢?這樣的結構是否存在呢? 我們先來分析一下行(表中的一條記錄)的本質。表中的一行是表結構類型的一個具體實例,也可以解釋為表結構類型的一個賦了值結構體變量,對行中列值的引用是通過列名來進行的。在這種意義上,我們可以認為表中的一行是一個名值對的列表。由此可進行如下定義: 名值對:=<名|值> (1) 行:=名值對1…,名值對i…,名值對N (2) 根據此定義,我們可以定義行數據類型是一個字符串,字符串中按名值對的形式存放數據。 由此可進行行數據和表數據類型定義,見代碼4。代碼4:類型定義
CREATE OR REPLACE TYPE nvs_row AS OBJECT( nvs VARCHAR2(32767) ); CREATE OR REPLACE TYPE nvs_tab AS TABLE OF nvs_row;
其中32767是varchar2類型變量的長度最大值。 以下是一行數據的例子: ,FGROUP_COL1|Commercial,F121|200,F125|500 至於從這樣的字符串中取出名對應的值,就是件純技術上的事情了,實現參見代碼5。代碼5:GETNV函數
CREATE OR REPLACE FUNCTION GETNV(PROPERTIES IN VARCHAR2, PROPERTY_NAME IN VARCHAR2) RETURN VARCHAR2 AS RESULT VARCHAR2(100) := '0.0'; NAMELOCAL VARCHAR2(100) := 'F' || PROPERTY_NAME || '|'; POSBEGIN INT := INSTR(PROPERTIES, NAMELOCAL); POSEND INT := 0; LENGTHPROPERTY INT := 0; VALUEBEGIN INT := 0; BEGIN IF POSBEGIN > 1 THEN BEGIN VALUEBEGIN := POSBEGIN + LENGTH(NAMELOCAL); POSEND := INSTR(PROPERTIES, ',F', VALUEBEGIN); IF POSEND = 0 THEN POSEND := LENGTH(PROPERTIES); END IF; LENGTHPROPERTY := POSEND - VALUEBEGIN; RESULT := SUBSTR(PROPERTIES, VALUEBEGIN, LENGTHPROPERTY); END; END IF; RETURN(RESULT); END GETNV;
6.2生成自定義表 選擇使用DBMS_SQL包編寫生成自定義表的函數,原因如下: 原因一:代碼3所生成SQL語句執行的結果集字段名不確定; 原因二:結果集字段個數不確定; 原因三:DBMS_SQL包為探索和訪問動態游標提供了可能性。 根據SQL語句生成自定義表的函數實現,見代碼6。 為了使用上的方便,編寫GETNVST函數作為開給SQL的最終接口。在GETNVST函數中依次調用GETNVSQL函數和GETNVSTA函數,並返回自定義表,函數實現見代碼7。 7.完整解決方案 綜合以上討論,完整的解決方案包含以下五個構件: 構件一:NVS_ROW和NVS_TAB類型定義。見代碼4。 構件二:主控函數GETNVST。見代碼7。 構件三:動態生成SQL函數GETNVSQL。見代碼3。代碼6:GETNVSTA函數
CREATE OR REPLACE FUNCTION GETNVSTA(SQLSTR IN VARCHAR2) RETURN NVS_TAB AS NVS NVS_TAB := NVS_TAB(); L_CURSOR INTEGER := DBMS_SQL.OPEN_CURSOR; L_VALUE VARCHAR2(4000) := 'null'; L_STATUS INTEGER; L_COLCOUNTS INTEGER; L_DESCTBL DBMS_SQL.DESC_TAB; ROWSTR VARCHAR2(32766); I INTEGER := 0; BEGIN --分析sql DBMS_SQL.PARSE(L_CURSOR, SQLSTR, DBMS_SQL.NATIVE); --獲取列數 DBMS_SQL.DESCRIBE_COLUMNS(L_CURSOR, L_COLCOUNTS, L_DESCTBL); --對每一列設置 FOR I IN 1 .. L_COLCOUNTS LOOP DBMS_SQL.DEFINE_COLUMN(L_CURSOR, I, L_VALUE, 4000); END LOOP; L_STATUS := DBMS_SQL.EXECUTE(L_CURSOR); WHILE (DBMS_SQL.FETCH_ROWS(L_CURSOR) > 0) LOOP NVS.EXTEND; ROWSTR := ''; FOR I IN 1 .. L_COLCOUNTS LOOP --依次取值 DBMS_SQL.COLUMN_VALUE(L_CURSOR, I, L_VALUE); ROWSTR := ROWSTR || ',F' || L_DESCTBL(I).COL_NAME || '|' || L_VALUE; END LOOP; NVS(NVS.COUNT) := nvs_row(ROWSTR); END LOOP; DBMS_SQL.CLOSE_CURSOR(L_CURSOR); RETURN(NVS); END GETNVSTA; 代碼7:GETNVST函數 CREATE OR REPLACE FUNCTION GETNVST(TABNAME IN VARCHAR2, GROUP_COL IN VARCHAR2, COLUMN_COL IN VARCHAR2, VALUE_COL IN VARCHAR2, AGGREGATE_FUNC IN VARCHAR2 DEFAULT 'max', CONDITIONS IN VARCHAR2 DEFAULT ' 1 = 1', COLORDER IN VARCHAR2 DEFAULT NULL, ROWORDER IN VARCHAR2 DEFAULT NULL, WHEN_VALUE_NULL IN VARCHAR2 DEFAULT NULL) RETURN NVS_TAB AS QLSTR VARCHAR2(32767) := ' '; BEGIN SQLSTR := GETNVSQL(TABNAME, GROUP_COL, COLUMN_COL, VALUE_COL, AGGREGATE_FUNC, CONDITIONS, COLORDER, ROWORDER, WHEN_VALUE_NULL); RETURN(GETNVSTA(SQLSTR)); END GETNVST;
構件四:根據SQL生成自定義表函數GETNVSTA。見代碼6。 構件五:取名值函數GETNV。見代碼5。 構件關系圖見圖1。 圖1:構件關系圖至此開篇所列行列轉換數據透視的需求問題得以解決,應用示例代碼參見代碼8。代碼8:應用示例
SELECT getnv(nvs, 'GROUP_COL') "業務條線" , getnv(nvs,'E1.內部欺詐') + getnv(nvs,'E2.外部欺詐') "欺詐" , getnv(nvs,'E6.信息科技系統事件') "系統" FROM table(getnvst('Ts', 'group_col', 'column_col','value_col', 'sum', '1=1', 'column_col', 'group_col', '0.0'))
8.討論
該解決方案存在以下限制:
限制一:無法處理column_col或value_col值中含有字符’|’的情況。
限制二:GETNVST參數中tabname不接受會話級臨時表名。
限制三:生成的動態SQL存儲在VARCHAR2字符串變量中,而其最大長度為32767;NVS_ROW中的NVS也定義為VARCHAR2,同樣受32767的長度限制。
限制四:無法支持從外VALUE_COL,或對一個VALUE_COL同時使用多個聚合函數。變通的方法是多次調用生成多個臨時表,但這有性能上的損耗。
該解決方案可以有以下擴展應用:
擴展應用一:五個構件中除GETNVST外都可單獨使用。互相之間不存在較依賴關系。
擴展應用二:需求和例子中只給出了一個GROUP_COL的例子。實際上,該解決方案支持使用多個GROUP_COL,只需要在傳遞參數時將多個GROUP_COL之間用逗號分隔即可。
擴展應用三:需求和例子中只給出一個TABNAME的例子。實際上,支持使用多個TABNAME。注意兩點:一是在傳遞TABNAME參數時用類似於’tabname1,tabname2’的形式;二是在Conditions中使用正確的連接方式。
為了不使代碼占太多的篇幅,在代碼中省略了很多注釋和規范性的空行,可能造成閱讀上的很多不便。
代碼3的函數接口定義和實現部分參照了網上的內容,由於轉載次數過多,最初出處已不能明確標明。
本文鏈接http://www.cxybl.com/html/wlbc/db/Sql_Server/20121126/34381.html