您好,登錄后才能下訂單哦!
IN 和 EXISTS 是 SQL 中常見的復雜條件,在將 SQL(存儲過程)轉換成庫外計算獲取高性能時也會面對這些問題。本文將以 TPC-H 定義的模型為基礎,介紹如何用集算器的語法實現 IN、EXISTS 并做優化。
TPC-H 是 TPC 事務處理性能委員會制定的用于 OLAP 數據庫管理系統的測試標準,模擬真實商業應用環境,以評估商業分析中決策支持系統的性能。TPC-H 模型定義了 8 張表,表結構和表關系如下圖:
select P_SIZE, P_TYPE, P_BRAND, count(1) as P_COUNT from PART where P_SIZE in (2, 3, 8, 15, 17, 25, 27, 28, 30, 38, 41, 44, 45) and P_TYPE in ('SMALL BRUSHED NICKEL', 'SMALL POLISHED STEEL') and P_BRAND not in ('Brand#12', 'Brand#13') group by P_SIZE, P_TYPE, P_BRAND
如果常數集合元素數少于 3 個則可以翻譯成 (f == v1 || f == v2) 這種樣式,NOT IN 對應的就是(f != v1 && f != v2)。較多的時候可以在外層把常數集合定義成序列,然后用 A.contain(f)來判斷字段是否在序列中,經驗表明元素個數超過 10 個時二分查找會明顯快于順序查找,如果要用二分查找則需要先把序列排序,然后用 A.contain@b(f)來進行有序查找,NOT IN 對應的就是! A.contain(f)。注意一定要把序列定義在循環函數外,否則會被多次執行。
如果常數集合元素數量特別多可以用連接過濾,具體請參照下圖代碼。
A | B | |
---|---|---|
1 | =[28, 30, 38,2, 3, 8, 15, 17, 25, 27,50 , 41, 44, 45].sort() | / 對常數集合進行排序,這樣就可以用序列的有序查找,通常序列元素數超過 13 個用有序查找會比遍歷快 |
2 | =file(PART).cursor@b(P_SIZE, P_TYPE, P_BRAND) | / 在 PART 表所對應的集文件上定義游標,參數為選出列 |
3 | =A2.select(A1.contain@b(P_SIZE)&& (P_TYPE == “SMALL BRUSHED NICKEL” || P_TYPE == “SMALL POLISHED STEEL”)&& (P_BRAND != “Brand#12” && P_BRAND != “Brand#13”)) | / 對游標附加過濾操作,注意常數序列要定義在過濾函數外面否則會被重復運算 |
4 | =A3.groups(P_SIZE, P_TYPE, P_BRAND; count(1): P_COUNT) | / 對游標計算分組得到最終結果 |
如果 A1 的元素數量特別多,則可以使用哈希連接的方法來過濾,把第 3 行代碼替換如下:
3 | =A2.select((P_TYPE == “SMALL BRUSHED NICKEL” || P_TYPE == “SMALL POLISHED STEEL”)&& (P_BRAND != “Brand#12” && P_BRAND != “Brand#13”)).join@i(P_SIZE, A1:~) | // 對游標附加過濾操作后再附加連接過濾操作 |
select PS_SUPPKEY, count(1) as S_COUNT from PARTSUPP where PS_PARTKEY in ( select P_PARTKEY from PART where P_NAME like 'bisque%%' ) group by PS_SUPPKEY
子查詢過濾后讀入內存,然后外層表與先讀入的內存表(子查詢)做哈希連接進行過濾。集算器提供了 switch@i()、join@i() 兩個函數用來做哈希連接過濾,switch 是外鍵式連接,用來把外鍵字段變成指引字段,這樣就可以通過外鍵字段直接引用指向表的字段,join 函數不會改變外鍵字段的值,可用于只過濾。
A | B | |
---|---|---|
1 | =file(PART).cursor@b(P_PARTKEY, P_NAME) | / 在 PART 表所對應的集文件上定義游標,參數為選出列 |
2 | =A1.select(like(P_NAME, “bisque*”)).fetch() | / 對游標附加過濾操作并取數 |
3 | =file(PARTSUPP).cursor@b(PS_SUPPKEY, PS_PARTKEY) | / 在 PARTSUPP 表所對應的集文件上定義游標,參數為選出列 |
4 | =A3.join@i(PS_PARTKEY, A2:P_PARTKEY) | / 對 PARTSUPP 游標進行連接過濾,@i 選項表示內連接 |
5 | =A4.groups(PS_SUPPKEY; count(1):S_COUNT) | / 對游標計算分組得到最終結果 |
select O_ORDERPRIORITY, count(*) as O_COUNT from ORDERS where O_ORDERDATE >= date '1995-10-01' and O_ORDERDATE < date '1995-10-01' + interval '3' month and O_ORDERKEY in ( select L_ORDERKEY from LINEITEM where L_COMMITDATE< L_RECEIPTDATE ) group by O_ORDERPRIORITY
子查詢過濾后按關聯字段去重讀入內存,然后就變成類似于主鍵的情況了,可以繼續用上面說的 switch@i()、join@i() 兩個函數用來做哈希連接過濾。
A | B | |
---|---|---|
1 | 1995-10-01 | =after@m(A1,3) |
2 | =file(LINEITEM).cursor@b(L_ORDERKEY,L_COMMITDATE,L_RECEIPTDATE) | / 在 LINEITEM 表所對應的集文件上定義游標,參數為選出列 |
3 | =A2.select(L_COMMITDATE < L_RECEIPTDATE) | / 對游標附加過濾操作 |
4 | =A3.groups(L_ORDERKEY) | / 用 groups 對 L_ORDERKEY 去重 |
5 | =file(ORDERS).cursor@b(O_ORDERKEY,O_ORDERDATE,O_ORDERPRIORITY) | / 在 ORDER 表所對應的集文件上定義游標,參數為選出列 |
6 | =A5.select(O_ORDERDATE>=A1 && O_ORDERDATE < B1) | / 對游標附加過濾操作 |
7 | =A6.join@i(O_ORDERKEY, A4:L_ORDERKEY) | / 對 ORDERS 游標進行連接過濾,@i 選項表示內連接 |
8 | =A7.groups(O_ORDERPRIORITY; count(1):O_COUNT) | / 對游標計算分組得到最終結果 |
select O_ORDERPRIORITY, count(*) as O_COUNT from ORDERS where O_ORDERDATE >= date '1995-10-01' and O_ORDERDATE < date '1995-10-01' + interval '3' month and O_ORDERKEY in ( select L_ORDERKEY from LINEITEM where L_COMMITDATE< L_RECEIPTDATE ) group by O_ORDERPRIORITY
IN 子查詢相當于對子查詢結果集去重然后跟外層表做內連接,而做連接效率較好的就是哈希連接和有序歸并連接,所以這個問題就變成了怎么把 IN 翻譯成高效的連接,下面我們來分析在不同的數據分布下如何把 IN 轉成連接。
(1) 外層表數據量比較小可以裝入內存:
先讀入外層表,如果外層表關聯字段不是邏輯主鍵則去重,再拿上一步算出來的關聯字段的值對子查詢做哈希連接過濾,最后拿算出來的子查詢關聯字段的值對外層表做哈希連接過濾。
(2) 外層表和內層表按關聯字段有序:
此時可以利用函數 joinx() 來做有序游標的歸并連接,如果內層表關聯字段不是邏輯主鍵則需要先去重。此例中的 ORDERS 表和 LINEITEM 表是按照 ORDERKEY 同序存放,可以利用此方法來做優化。
(3) 內層表是大維表并且按主鍵有序存放:
集算器提供了針對有序大維表文件做連接的函數 A.joinx,其它方法跟內存能放下時的處理類似在此不再描述。
A | B | |
---|---|---|
1 | 1995-10-01 | =after@m(A1,3) |
2 | =file(ORDERS).cursor@b(O_ORDERKEY,O_ORDERDATE,O_ORDERPRIORITY) | / 在 ORDER 表所對應的集文件上定義游標,參數為選出列 |
3 | =A2.select(O_ORDERDATE>=A1 && O_ORDERDATE < B1).fetch() | / 對游標附加過濾操作并取數 |
4 | =file(LINEITEM).cursor@b(L_ORDERKEY,L_COMMITDATE,L_RECEIPTDATE) | / 在 LINEITEM 表所對應的集文件上定義游標,參數為選出列 |
5 | =A4.select(L_COMMITDATE < L_RECEIPTDATE).join@i(L_ORDERKEY,A3:O_ORDERKEY) | / 對游標附加過濾操作和鏈接過濾操作 |
6 | =A5.groups(L_ORDERKEY) | / 對 L_ORDERKEY 去重 |
7 | =A3.join@i(O_ORDERKEY, A6:L_ORDERKEY) | / 對排列執行鏈接過濾操作 |
8 | =A7.groups(O_ORDERPRIORITY;count(1):O_COUNT) | / 對排列計算分組得到最終結果 |
A | B | |
---|---|---|
1 | 1995-10-01 | =after@m(A1,3) |
2 | =file(ORDERS).cursor@b(O_ORDERKEY,O_ORDERDATE,O_ORDERPRIORITY) | / 在 ORDER 表所對應的集文件上定義游標,參數為選出列 |
3 | =A2.select(O_ORDERDATE>=A1 && O_ORDERDATE < B1) | / 對游標附加過濾操作 |
4 | =file(LINEITEM).cursor@b(L_ORDERKEY,L_COMMITDATE,L_RECEIPTDATE) | / 在 LINEITEM 表所對應的集文件上定義游標,參數為選出列 |
5 | =A4.select(L_COMMITDATE < L_RECEIPTDATE) | / 對游標附加過濾操作 |
6 | =A5.group@1(L_ORDERKEY) | / 按 L_ORDERKEY 去重 |
7 | =joinx(A3:ORDER, O_ORDERKEY; A6, L_ORDERKEY) | / 對有序游標執行內連接 |
8 | =A7.groups(ORDER.O_ORDERPRIORITY:O_ORDERPRIORITY;count(1):O_COUNT) | / 對游標計算分組得到最終結果 |
此章節的優化思路和 IN 子查詢的優化思路是相同的,事實上這種 EXISTS 也都可以用 IN 寫出來(或者倒過來,把 IN 用 EXISTS 寫出來)。
select PS_SUPPKEY, count(1) as S_COUNT from PARTSUPP where exists ( select * from PART where P_PARTKEY = PS_PARTKEY and P_NAME like 'bisque%%' ) group by PS_SUPPKEY
子查詢過濾后讀入內存,然后外層表與先讀入的內存表(子查詢)做哈希連接進行過濾。集算器提供了 switch@i()、join@i() 兩個函數用來做哈希連接過濾,switch 是外鍵式連接,用來把外鍵字段變成指引字段,這樣就可以通過外鍵字段直接引用指向表的字段,join 函數不會改變外鍵字段的值,可用于只過濾。
A | B | |
---|---|---|
1 | =file(PART).cursor@b(P_PARTKEY, P_NAME) | / 在 PART 表所對應的集文件上定義游標,參數為選出列 |
2 | =A1.select(like(P_NAME, “bisque*”)).fetch() | / 對游標附加過濾操作并取數 |
3 | =file(PARTSUPP).cursor@b(PS_SUPPKEY, PS_PARTKEY) | / 在 PARTSUPP 表所對應的集文件上定義游標,參數為選出列 |
4 | =A3.join@i(PS_PARTKEY, A2:P_PARTKEY) | / 對 PARTSUPP 游標進行連接過濾,@i 選項表示內連接 |
5 | =A4.groups(PS_SUPPKEY; count(1):S_COUNT) | / 對游標計算分組得到最終結果 |
select O_ORDERPRIORITY, count(*) as O_COUNT from ORDERS where O_ORDERDATE >= date '1995-10-01' and O_ORDERDATE < date '1995-10-01' + interval '3' month and exists ( select * from LINEITEM where L_ORDERKEY = O_ORDERKEY and L_COMMITDATE < L_RECEIPTDATE ) group by O_ORDERPRIORITY
子查詢過濾后按關聯字段去重讀入內存,然后就變成類似于主鍵的情況了,可以繼續用上面說的 switch@i()、join@i() 兩個函數用來做哈希連接過濾。
A | B | |
---|---|---|
1 | 1995-10-01 | =after@m(A1,3) |
2 | =file(LINEITEM).cursor@b(L_ORDERKEY,L_COMMITDATE,L_RECEIPTDATE) | / 在 LINEITEM 表所對應的集文件上定義游標,參數為選出列 |
3 | =A2.select(L_COMMITDATE < L_RECEIPTDATE) | / 對游標附加過濾操作 |
4 | =A3.groups(L_ORDERKEY) | / 對 L_ORDERKEY 去重 |
5 | =file(ORDERS).cursor@b(O_ORDERKEY,O_ORDERDATE,O_ORDERPRIORITY) | / 在 ORDER 表所對應的集文件上定義游標,參數為選出列 |
6 | =A5.select(O_ORDERDATE>=A1 && O_ORDERDATE < B1) | / 對游標附加過濾操作 |
7 | =A6.join@i(O_ORDERKEY, A4:L_ORDERKEY) | / 對 ORDERS 游標進行連接過濾,@i 選項表示內連接 |
8 | =A7.groups(O_ORDERPRIORITY; count(1):O_COUNT) | / 對游標計算分組得到最終結果 |
select O_ORDERPRIORITY, count(*) as O_COUNT from ORDERS where O_ORDERDATE >= date '1995-10-01' and O_ORDERDATE < date '1995-10-01' + interval '3' month and exists ( select * from LINEITEM where L_ORDERKEY = O_ORDERKEY and L_COMMITDATE < L_RECEIPTDATE ) group by O_ORDERPRIORITY
等值 EXISTS 相當于對內部表關聯字段去重然后跟外層表做內連接,而做連接效率較好的就是哈希連接和有序歸并連接,所以這個問題就變成了怎么把 EXISTS 翻譯成高效的連接,下面我們來分析在不同的數據分布下如何把 EXISTS 轉成連接。
1、外層表數據量比較小可以裝入內存:
先讀入外層表,如果外層表關聯字段不是邏輯主鍵則去重,再拿上一步算出來的關聯字段的值對子查詢做哈希連接過濾,最后拿算出來的子查詢關聯字段的值對外層表做哈希連接過濾。
2、外層表和內層表按關聯字段有序:
此時可以利用函數 joinx() 來做有序游標的歸并連接,如果內層表關聯字段不是邏輯主鍵則需要先去重。此例中的 ORDERS 表和 LINEITEM 表是按照 ORDERKEY 同序存放,可以利用此方法來做優化。
3、內層表是大維表并且按主鍵有序存放:
集算器提供了針對有序大維表文件做連接的函數 A.joinx,其它方法跟內存能放下時的處理類似在此不再描述。
A | B | |
---|---|---|
1 | 1995-10-01 | =after@m(A1,3) |
2 | =file(ORDERS).cursor@b(O_ORDERKEY,O_ORDERDATE,O_ORDERPRIORITY) | / 在 ORDER 表所對應的集文件上定義游標,參數為選出列 |
3 | =A2.select(O_ORDERDATE>=A1 && O_ORDERDATE < B1).fetch() | / 對游標附加過濾操作并取數 |
4 | =file(LINEITEM).cursor@b(L_ORDERKEY,L_COMMITDATE,L_RECEIPTDATE) | / 在 LINEITEM 表所對應的集文件上定義游標,參數為選出列 |
5 | =A4.select(L_COMMITDATE < L_RECEIPTDATE).join@i(L_ORDERKEY,A3:O_ORDERKEY) | / 對游標附加過濾操作和鏈接過濾操作 |
6 | =A5.groups(L_ORDERKEY) | / 對 L_ORDERKEY 去重 |
7 | =A3.join@i(O_ORDERKEY, A6:L_ORDERKEY) | / 對排列執行鏈接過濾操作 |
8 | =A7.groups(O_ORDERPRIORITY;count(1):O_COUNT) | / 對排列計算分組得到最終結果 |
A | B | |
---|---|---|
1 | 1995-10-01 | =after@m(A1,3) |
2 | =file(ORDERS).cursor@b(O_ORDERKEY,O_ORDERDATE,O_ORDERPRIORITY) | / 在 ORDER 表所對應的集文件上定義游標,參數為選出列 |
3 | =A2.select(O_ORDERDATE>=A1 && O_ORDERDATE < B1) | / 對游標附加過濾操作 |
4 | =file(LINEITEM).cursor@b(L_ORDERKEY,L_COMMITDATE,L_RECEIPTDATE) | / 在 LINEITEM 表所對應的集文件上定義游標,參數為選出列 |
5 | =A4.select(L_COMMITDATE < L_RECEIPTDATE) | / 對游標附加過濾操作 |
6 | =A5.group@1(L_ORDERKEY) | / 按 L_ORDERKEY 去重 |
7 | =joinx(A3:ORDER, O_ORDERKEY; A6, L_ORDERKEY) | / 對有序游標執行內連接 |
8 | =A7.groups(ORDER.O_ORDERPRIORITY:O_ORDERPRIORITY;count(1):O_COUNT) | / 對游標計算分組得到最終結果 |
select L_SUPPKEY, count(*) as numwait from LINEITEM L1, where L1.L_RECEIPTDATE > L1.L_COMMITDATE and exists ( select * from LINEITEM L2 where L2.L_ORDERKEY = L1.L_ORDERKEY and L2.L_SUPPKEY <> L1.L_SUPPKEY ) and not exists ( select * from LINEITEM L3 where L3.L_ORDERKEY = L1.L_ORDERKEY and L3.L_SUPPKEY <> L1.L_SUPPKEY and L3.L_RECEIPTDATE > L3.L_COMMITDATE ) group by L_SUPPKEY
我們先來看一下 LINEITEM 表的數據特點,LINEITEM 表的主鍵是 L_ORDERKEY、L_LINENUMBER,一個訂單對應 LINEITEM 里的多條記錄,這些記錄的 L_ORDERKEY 是相同的并且在數據文件中是相鄰的。知道這些信息后再來分析上面的 SQL,其條件是為了找出有多個供應商供貨并且有且僅有一個供應商沒有按時交貨的訂單,因為數據是按訂單順序存放的,這樣我們就可以按訂單有序分組,然后循環每組訂單判斷是否有沒按時交貨的訂單項,是否有多個供貨商,并且是不是只有一個供應商沒有按時交貨。
A | B | |
---|---|---|
1 | =file(LINEITEM).cursor@b(L_ORDERKEY,L_SUPPKEY,L_RECEIPTDATE,L_COMMITDATE) | / 在 LINEITEM 表所對應的集文件上定義游標,參數為選出列 |
2 | =A1.group(L_ORDERKEY) | / 對有序游標附加分組, 結果為排列構成的游標 |
3 | =A2.conj((t=~.select(L_RECEIPTDATE > L_COMMITDATE),if(t.len() > 0 && t.select@1(t(1).L_SUPPKEY!=L_SUPPKEY)== null && ~.select@1(t(1).L_SUPPKEY!=L_SUPPKEY)!= null,t,null))) | / 選出每一組中沒有按時發貨的訂單賦值給臨時變量 t,如果 t 長度大于 0 并且 t 中的供應商只有一個并且此組中供應商有多個則返回 t,否則返回 null,conj 相當于 group 的逆操作 |
4 | =A3.groups@u(L_SUPPKEY;count(1):numwait) | / 對游標計算分組得到最終結果 |
在沒有空值的時候帶子查詢的 IN 都可以用 EXISTS 描述,同一個查詢需求用 IN 描述和用 EXISTS 描述翻譯成的集算器代碼是相同的,所以我們只要弄清楚 EXISTS 怎么翻譯和優化就知道 IN 怎么處理了。
等值 exist 本質上是做連接,兩個表做連接效率較好的兩種方式是哈希連接和有序歸并連接,對于翻譯 select *** from A where exists (select *** from B where ***) 樣式的 SQL,我們首先要弄清楚下列信息:
(1)關聯字段是否是各表的主鍵或者邏輯主鍵
(2)A、B 表的規模,執行其它過濾條件后是否能載入內存
(3)如果沒有某個表能裝入內存則要考察兩個表是否按關聯字段有序
如果有一個表能載入內存則可以選用哈希連接的方式來實現,相關的集算器函數有兩個 cs.switch()、cs.join(),這兩個函數有兩個可用的選項 @i、@d 分別對應 exists 和 not exists,參數里的表要求按關聯字段值唯一,如果不是邏輯主鍵則要先去重,可用 A.groups()去重。如果兩個表都很大不能載入內存則要考察兩個表是否按關聯字段有序,如果無序可以用 cs.sortx() 排序,對于有序的兩個表就可以用 joinx() 來做連接了。
非等值運算則要分析其中的運算邏輯看能否轉成分組后再計算,如果不能則只能使用嵌套循環連接的方式了,對應的函數是 xjoin()。
知道這些信息并熟練掌握集算器相關的幾個函數后我們就能夠寫出高效的代碼。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。