ClickHouse 高級(四)優化(4)查詢優化


1 單表查詢

1.1 Prewhere 替代 where
  Prewhere 和 where 語句的作用相同,用來過濾數據。不同之處在於 prewhere 只支持*MergeTree 族系列引擎的表,首先會讀取指定的列數據,來判斷數據過濾,等待數據過濾之后再讀取 select 聲明的列字段來補全其余屬性。
  當查詢列明顯多於篩選列時使用 Prewhere 可十倍提升查詢性能,Prewhere 會自動優化執行過濾階段的數據讀取方式,降低 io 操作。
  在某些場合下,prewhere 語句比 where 語句處理的數據量更少性能更高。
#關閉 where 自動轉 prewhere(默認情況下, where 條件會自動優化成 prewhere)
set optimize_move_to_prewhere=0; 
# 使用 where
select WatchID, 
 JavaEnable, 
 Title, 
 GoodEvent, 
 EventTime, 
 EventDate, 
 CounterID, 
 ClientIP, 
 ClientIP6, 
 RegionID, 
 UserID, 
 CounterClass, 
 OS, 
 UserAgent, 
 URL, 
 Referer, 
 URLDomain, 
 RefererDomain, 
 Refresh, 
 IsRobot, 
 RefererCategories, 
 URLCategories, 
 URLRegions, 
 RefererRegions, 
 ResolutionWidth, 
 ResolutionHeight, 
 ResolutionDepth, 
 FlashMajor, 
 FlashMinor, 
 FlashMinor2
from datasets.hits_v1 where UserID='3198390223272470366';

# 使用 prewhere 關鍵字
select WatchID, 
 JavaEnable,
 Title, 
 GoodEvent, 
 EventTime, 
 EventDate, 
 CounterID, 
 ClientIP, 
 ClientIP6, 
 RegionID, 
 UserID, 
 CounterClass, 
 OS, 
 UserAgent, 
 URL, 
 Referer, 
 URLDomain, 
 RefererDomain, 
 Refresh, 
 IsRobot, 
 RefererCategories, 
 URLCategories, 
 URLRegions, 
 RefererRegions, 
 ResolutionWidth, 
 ResolutionHeight, 
 ResolutionDepth, 
 FlashMajor, 
 FlashMinor, 
 FlashMinor2
from datasets.hits_v1 prewhere UserID='3198390223272470366';
默認情況,我們肯定不會關閉 where 自動優化成 prewhere,但是某些場景即使開啟優化,也不會自動轉換成 prewhere,需要手動指定 prewhere:
⚫使用常量表達式
⚫使用默認值為 alias 類型的字段
⚫包含了 arrayJOIN,globalIn,globalNotIn 或者 indexHint 的查詢
⚫select 查詢的列字段和 where 的謂詞相同
⚫使用了主鍵字段
1.2 數據采樣
通過采樣運算可極大提升數據分析的性能
SELECT Title,count(*) AS PageViews 
FROM hits_v1
SAMPLE 0.1 #代表采樣 10%的數據,也可以是具體的條數
WHERE CounterID =57
GROUP BY Title
ORDER BY PageViews DESC LIMIT 1000
采樣修飾符只有在 MergeTree engine 表中才有效,且在創建表時需要指定采樣策略。
1.3 列裁剪與分區裁剪
數據量太大時應避免使用 select * 操作,查詢的性能會與查詢的字段大小和數量成線性表換,字段越少,消耗的 io 資源越少,性能就會越高。
反例:
select * from datasets.hits_v1;
正例:
select WatchID, 
 JavaEnable, 
 Title, 
 GoodEvent, 
 EventTime, 
 EventDate, 
 CounterID, 
 ClientIP, 
 ClientIP6, 
 RegionID, 
 UserID
from datasets.hits_v1;
分區裁剪就是只讀取需要的分區,在過濾條件中指定。
select WatchID, 
 JavaEnable, 
 Title, 
 GoodEvent, 
 EventTime, 
 EventDate, 
 CounterID, 
 ClientIP, 
 ClientIP6, 
 RegionID, 
 UserID
from datasets.hits_v1
where EventDate='2014-03-23';
1.4 orderby 結合 where、limit
千萬以上數據集進行 order by 查詢時需要搭配 where 條件和 limit 語句一起使用。
#正例:
SELECT UserID,Age
FROM hits_v1 
WHERE CounterID=57
ORDER BY Age DESC LIMIT 1000
#反例:
SELECT UserID,Age
FROM hits_v1 
ORDER BY Age DESC
1.5 避免構建虛擬列
如非必須,不要在結果集上構建虛擬列,虛擬列非常消耗資源浪費性能,可以考慮在前
端進行處理,或者在表中構造實際字段進行額外存儲。
 
反例:
SELECT Income,Age,Income/Age as IncRate FROM datasets.hits_v1;
正例:拿到 Income 和 Age 后,考慮在前端進行處理,或者在表中構造實際字段進行額外存儲
SELECT Income,Age FROM datasets.hits_v1;
1.6 uniqCombined 替代 distinct
  性能可提升 10 倍以上,uniqCombined 底層采用類似 HyperLogLog 算法實現,能接收 2%左右的數據誤差,可直接使用這種去重方式提升查詢性能。Count(distinct )會使用 uniqExact
精確去重。
  不建議在千萬級不同數據上執行 distinct 去重查詢,改為近似去重 uniqCombined
反例:
select count(distinct rand()) from hits_v1;
正例:
SELECT uniqCombined(rand()) from datasets.hits_v1
1.7 使用物化視圖
參考第 6 章。
1.8 其他注意事項
(1)查詢熔斷
  為了避免因個別慢查詢引起的服務雪崩的問題,除了可以為單個查詢設置超時以外,還可以配置周期熔斷,在一個查詢周期內,如果用戶頻繁進行慢查詢操作超出規定閾值后將無法繼續進行查詢操作。
(2)關閉虛擬內存
  物理內存和虛擬內存的數據交換,會導致查詢變慢,資源允許的情況下關閉虛擬內存。
(3)配置 join_use_nulls
  為每一個賬戶添加 join_use_nulls 配置,左表中的一條記錄在右表中不存在,右表的相應字段會返回該字段相應數據類型的默認值,而不是標准 SQL 中的 Null 值。
(4)批量寫入時先排序
  批量寫入數據時,必須控制每個批次的數據中涉及到的分區的數量,在寫入之前最好對需要導入的數據進行排序。無序的數據或者涉及的分區太多,會導致 ClickHouse 無法及時對新導入的數據進行合並,從而影響查詢性能。
(5)關注 CPU
  cpu 一般在 50%左右會出現查詢波動,達到 70%會出現大范圍的查詢超時,cpu 是最關鍵的指標,要非常關注。

2 多表關聯

2.1 准備表和數據
#創建小表
CREATE TABLE visits_v2 
ENGINE = CollapsingMergeTree(Sign)
PARTITION BY toYYYYMM(StartDate)
ORDER BY (CounterID, StartDate, intHash32(UserID), VisitID)
SAMPLE BY intHash32(UserID)
SETTINGS index_granularity = 8192
as select * from visits_v1 limit 10000;

#創建 join 結果表:避免控制台瘋狂打印數據
CREATE TABLE hits_v2 
ENGINE = MergeTree()
PARTITION BY toYYYYMM(EventDate)
ORDER BY (CounterID, EventDate, intHash32(UserID))
SAMPLE BY intHash32(UserID)
SETTINGS index_granularity = 8192
as select * from hits_v1 where 1=0;
2.2 用 IN 代替 JOIN
當多表聯查時,查詢的數據僅從其中一張表出時,可考慮用 IN 操作而不是 JOIN
insert into hits_v2
select a.* from hits_v1 a where a. CounterID in (select CounterID from 
visits_v1); 

#反例:使用 join
insert into table hits_v2
select a.* from hits_v1 a left join visits_v1 b on a. CounterID=b. 
CounterID;
2.3 大小表 JOIN
  多表 join 時要滿足小表在右的原則,右表關聯時被加載到內存中與左表進行比較,ClickHouse 中無論是 Left join 、Right join 還是 Inner join 永遠都是拿着右表中的每一條記錄到左表中查找該記錄是否存在,所以右表必須是小表。
(1)小表在右
insert into table hits_v2
select a.* from hits_v1 a left join visits_v2 b on a. CounterID=b. 
CounterID;
(2)大表在右
insert into table hits_v2
select a.* from visits_v2 b left join hits_v1 a on a. CounterID=b. 
CounterID;
2.4 注意謂詞下推(版本差異)
ClickHouse 在 join 查詢時不會主動發起謂詞下推的操作,需要每個子查詢提前完成過濾操作,需要注意的是,是否執行謂詞下推,對性能影響差別很大(新版本中已經不存在此問題,但是需要注意謂詞的位置的不同依然有性能的差異)
Explain syntax
select a.* from hits_v1 a left join visits_v2 b on a. CounterID=b. 
CounterID
having a.EventDate = '2014-03-17';

Explain syntax
select a.* from hits_v1 a left join visits_v2 b on a. CounterID=b. 
CounterID
having b.StartDate = '2014-03-17';

insert into hits_v2
select a.* from hits_v1 a left join visits_v2 b on a. CounterID=b. 
CounterID
where a.EventDate = '2014-03-17';

insert into hits_v2
select a.* from (
 select * from 
 hits_v1 
 where EventDate = '2014-03-17'
) a left join visits_v2 b on a. CounterID=b. CounterID;
2.5 分布式表使用 GLOBAL
  兩張分布式表上的 IN 和 JOIN 之前必須加上 GLOBAL 關鍵字,右表只會在接收查詢請求的那個節點查詢一次,並將其分發到其他節點上。如果不加 GLOBAL 關鍵字的話,每個節點都會單獨發起一次對右表的查詢,而右表又是分布式表,就導致右表一共會被查詢 N²次(N是該分布式表的分片數量),這就是查詢放大,會帶來很大開銷。
2.6 使用字典表
  將一些需要關聯分析的業務創建成字典表進行 join 操作,前提是字典表不宜太大,因為字典表會常駐內存
2.7 提前過濾
  通過增加邏輯過濾可以減少數據掃描,達到提高執行速度及降低內存消耗的目的

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM