MaxCompute笛卡爾積邏輯的參數優化&複雜JOIN邏輯優化

簡介: 這篇文章主要講一個SQL優化反映的兩個優化點。分別是: 一、笛卡爾積邏輯的參數優化。 二、一個複雜JOIN邏輯的優化思路。

1.  優化概述

最近協助一個項目做下優化任務的工作。因爲主要數據都是報表,對數對的昏天暗地的不敢隨便調整SQL邏輯,所以本身只想做點參數調整,但是逼不得已後來還是改了一下SQL。

這篇文章主要講一個SQL優化反映的兩個優化點。分別是:

一、笛卡爾積邏輯的參數優化。

二、一個複雜JOIN邏輯的優化思路。

2.  笛卡爾積邏輯參數優化

早期版本MaxCompute(ODPS)好像對笛卡爾積的支持並不友好,對笛卡爾積邏輯檢測很嚴格,但是現在的版本就比較正常了,在項目裏最近這段時間遇到了2個性能問題都是這種笛卡爾積導致的。

笛卡爾積會造成數據量的膨脹,如果是一些數據量是幾千幾萬的小表的一些關聯,好像大家也體會不到什麼性能問題,不過如果某個表是幾千萬、幾億這個規模就可能導致單個數據處理的WORKER超出其預估,導致SQL運行時間超出預期。

2.1. 發現問題

一般對於特別去調優的場景,其實不好好看看輸入輸出的數據量,看看邏輯是不太好發現笛卡爾積的邏輯的。所以,一般是先從執行日誌的問題着手。

x.png

如上圖所示,很明顯JOIN階段有數據傾斜。J4_1_3_6_job_0階段,10分鐘只是執行了2個backups(我沒有截全,其實時間更久)。

2.2. 對付傾斜的暴力參數方法

傾斜的核心邏輯是有個別WORKER處理的數據比其他WORKER要多數倍,並且超過了1個WORKER處理能力太多。一般我們的WORKER處理數據的時間在秒級是最常見的,但是到幾分鐘這種程度的傾斜一般也能接受。畢竟不傾斜的數據真實情況下很難遇到,但是有backups的場景就說明超出了這個WORKER的處理能力,這個程序居然跑掛了。然後又啓動了一個WORKER,繼續跑。

那這種情況,不管這個數據到底怎麼傾斜,爲什麼傾斜,最簡單暴力的方法就是:

1-把單個WORKER處理的數據量變小;

-- 讓map階段的worker變多【默認256,值域1-最大值】

SET odps.sql.mapper.split.size = 16;

-- 讓reduce階段的worker變多【默認-1,動態;最大1111,手動最大9999】

SET odps.sql.reducer.instances = 99;

-- 讓join階段的worker變多【默認-1,動態;最大1111,手動最大9999】

SET odps.sql.joiner.instances = 200;

      注意:一個worker至少佔用1core的CPU,如果集羣不夠大,或者分配給項目的資源有限,都會讓執行的WORKER排隊執行。再有,如果本來1個WORKER就能很快執行完的數據,你拆分成很多份,反而會讓處理步驟更多,導致處理時間更慢。線上任務資源緊張的時候,要獲取很多WORKER執行也會困難,導致長期的等待足夠多的資源釋放。所以,不是越大越好,最好是牛刀小試,淺嘗即止。

2-讓單個WORKER擁有更多的計算資源;

-- 讓map階段的CPU變多【默認100,值域50-800,官方認爲沒必要調整】

SET odps.sql.mapper.cpu = 800;

-- 讓map階段的內存變多【默認1024,值域256-12288,少量場景可調整】

SET odps.sql.mapper.memory = 12288;

-- 讓reduce階段的CPU變多【默認100,值域50-800,官方認爲沒必要調整】

SET odps.sql.reducer.cpu = 800;

-- 讓reduce階段的內存變多【默認1024,值域256-12288,少量場景可調整】

SET odps.sql.reducer.memory = 12288;

-- 讓join階段的CPU變多【默認100,值域50-800,官方認爲沒必要調整】

SET odps.sql.joiner.cpu = 800;

-- 讓join階段的內存變多【默認1024,值域256-12288,少量場景可調整】

SET odps.sql.joiner.memory = 12288;

注意:一個worker至少佔用1core的CPU,1G內存。可以增加CPU和內存,讓單個worker的資源更多,一方面不會因爲資源不足掛了,另外一方面也可以提升運算速度。但是實際上一般CPU資源在集羣中更爲緊張,內存相對富裕。啓動一個worker需要8覈資源,啓動10個worker就要80個,在資源不足的時候,只需要1核的worker可能獲取資源就跑起來了,這個需要資源多的還需要等待,所以,不是越大越好,也不是越多越好。這些參數,都是儘量別用,只有你發現具體問題去解決的時候再斟酌使用。

在一些場景,這個暴力手段還是很好用,如果能解決問題又不影響其他任務運行,用用也不錯。尤其是我們需要立即解決問題的時候,簡單粗暴很正確。

3.   一個複雜JOIN邏輯的優化思路

一般JOIN邏輯都是很清晰的,但是有的時候開發同學會沒有把握,那麼最好的方法就是驗證一下。

今天就遇到了一個特殊的關聯邏輯:不等關聯。

3.1. 理解原始的邏輯

原始腳本內容示例如下:

with ta as(

select a,b from values ('12', 2), ('22', 2), ('23', 4), ('34', 7), ('35', 2), ('46', 4), ('47', 7) t(a, b))

select x.a,sum(t.b) as b

FROM (select distinct a from ta) x

left outer join ta t

ON SUBSTR(x.a, 1, 1) =  SUBSTR(t.a, 1, 1)

AND x.a >= t.a

group by x.a

;

   a   b

1   12  2

2   22  2

3   23  6

4   34  7

5   35  9

6   46  4

7   47  11

上面這段腳本其實只用到了一個表,字段a是按月報表的 統計日期,截取了一段“年”關聯,並且關聯限制了關聯的統計月份不能大於當前月。

其實看到這個寫法,我映入腦海的就是計算累計值。我原以爲maxcompute有這個累計函數,不過確實沒找到,不過窗口函數也可以計算。邏輯如下:

with ta as(

select a,b from values ('12', 2), ('22', 2), ('23', 4), ('34', 7), ('35', 2), ('46', 4), ('47', 7) t(a, b))

select t.a,sum(t.b) over (partition by SUBSTR(t.a, 1, 1) order by t.a)as b

FROM ta t

;

   a   b

1   12  2

2   22  2

3   23  6

4   34  7

5   35  9

6   46  4

7   47  11

通過上面這段SQL,我們發現用窗口計算累計值是可以替代原來的JOIN邏輯的。因爲上面我們看執行計劃,發現SQL性能瓶頸出現在JOIN階段,如果沒有JOIN,自然沒有這個問題了。

但是仔細查看了原始腳本的產出後,發現數據量是膨脹的,也就是說JOIN後的記錄數比原始表多了。這主要因爲腳本保留了JOIN的兩個表的不等關聯關聯字段,形成了【2022年2月關聯2022年1月】、【2022年2月關聯2022年2月】,原來2月就一行現在變成兩行。這個時候窗口函數沒辦法實現數據量變多的邏輯,只能老老實實用JOIN。

3.2. 進一步分析原始邏輯

諮詢分析後發現其實邏輯如下(去掉了聚合示意):

with ta as(

select a,b from values ('12', 2), ('22', 2), ('23', 4), ('34', 7), ('35', 2), ('46', 4), ('47', 7) t(a, b))

select t.*,x.*

FROM (select distinct a from ta) x

left outer join ta t

ON SUBSTR(x.a, 1, 1) =  SUBSTR(t.a, 1, 1)

AND x.a >= t.a;

   a   b   a2

1   12  2   12

2   22  2   22

3   22  2   23

4   23  4   23

5   34  7   34

6   34  7   35

7   35  2   35

8   46  4   46

9   46  4   47

10  47  7   47

上面的邏輯我們可以看到,JOIN的兩張表其實是一張表產出,關聯字段是原始明細表group by日期(字段a)獲取。所以,兩個表沒必要寫成左連接,內連接也是可以的。因爲如果左連接,就沒辦法用mapjoin小表的方法跳過JOIN步驟,但是內連接就可以了。邏輯如下:

with ta as(

select a,b from values ('12', 2), ('22', 2), ('23', 4), ('34', 7), ('35', 2), ('46', 4), ('47', 7) t(a, b))

select /*+mapjoin(x)*/t.*,x.*

FROM ta t

join (select distinct a from ta) x

ON SUBSTR(x.a, 1, 1) =  SUBSTR(t.a, 1, 1)

AND x.a >= t.a;

   a   b   a2

1   12  2   12

2   22  2   22

3   22  2   23

4   23  4   23

5   34  7   34

6   34  7   35

7   35  2   35

8   46  4   46

9   46  4   47

10  47  7   47

如上我們可以看到數據結果都是一樣的,所以這兩個JOIN邏輯是可以替代的。因爲考慮到原始腳本的複雜性,我還是拿原始腳本修改做了數據比對測試,結果也是全部記錄的字段值都是一樣的。(原型只是一個方向,具體場景一定要做足驗證測試)因爲原始腳本還是比較複雜,我就不展示了,我把全字段比對兩種寫法的SQL寫一下,方便大家測試使用。

select count(*) from (

select 1

from ta a

join tb b

on 1=1

and coalesce(a.col1,'')= coalesce(b.col1,'')

and coalesce(a.col2,'')= coalesce(b.col2,'')

......

and coalesce(a.coln,0 )= coalesce(b.coln,0)  

)t

;

     注意:全字段比對關聯,返回的記錄數要與單表記錄數一致。

4.  長腳本優化其他要注意的點

4.1. 一次處理多次引用

實際上這個原始腳本很長,差不多1000行,每一段SQL運行時間都不算長。即便是上面提到的笛卡爾積的場景,也才十幾分鍾就跑完了,但是整體運行時間超過了1小時40分鐘。裏面的SQL寫了十多段,簡直是手工串行執行任務。

但是很快我就發現一個問題,腳本里面用了很多次insert into,說明多段SQL其實可以使用union all一次寫入的。尤其是我看到很多段SQL的使用的表和表的關聯邏輯都是一樣的,就像我在上面優化的那段SQL,連着好幾段SQL都是表和關聯邏輯完全一樣。這種時候,如果寫成union all,map階段和join階段其實只需要做一次,寫了4遍就需要做四次。對於一段就要跑十幾分鐘的腳本,直接就讓本來只需要十幾分鍾就跑完的邏輯運行時間翻了4倍。在這個腳本中,這樣的地方有2大段,涉及很多小段。

--建立測試表

create table tmp_mj_ta as

select a,b from values ('12', 2), ('22', 2), ('23', 4), ('34', 7), ('35', 2), ('46', 4), ('47', 7) t(a, b);

--建立寫入表

drop table if exists tmp_mj_01;

create table tmp_mj_01 (xa string,ta string,tb bigint,tc string);

-- 1-多段insert into寫入,關聯邏輯一樣,每一段都有map階段和Join階段

insert overwrite table tmp_mj_01

select /*+mapjoin(x)*/x.a,t.a,t.b,'01' as c

FROM tmp_mj_ta t

join (select distinct a from tmp_mj_ta) x

ON SUBSTR(x.a, 1, 1) =  SUBSTR(t.a, 1, 1)

AND x.a >= t.a;

insert into table tmp_mj_01

select /*+mapjoin(x)*/x.a,t.a,t.b,'02' as c

FROM tmp_mj_ta t

join (select distinct a from tmp_mj_ta) x

ON SUBSTR(x.a, 1, 1) =  SUBSTR(t.a, 1, 1)

AND x.a >= t.a;

insert into table tmp_mj_01

select /*+mapjoin(x)*/x.a,t.a,t.b,'03' as c

FROM tmp_mj_ta t

join (select distinct a from tmp_mj_ta) x

ON SUBSTR(x.a, 1, 1) =  SUBSTR(t.a, 1, 1)

AND x.a >= t.a;

-- 2-改爲union all寫入,關聯邏輯一樣,只有一個map階段和Join階段

insert overwrite table tmp_mj_01

select /*+mapjoin(x)*/x.a,t.a,t.b,'01' as c

FROM tmp_mj_ta t

join (select distinct a from tmp_mj_ta) x

ON SUBSTR(x.a, 1, 1) =  SUBSTR(t.a, 1, 1)

AND x.a >= t.a

union all

select /*+mapjoin(x)*/x.a,t.a,t.b,'02' as c

FROM tmp_mj_ta t

join (select distinct a from tmp_mj_ta) x

ON SUBSTR(x.a, 1, 1) =  SUBSTR(t.a, 1, 1)

AND x.a >= t.a

union all

select /*+mapjoin(x)*/x.a,t.a,t.b,'03' as c

FROM tmp_mj_ta t

join (select distinct a from tmp_mj_ta) x

ON SUBSTR(x.a, 1, 1) =  SUBSTR(t.a, 1, 1)

AND x.a >= t.a;

還有上面處理邏輯中的(select distinct a from ta)邏輯,出現很多次。這種時候,可以使用with表達式來寫在最前面一次,多個union步驟都可以引用,不但代碼整潔可讀性強,也不容易出錯。

--更爲簡潔的寫法with表達式

-- 改爲union all寫入,關聯邏輯一樣,只有一個map階段和Join階段

with tmp_mj_tx as (

select distinct a from tmp_mj_ta

)

insert overwrite table tmp_mj_01

select /*+mapjoin(x)*/x.a,t.a,t.b,'01' as c

FROM tmp_mj_ta t

join tmp_mj_tx x

ON SUBSTR(x.a, 1, 1) =  SUBSTR(t.a, 1, 1)

AND x.a >= t.a

union all

select /*+mapjoin(x)*/x.a,t.a,t.b,'02' as c

FROM tmp_mj_ta t

join tmp_mj_tx x

ON SUBSTR(x.a, 1, 1) =  SUBSTR(t.a, 1, 1)

AND x.a >= t.a

union all

select /*+mapjoin(x)*/x.a,t.a,t.b,'03' as c

FROM tmp_mj_ta t

join tmp_mj_tx x

ON SUBSTR(x.a, 1, 1) =  SUBSTR(t.a, 1, 1)

AND x.a >= t.a;

4.2. 檢查自動mapjoin小表是否生效

MaxCompute(ODPS)的最新版本是支持自動識別小表,執行MAPJOIN的。但是天有不測風雲,這個腳本中剛好有一段SQL的這個自動沒生效,導致這段邏輯運行了幾十分鐘。

在查看日誌的時候,如果遇到這種問題,後面一定要顯示的在腳本中加上mapjoin提示。如果不放心,建議開發同學主動把小表都寫到mapjoin提示裏面。小心使得萬年船,謹慎一點也不錯。

image.png

 原文鏈接:https://click.aliyun.com/m/1000361016/
 
本文爲阿里雲原創內容,未經允許不得轉載。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章