如何用外部程式優化SQL語句中的IN和EXISTS
資料結構
IN 和 EXISTS 是 SQL 中常見的複雜條件,在將 SQL(儲存過程)轉換成庫外計算獲取高效能時也會面對這些問題。本文將以 TPC-H 定義的模型為基礎,介紹如何用集算器的語法實現 IN、EXISTS 並做優化。
TPC-H 是 TPC 事務處理效能委員會制定的用於 OLAP 資料庫管理系統的測試標準,模擬真實商業應用環境,以評估商業分析中決策支援系統的效能。TPC-H 模型定義了 8 張表,表結構和表關係如下圖:
IN 常數集合
SQL 示例(1):
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)。注意一定要把序列定義在迴圈函式外,否則會被多次執行。
如果常數集合元素數量特別多可以用連線過濾,具體請參照下圖程式碼。
集算器實現:
如果 A1 的元素數量特別多,則可以使用雜湊連線的方法來過濾,把第 3 行程式碼替換如下:
IN子查詢
子查詢選出欄位是主鍵
SQL 示例(2):
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 函式不會改變外來鍵欄位的值,可用於只過濾。
集算器實現:
子查詢選出欄位不是主鍵
SQL 示例(3):
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() 兩個函式用來做雜湊連線過濾。
集算器實現:
子查詢結果集存放不下
SQL 示例(3):
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,其它方法跟記憶體能放下時的處理類似在此不再描述。
集算器實現(1):
集算器實現(2):
EXISTS 等值條件
此章節的優化思路和 IN 子查詢的優化思路是相同的,事實上這種 EXISTS 也都可以用 IN 寫出來(或者倒過來,把 IN 用 EXISTS 寫出來)。
子查詢關聯欄位是主鍵
SQL 示例(4):
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 函式不會改變外來鍵欄位的值,可用於只過濾。
集算器實現:
子查詢關聯欄位不是主鍵
SQL 示例(5):
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() 兩個函式用來做雜湊連線過濾。
集算器實現:
子查詢結果集存放不下
SQL 示例(5):
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,其它方法跟記憶體能放下時的處理類似在此不再描述。
集算器實現(1):
集算器實現(2):
EXISTS 非等值條件
同表關聯
SQL 示例(6):
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,其條件是為了找出有多個供應商供貨並且有且僅有一個供應商沒有按時交貨的訂單,因為資料是按訂單順序存放的,這樣我們就可以按訂單有序分組,然後迴圈每組訂單判斷是否有沒按時交貨的訂單項,是否有多個供貨商,並且是不是隻有一個供應商沒有按時交貨。
集算器實現:
總結
在沒有空值的時候帶子查詢的 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()。
知道這些資訊並熟練掌握集算器相關的幾個函式後我們就能夠寫出高效的程式碼。
Linux公社的RSS地址 : ofollow,noindex" target="_blank">https://www.linuxidc.com/rssFeed.aspx
本文永久更新連結地址: https://www.linuxidc.com/Linux/2018-09/154355.htm