你真的足夠了解Join麼
在平時寫sql時,join語句可能是使用頻率最高語句之一。可是,你真的足夠了解join語句麼。接下來以oracle和hive為例子,介紹join相關的基礎知識,目錄如下
oracle
-
連線型別
-
nested join
-
hash join
-
sort merge join
-
-
連線方法
-
內連線
-
外連線
-
半連線
-
反連線
-
笛卡爾連線
-
hive
-
連線型別
-
map side join
-
reduce side join
-
sort merge join
-
-
連線方法
簡化
優化
Oracle
連線型別
nested join
巢狀迴圈連線將驅動表(外表)和被驅動表(內表)進行join,讀取外表的每一行,和內表進行比較操作,資料庫一般將建有索引的表作為內表。
-
適用範圍:當資料集較小,訪問列上有索引時
-
例子:
SQL> select /*+ leading(t1) use_nl(t) */ 2 empno 3 , ename 4 , dname 5 , loc 6 from emp t 7 join dept t1 8 on t.deptno = t1.deptno; Execution Plan ---------------------------------------------------------- Plan hash value: 4192419542 --------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | --------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 15 | 420 | 13 (0)| 00:00:01 | | 1 | NESTED LOOPS | | 15 | 420 | 13 (0)| 00:00:01 | | 2 | TABLE ACCESS FULL| DEPT | 4 | 72 | 3 (0)| 00:00:01 | |* 3 | TABLE ACCESS FULL| EMP | 4 | 40 | 3 (0)| 00:00:01 | --------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 3 - filter("T"."DEPTNO" IS NOT NULL AND "T"."DEPTNO"="T1"."DEPTNO")
這裡使用hint強制走nested loop, /*+ leading(t1) use_nl(t) */ 表時t1表為驅動表,t為驅動表。執行計劃中靠NESTED LOOPS最近的為驅動表,即
NESTED LOOPS outer_loop inner_loop
-
執行原理:
上面執行計劃可以用如下偽程式碼表示:
for erow in (select empno,dname,loc from dept) loop for drow in (select * from empno,ename where depto = outer.deptno) loop if matched then output values from erow and drow if not matched then discard the row end loop end loop
通過虛擬碼可以看到,執行主要分三步:
-
選定驅動表
-
選定另一張表為內表
-
將驅動表中每一行和內表中的記錄逐一進行匹配
-
hash join
使用場景,當資料量較大,且為等值條件。尤其當小表可以完全讀入記憶體,hash join效率較高
執行步驟如下:
-
掃描小表,對小表使用hash函式並在pga中建立hash表,虛擬碼如下
for small_table_row in (select * from small_table)
loop
slot_number := hash(small_table_row.join_key);
insert_hash_table(slot_number,small_table_row);
end loop
-
訪問另一張較大的表,稱為探測表。資料庫訪問較大表每一行,利用hash函式生成hash值,並和記憶體中較小的表逐一匹配,虛擬碼如下
for large_table_row IN (select * from large_table)
loop
slot_number := hash(large_table_row.join_key);
small_table_row = look_hash_table(slot_number,large_table_row.join_key);
IF small_table_row found
then
output small_table_row + large_table_row;
end if;
end loop;
在此過程中,可能會遇到以下問題:
-
hash過程中發生hash碰撞。 遇到這種情況,資料庫將相同hash值資料儲存一個在連結串列中,想了解LinkedList,猛戳此處
-
小表在pga中儲存不夠。此時將生成hash桶表,將較大的桶寫入臨時表空間(磁碟)中。對探測表每一行,應用hash函式後和pga中hash桶進行匹配,沒找到的情況下再去磁碟hash桶中查詢
sort merge join
hash Join需要一張hash表和一張探測表,而sort merge需要對兩張排序表。在實際過程中,sort消耗成本較大,但在如下情況會使用sort merge
-
Join 條件為不等連線,如 >,<, !=,此種情況下,hash join無法工作。
-
當join 的列中存在索引,此時可以避免對第一張表進行排序(但是資料庫會忽略索引,對第二張表進行排序)。
-
當小表無法完全快取進記憶體中時,此種情況可能會進行sort merge。相對hash join會將表寫入磁碟中,多次進行讀取操作。而sort merge此時會將兩張表部分資料都寫入磁碟中,並且只會讀取一次。
sort merge 執行虛擬碼如下
read data_set_1 sort by join key to temp_ds1
read data_set_2 sort by join key to temp_ds2
read ds1_row from temp_ds1
read ds2_row from temp_ds2
while not eof on temp_ds1,temp_ds2
loop
IF ( temp_ds1.key = temp_ds2.key ) OUTPUT JOIN ds1_row,ds2_row
ELSIF ( temp_ds1.key <= temp_ds2.key ) READ ds1_row FROM temp_ds1
ELSIF ( temp_ds1.key => temp_ds2.key ) READ ds2_row FROM temp_ds2
end loop
總結:nested loop適用於兩個較小資料集,或者較大資料集但是隻返回幾行資料(first_rows hint),nested loop在sga中進行;當資料量較大時且連線條件不為不等連線或沒有索引,此時適用hash join,hash join發生在pag中;如果資料集較大且為不等連線或存在索引,此時sort merge登場了,sort merge在pga中進行。注意:此處闡述的規律為一般情況,不排除有其它的特殊情況;表述的較大或較小資料集指的是進行謂詞過濾後的資料集。
連線方法
連線方法主要分為內連線,外連線,半連線、反連線和笛卡爾連線
內連線
內連線取兩個資料集中重合部分。
外連線
外連結分左外連結,右外連結和全連線。左外連線取左表全部資料及右表能關聯上資料,右表不能關聯上則值為null。右外連結和左外連結剛好相反。全連線同時取左右表全部資料,不能關聯上則置空。以下分別為inner join,left outer join,right outer join,full outer join
半連線
左連線發生在兩個資料集之間。當第一個資料的值在第二個資料集中找到第一行匹配項則停止查詢,此時,第一個資料集返回函式。這樣可以避免分返回大量資料行。
使用場景:當查詢語句中含有in 或 exists子句
反連線
反連線發生在兩個資料集之間。當第一個資料集中在第二個資料集中找到第一行匹配項則停止查詢,此時,第一個資料集不返回函式
使用場景
-
語句中存在not In或not exists
-
語句中存在outer join且條件中存在is null條件,如下:
笛卡爾連線
笛卡爾連線為每一個集合的每一條記錄和另一個集合的每一條記錄進行連線,此時不需要連線鍵。若兩個集合記錄數分別為M、N,則結果記錄數為M*N
hive
hive儲存基於hdfs,計算主要是基於mapreduce框架。
連線型別
map side join
如果join中有一張表較小,這種情況下,可以使用map join,不需要進行reduce操作。
select /*+ mapjoin(b) */
a.key
, a.value
from a
join b
on a.key = b.key
如果join的表在join列上進行了分桶,一張表的桶數是其它表的倍數,這種情況下,相關桶之間在map端進行join操作,稱為bucketized map-side join,通過如下引數開啟
-- 方法一
set hive.optimize.bucketmapjoin = true;
hive中還可以通過以下兩種方式開啟map join
--方式二
--根據輸入檔案大小判斷是否開啟map Join
-- 0.7.0增加引數,預設值為false,0.11.0及以後版本為true
set hive.auto.convert.join=true;
--方式三
-- 根據輸入檔案大小判斷是否開啟map join,如果小於hive.auto.convert.join.noconditionaltask.size值則開啟mapjoin,hive.auto.convert.join.noconditionaltask.size值預設10M;
-- 0.11.0版本增加引數,可以自動識別各種案例進行優化
set hive.auto.convert.join.noconditionaltask=true;
set hive.auto.convert.join.noconditionaltask.size=10000000;
目前hive推薦使用第三種方式智慧進行mapjoin配置,只有在輸入表為分桶表或者已排序且需要轉換成bucketized map-side join或者bucketized sort-merge join才需要進行map join hint
select /*+mapjoin(smallTableTwo)*/
idOne
, idTwo
, value
from (select /*+MAPJOIN(smallTableOne)*/
idOne
, idTwo
, value
from bigTable
join smallTableOne
on bigTable.idOne = smallTableOne.idOne) firstjoin
join smallTableTwo
on firstjoin.idTwo = smallTableTwo.idTwo
以上查詢語句不支援。如果沒有mapjoin,以上查詢會生成兩個map join,採用第三種方式設定則生成一個map join。
map join執行步驟:
-
本地工作:在本地機器讀入資料,在記憶體中建立hash表,然後寫入本地磁碟並上傳至hdfs。最後將hash表寫入分散式快取中
-
map任務:將資料從分散式快取中讀入記憶體,將表中資料逐一和hash表匹配。將匹配到資料進行合併並寫入hdfs中
-
無reduce任務
common join(reduce side join)
若兩種表都較大在map端無法快取進記憶體,此時只能在reduce端進行join
如果多張表中用同一列進行join操作,那麼hive會將其轉成一個map/redcuce job
-- 同一列進行join,轉成一個map reduce job
select a.val
, b.val
, c.val
from a
join b
on a.key = b.key1
join c ON c.key = b.key1;
-- b中關聯條件時分別用到 key1和key2,此時生成兩個 map/reduce job
select a.val
, b.val
, c.val
from a
join b
on a.key = b.key1
join c
on c.key = b.key2
在每一個join相關的map reduce任務中,reducer時最後一張表會作為流式(stream)處理,其它表會進行快取。所以將最大表放在最後可以減少需要的記憶體。
另外,可以通過hint指定流式表。如果hint被忽略,hive將最右邊的表作為流式表
select /*+ streamtable(a) */
a.val
, b.val
, c.val
from a
join b
on a.key = b.key1
join c
on c.key = b.key1
sort merge join
如果join表在join列上進行了分桶和排序,並且有相同的桶數,此時進行sort merge join操作。相關的桶會在mapper進行Join。需要進行如下設定開啟sort merge join
set hive.input.format=org.apache.hadoop.hive.ql.io.BucketizedHiveInputFormat;
set hive.optimize.bucketmapjoin = true;
set hive.optimize.bucketmapjoin.sortedmerge = true;
連線型別
hive 連線型別和oracle類似,寫法上稍有不同
左半連線
在Hive 0.13版本以前,不支援in、not in、exists、not exists子句。此時用left semi join實現in操作。下面兩種寫法是等價的
-- 寫法一
select a.key
, a.value
from a
where a.key in (select b.key
from b);
-- 寫法二
select a.key
, a.val
from a
left semi join b
on a.key = b.key
JOIN簡化
謂詞下推(Predicate Pushdown)
原理
對於left join,先給出如下定義:
-
保留行表:outer join中返回所有行的表,如left outer join中的左表,right outer join中的右表
-
空供應表:如果和主表沒有匹配上返回空值的表,如left outer join中的左表,right outer join中左表
-
join過程中謂詞:寫在join過程中的過濾條件
-
join後謂詞:寫在where條件中的過濾條件
存在如下反向規則:
-
Join過程中謂詞不能推移到保留行表
-
join後謂詞不能推移到空供應表
正向解釋下:join過程中的過濾條件可以推移到空供應表上,where 過程中的條件可以推移到保留行表。
例項
相信70%工程師都會寫出來如下的程式碼:
select t.emp_id
, t.emp_name
, t1.dept_name
from (select emp_id
, emp_name
, dept_id
from emp
where emp_id != 123
and dt = '20180618')t
left join (select dept_id
, dept_name
from dept
where dept != 'abc'
and dt = '20180618'
)t1
on t.dept_id = t1.dept_id
根據謂詞下推規則,等價程式碼如下:
select t.emp_id
, t.emp_name
, t1.dept_name
from emp t
left join dept t1
on t.dept_id = t1.dept_id
and t1.dept_id = 'abc'
and t1.dt = '20180618'
where t.emp_id = '123'
and t.dt = '20180618'
emp為保留行表,dept為空供應表。where中t.emp_id = '123' and dt = '20180618'條件推移到emp表上;join中t1.dept_id='abc' and t1.dt = '20180618'推移到dept表上。
以上兩種寫法等價,第一種寫法比較容易理解;第二種寫法非常簡潔,對於一個sql高手來說也可以用通俗易懂形容,個人推薦第二種寫法。
優化
可能很多書籍或者文章寫過如列裁剪,建索引等方法。這裡介紹的是根據筆者經驗,通過一個例子介紹思維和寫法上的優化。
-- 優化前
select order_id
, greast(t.consume_time,t1.consume_time) consume_time
from (select order_id
, min(consume_time) consume_time
from order_info
where dt = '${bizdate}'
group by order_id
)t
join (select order_id
, max(consume_time) consume_time
from order_info_his
where dt = '${bizdate}'
group by order_id
)t1
on t.order_id = t1.order_id;
-- 優化後
select order_id
, greast(min(if(flg = 1,t.consume_time,null)),max(if(flg =2,t2.consume_time,null))) consume_time
from (select 1 flg
, order_id
, consume_time consume_time
from order_info
where dt = '${bizdate}'
union all
select 2 flg
, order_id
, consume_time consume_time
from order_info_his
where dt = '${bizdate}'
)t
group by order_id
也許我們有多種方法到達彼岸,但是請駐足思考片刻,或許你會發現不一樣的奚徑