本文介紹有關SQL性能相關的常見問題。
實時計算Flink版如何拆分SQL作業節點?
在
頁面,單擊目標作業名稱,在部署詳情頁簽的運行參數配置區域的其他配置中,添加如下代碼后保存生效。pipeline.operator-chaining: 'false'
Group Aggregate優化技巧有哪些?
開啟MiniBatch(提升吞吐)
MiniBatch是緩存一定的數據后再觸發處理,以減少對State的訪問,從而提升吞吐并減少數據的輸出量。
MiniBatch主要基于事件消息來觸發微批處理,事件消息會按您指定的時間間隔在源頭插入。
適用場景
微批處理通過增加延遲換取高吞吐,如果您有超低延遲的要求,不建議開啟微批處理。通常對于聚合場景,微批處理可以顯著地提升系統性能,建議開啟。
開啟方式
MiniBatch默認關閉,您需要在目標作業的部署詳情頁簽,運行參數配置區域的其他配置中,填寫以下代碼。
table.exec.mini-batch.enabled: true table.exec.mini-batch.allow-latency: 5s
參數解釋如下表所示。
參數
說明
table.exec.mini-batch.enabled
是否開啟mini-batch。
table.exec.mini-batch.allow-latency
批量輸出數據的時間間隔。
開啟LocalGlobal(解決常見數據熱點問題)
LocalGlobal本質上能夠靠LocalAgg的聚合篩除部分傾斜數據,從而降低GlobalAgg的熱點,提升性能。
LocalGlobal優化將原先的Aggregate分成Local和Global兩階段聚合,即MapReduce模型中的Combine和Reduce兩階段處理模式。第一階段在上游節點本地攢一批數據進行聚合(localAgg),并輸出這次微批的增量值(Accumulator)。第二階段再將收到的Accumulator合并(Merge),得到最終的結果(GlobalAgg)。
適用場景
提升普通聚合(例如SUM、COUNT、MAX、MIN和AVG)的性能,以及這些場景下的數據熱點問題。
使用限制
LocalGlobal是默認開啟的,但是有以下限制:
在minibatch開啟的前提下才能生效。
需要使用AggregateFunction實現Merge。
判斷是否生效
觀察最終生成的拓撲圖的節點名字中是否包含GlobalGroupAggregate或LocalGroupAggregate。
開啟PartialFinal(解決COUNT DISTINCT熱點問題)
為了解決COUNT DISTINCT的熱點問題,通常需要手動改寫為兩層聚合(增加按Distinct Key取模的打散層)。目前,實時計算提供了COUNT DISTINCT自動打散,即PartialFinal優化,您無需自行改寫為兩層聚合。
LocalGlobal優化針對普通聚合(例如SUM、COUNT、MAX、MIN和AVG)有較好的效果,對于COUNT DISTINCT收效不明顯,因為COUNT DISTINCT在Local聚合時,對于DISTINCT KEY的去重率不高,導致在Global節點仍然存在熱點問題。
適用場景
使用COUNT DISTINCT,但無法滿足聚合節點性能要求。
重要不能在包含UDAF的Flink SQL中使用PartialFinal優化方法。
數據量較少的情況,不建議使用PartialFinal優化方法,浪費資源。因為PartialFinal優化會自動打散成兩層聚合,引入額外的網絡Shuffle。
開啟方式
默認不開啟。如果您需要開啟,則需要在目標作業的部署詳情頁簽,運行參數配置區域的其他配置中,填寫以下代碼。
table.optimizer.distinct-agg.split.enabled: true
判斷是否生效
觀察最終生成的拓撲圖,是否由原來一層的聚合變成了兩層的聚合。
AGG WITH CASE WHEN改寫為AGG WITH FILTER語法(提升大量COUNT DISTINCT場景性能)
統計作業需要計算各種維度的UV,例如全網UV、來自手機客戶端的UV、來自PC的UV等等。建議使用標準的AGG WITH FILTER語法來代替CASE WHEN實現多維度統計的功能。實時計算目前的SQL優化器能分析出Filter參數,從而同一個字段上計算不同條件下的COUNT DISTINCT能共享State,減少對State的讀寫操作。性能測試中,使用AGG WITH FILTER語法來代替CASE WHEN能夠使性能提升1倍。
適用場景
對于同一個字段上計算不同條件下的COUNT DISTINCT結果的場景,性能提升很大。
原始寫法
COUNT(distinct visitor_id) as UV1 , COUNT(distinct case when is_wireless='y' then visitor_id else null end) as UV2
優化寫法
COUNT(distinct visitor_id) as UV1 , COUNT(distinct visitor_id) filter (where is_wireless='y') as UV2
TopN優化技巧有哪些?
TopN算法
當TopN的輸入是非更新流(例如SLS數據源),TopN只有1種算法AppendRank。當TopN的輸入是更新流時(例如經過了AGG或JOIN計算),TopN有2種算法,性能從高到低分別是:UpdateFastRank和RetractRank。算法名字會顯示在拓撲圖的節點名字上。
AppendRank:對于非更新流,只支持該算法。
UpdateFastRank:對于更新流,最優算法。
RetractRank:對于更新流,保底算法。性能不佳,在某些業務場景下可優化成UpdateFastRank。
下面介紹RetractRank如何能優化成UpdateFastRank。使用UpdateFastRank算法需要具備3個條件:
輸入流為更新流。
輸入流有Primary Key信息,例如上游做了GROUP BY聚合操作。
排序字段的更新是單調的,且單調方向與排序方向相反。例如,ORDER BY COUNT/COUNT_DISTINCT/SUM(正數)DESC。
如果您要獲取到UpdateFastRank的優化Plan,則您需要在使用ORDER BY SUM DESC時,添加SUM為正數的過濾條件,確保total_fee為正數。
insert into print_test SELECT cate_id, seller_id, stat_date, pay_ord_amt --不輸出rownum字段,能減小結果表的輸出量。 FROM ( SELECT *, ROW_NUMBER () OVER ( PARTITION BY cate_id, stat_date --注意要有時間字段,否則State過期會導致數據錯亂。 ORDER BY pay_ord_amt DESC ) as rownum --根據上游sum結果排序。 FROM ( SELECT cate_id, seller_id, stat_date, --重點。聲明Sum的參數都是正數,所以Sum的結果是單調遞增的,因此TopN能使用優化算法,只獲取前100個數據。 sum (total_fee) filter ( where total_fee >= 0 ) as pay_ord_amt FROM random_test WHERE total_fee >= 0 GROUP BY cate_name, seller_id, stat_date, cate_id ) a ) WHERE rownum <= 100;
TopN優化方法
無排名優化
TopN的輸出結果不需要顯示rownum值,僅需在最終前端顯示時進行1次排序,極大地減少輸入結果表的數據量。無排名優化方法詳情請參見Top-N。
增加TopN的Cache大小
TopN為了提升性能有一個State Cache層,Cache層能提升對State的訪問效率。TopN的Cache命中率的計算公式如下。
cache_hit = cache_size*parallelism/top_n/partition_key_num
例如,Top100配置緩存10000條,并發50,當您的PatitionBy的Key維度較大時,例如10萬級別時,Cache命中率只有10000*50/100/100000=5%,命中率會很低,導致大量的請求都會擊中State(磁盤),觀察state seek metric可能會有很多毛刺。性能會大幅下降。
因此當partitionKey維度特別大時,可以適當加大TopN的cache size,相對應的也建議適當加大TopN節點的heap memory,詳情請參見配置作業部署信息。
table.exec.rank.topn-cache-size: 200000
默認10000條,調整TopN cache到200000,那么理論命中率能達到200000*50/100/100000 = 100%。
PartitionBy的字段中要有時間類字段
例如每天的排名,要帶上Day字段,否則TopN的最終結果會由于State TTL產生錯亂。
有哪些高效去重方案?
實時計算的源數據在部分場景中存在重復數據,去重成為了用戶經常反饋的需求。實時計算有保留第一條(Deduplicate Keep FirstRow)和保留最后一條(Deduplicate Keep LastRow)2種去重方案。
語法
由于SQL上沒有直接支持去重的語法,還要靈活地保留第一條或保留最后一條。因此我們使用了SQL的ROW_NUMBER OVER WINDOW功能來實現去重語法。去重本質上是一種特殊的TopN。
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY col1[, col2..] ORDER BY timeAttributeCol [asc|desc]) AS rownum FROM table_name) WHERE rownum = 1
參數
說明
ROW_NUMBER()
計算行號的OVER窗口函數。行號從1開始計算。
PARTITION BY col1[, col2..]
可選。指定分區的列,即去重的KEYS。
ORDER BY timeAttributeCol [asc|desc])
指定排序的列,必須是一個時間屬性的字段(即Proctime或Rowtime)。可以指定順序(Keep FirstRow)或者倒序 (Keep LastRow)。
rownum
僅支持
rownum=1
或rownum<=1
。如上語法所示,去重需要兩層Query:
使用
ROW_NUMBER()
窗口函數來對數據根據時間屬性列進行排序并標上排名。當排序字段是Proctime列時,Flink就會按照系統時間去重,其每次運行的結果是不確定的。
當排序字段是Rowtime列時,Flink就會按照業務時間去重,其每次運行的結果是確定的。
對排名進行過濾,只取第一條,達到了去重的目的。
排序方向可以是按照時間列的順序,也可以是倒序:
Deduplicate Keep FirstRow:順序并取第一條行數據。
Deduplicate Keep LastRow:倒序并取第一條行數據。
Deduplicate Keep FirstRow
保留首行的去重策略:保留KEY下第一條出現的數據,之后出現該KEY下的數據會被丟棄掉。因為STATE中只存儲了KEY數據,所以性能較優,示例如下。
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY b ORDER BY proctime) as rowNum FROM T ) WHERE rowNum = 1
以上示例是將T表按照b字段進行去重,并按照系統時間保留第一條數據。proctime在這里是源表T中的一個具有Processing Time屬性的字段。如果您按照系統時間去重,也可以將proctime字段簡化proctime()函數調用,可以省略proctime字段的聲明。
Deduplicate Keep LastRow
保留末行的去重策略:保留KEY下最后一條出現的數據。保留末行的去重策略性能略優于LAST_VALUE函數,示例如下。
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY b, d ORDER BY rowtime DESC) as rowNum FROM T ) WHERE rowNum = 1
以上示例是將T表按照b和d字段進行去重,并按照業務時間保留最后一條數據。rowtime在這里是源表T中的一個具有Event Time屬性的字段。
在使用內置函數時,需要注意什么?
使用內置函數替換自定義函數
實時計算的內置函數在持續的優化當中,請盡量使用內置函數替換自定義函數。實時計算對內置函數主要進行了如下優化:
優化數據序列化和反序列化的耗時。
新增直接對字節單位進行操作的功能。
KEY VALUE函數使用單字符的分隔符
KEY VALUE的簽名:
KEYVALUE(content, keyValueSplit, keySplit, keyName)
,當keyValueSplit和KeySplit是單字符,例如,冒號(:)、逗號(,)時,系統會使用優化算法,在二進制數據上直接尋找所需的keyName值,而不會將整個content進行切分,性能約提升30%。LIKE操作注意事項
如果需要進行StartWith操作,使用
LIKE 'xxx%'
。如果需要進行EndWith操作,使用
LIKE '%xxx'
。如果需要進行Contains操作,使用
LIKE '%xxx%'
。如果需要進行Equals操作,使用
LIKE 'xxx'
,等價于str = 'xxx'
。如果需要匹配下劃線(_),請注意要完成轉義
LIKE '%seller/_id%' ESCAPE '/'
。下劃線(_)在SQL中屬于單字符通配符,能匹配任何字符。如果聲明為LIKE '%seller_id%'
,則不單會匹配seller_id
,還會匹配seller#id
、sellerxid
或seller1id
等,導致結果錯誤。
慎用正則函數(REGEXP)
正則表達式是非常耗時的操作,對比加減乘除通常有百倍的性能開銷,而且正則表達式在某些極端情況下可能會進入無限循環,導致作業阻塞,具體情況請參見Regex execution is too slow,因此建議使用LIKE。正則函數包括: