1. 程式人生 > >一個開發需求的解決方案 & Oracle臨時表介紹

一個開發需求的解決方案 & Oracle臨時表介紹

一、開發需求
最近有一個開發需求,大致需要先使用主表,或主表和幾張子表關聯查詢出ID(主鍵)及一些主表字段,然後再用這些ID查詢最多10張表中對應的記錄,主表記錄數大約2000萬,每張子表的記錄數均為百萬以上,最多可能會有5000萬,主表一條資料可能對應子表多條資料。現在開發使用的邏輯是:
1.使用條件查詢主表或主表和幾張子表(不同場景)符合條件的主表記錄ID值及其他一些主表字段項。
2.利用這些主表ID值,分別和幾張子表使用IN子句,查詢出子表中符合條件的記錄項。有幾張子表,就執行幾次SQL語句。

這麼做的弊端是:
由於(1)查出的ID值最多可能會有100個以上,因此子表使用IN子句的時候很有可能導致CBO選擇全表掃描,雖然從理論上說,一條SQL未必適用索引掃描效率就一定高,CBO一定是基於現有的統計資訊選擇一條成本值最低的執行計劃,但一張百萬級甚至千萬級的表,全表掃描的效率可想而知(這兒我們不較真,可能通過SSD、Exadata硬體層面的使用能提高全表掃描的效率,此處只討論一般儲存條件下可行的方案)。另外,就是場景需要幾張子表,就會執行幾次SQL,一個場景下可能需要執行很多次SQL語句。

綜合需求,可能至少有以下幾種改進方案:
1.使用一條SQL完成上述需求。
(1.1) 主表和所有子表採用join關聯的方式。
兩表兩表做join,又由於主子表之間是一對多的關係,很可能造成結果集因為笛卡爾積變得很大,應用處理出現記憶體溢位的錯誤。
(1.2) 使用union all的方式關聯子表,作為VIEW,然後和主表做關聯,這是羅大師推薦的方式,例如:

SELECT A.ID, A.NAME 
FROM 
T_ZHUBIAO A, 
(SELECT ID, NAME FROM T_ZIBIAO1 UNION ALL SELECT ID, NAME FROM T_ZIBIAO2) B 
WHERE
A.NAME = 'A' AND A.ID = B.ID;

和(1.1)的區別就是每一張子表的檢索都是一次獨立的索引唯一掃描,所有子表關聯後作為VIEW,和主表做一次巢狀迴圈連線。但據瞭解,需求中每張子表的欄位基本都不相同,有的子表選擇欄位有幾十個,這麼一來,使用這種UNION ALL需要檢索欄位型別相同,開發拼接起來就比較費勁,不靈活。
2.將(1)的結果集存入一張臨時表(temporary table,不是應用自行處理的普通表),相當於臨時結果集,每次子表都是和這張臨時表做兩表關聯查詢,這麼做可以避免因為IN值太多導致的低效檢索,同時由於兩表關聯欄位均為主鍵或外來鍵(設定索引),可以使用索引掃描檢索,採用交易級別控制的臨時表,可以在完成本次交易後讓Oracle自動清空資料,同時session之間資料隔離。
3.(1)不變,只是(2)中每次子表查詢,由應用控制,例如每30個IN值執行一條SQL語句,將一次子表查詢拆分為若干次查詢,好處是每次可以使用外來鍵索引掃描檢索結果集,壞處就是無形中又多了N次SQL語句的執行。

綜上三種方案,(1)由於潛在的結果集過大的問題以及靈活性問題,被開發否了,目前採用的是方案(3),因為其對開發的改造較小,僅需要拆分IN語句,如果檢索效率較高,測試結論符合非功能要求,就採用這種方式,若不滿足要求,則會考慮使用方案(2)。

就我來說,如果能滿足需求,方案1是最好的,使用合適的索引完成一次檢索,減少了應用和資料庫之間的互動次數,但可能這種業務需求確實很複雜,獲取資訊方面確實要求比較高。其次是方案2,雖然子表執行SQL次數未變,但通過臨時表,可以保證每次檢索均可以使用索引快速定位,避免大表的全表掃描,同時臨時表特性對應用幾乎透明。方案3,唯一的好處就是避免了大表的全表掃描,但代價是會多一些SQL互動,至於究竟是否可以彌補效能上的差異,只能待效能測試的結論來看了。

如果各位對上述需求有更好的解決方案,或是上述方案仍有問題,還請不吝指正

二、臨時表介紹和實驗
需要快取中間結果集的場景,可以考慮使用臨時表,因為臨時表中的資料是session級別私有,每個session僅能看見和修改自己的資料,在session結束的時候,表中資料會被自動刪除,無需應用操作。建立臨時表使用的是CREATE GLOBAL TEMPORARY TABLE語法,ON COMMIT子句則決定了表資料是交易級別還是session級別,預設是交易級別。可以對臨時表建立索引、檢視或觸發器。

ON COMMIT子句的兩種引數區別如下:
這裡寫圖片描述

臨時表中的資料預設儲存於預設的臨時表空間,可以建立過程中指定其他的臨時表空間。臨時表的資料和索引在定義的時候不會分配段,只有使用INSERT(CTAS)插入語句的時候,才會開始分配段空間。

建立交易級別臨時表:

SQL> create global temporary table test (id number, name varchar2(10)) on commit delete rows;

查看錶屬性,TEMPORARY指定為Y,說明是臨時表,沒有tablespace_name引數值,說明不是使用普通表空間儲存。

SQL> select table_name, tablespace_name, temporary from dba_tables where owner='BISAL';
TABLE_NAME       TABLESPACE_NAME      TEM
---------------- -------------------- ---
TEST                                  Y

session 1執行:

SQL> insert into test values(1, 'a');
SQL> select * from test;
ID NAME
-- ----
1  a

session 2執行:

SQL> select * from test;
no rows selected

說明臨時表資料session級別隔離,

session 1執行:

SQL> commit;
SQL> select * from test;
no rows selected

執行commit結束交易,Oracle會自動刪除臨時表中資料。

建立session級臨時表:

SQL> create global temporary table test (id number, name varchar2(10)) on commit preserve rows;

表屬性相同:

SQL> select table_name, tablespace_name, temporary from dba_tables where owner='BISAL';
TABLE_NAME       TABLESPACE_NAME      TEM
---------------- -------------------- ---
TEST                                  Y

session 1執行:

SQL> insert into test values(1, 'a');
SQL> select * from test;
ID NAME
-- ----
1  a

session 2執行:

SQL> select * from test;
no rows selected

session 1執行:

SQL> commit;
SQL> select * from test;
ID NAME
-- ----
1  a

執行commit後,資料未刪除。退出當前session再登陸,發現數據已被刪除了:

SQL> select * from test;
no rows selected

總結:
臨時表使用起來其實很簡單,除了一些語法上和普通建表語句有些不同,對應用來說就可以當作普通表使用,但其實還是有一些細節需要注意:
1.臨時表預設使用的是預設臨時表空間,如果應用會有很多排序等需要耗費臨時表空間的場景,而且臨時表使用頻率很高,那麼為了避免互相影響,可以考慮為臨時表建一個獨立的臨時表空間。
2.如果使用session級別的臨時表,且應用使用了連線池,則需要確保應用完成一次交易過程中使用的是同一session,避免違反臨時表使用規則。