搞點多元分析,糙快猛的解決方案就是使用rolap(關系型olap)了。資料經次元模組化後存儲在mysql,rolap引擎(比如開源的mondrian)負責将olap請求轉化為sql語句送出給資料庫。olap計算分析功能導緻mysql需要進行較多複雜sql查詢,性能調優必不可少,本文總結了一些實用原則。
olap的典型應用包括複雜動态報表,需要支援鑽取(上卷和下鑽)、切片、切塊和旋轉操作。下表總結了olap和oltp系統的主要差別。olap的特點決定了sql的查詢場景和優化方案,下文将從索引、聚合、子查詢、表連接配接和pivoting等幾個方面分别介紹。
olap
oltp
使用者量
分析人員使用者量相對小
高并發
資料庫設計
次元模型:星型、雪花型号
規範化
資料量
大,動辄千萬級别
小,一般不超過百萬級别
sql讀寫場景
定期導入,一般無更新,複雜查詢每次檢索大量資料
以事務為機關每次讀寫少量資料
在權衡資料容錯恢複和性能之後,存儲引擎選擇的是innodb。innodb索引的特性是主鍵聚集索引和b+tree資料結構。利用這兩個特性,能夠提升資料導入和多元度組合切片的性能。
1) 資料導入速度
下圖為innodb表主鍵索引示意圖,聚集索引使表中所有資料必須按照主鍵順序存儲在主鍵索引葉子節點上。如果不按照主鍵順序導入資料,會導緻額外的分頁、資料查找、移動io操作,這樣,innodb表的插入速度嚴重依賴于插入順序。解決方法比較簡單:主鍵使用auto_increment列。
2) 多元度切片
多元度組合查詢、分組和彙總操作非常常見,那麼在多個次元字段上添加複合索引是必不可少的,而複合索引的字段選擇和順序尤為重要。
誰排no.1?一般遵循以下原則:
a) mysql隻進行索引最左字首比對,可以選擇最常查詢的字段排首位。特殊情況:如果少量查詢場景不存在該字段怎麼處理?需要另外再建索引嗎?假設在盤古系統中,營運機關一般會出現在所有查詢中,是以會建立[營運機關,行業,産品線……]的複合索引,但某些進階别管理人員的查詢語句中,不包含營運機關,那麼需要再建立[行業,産品線……]的複合索引嗎?答案是看情況,提供小技巧:應用層處理,在不包括營運機關條件的查詢sql中加入“營運機關 in(所有營運機關)”條件
b) 最佳性能優化原則決定索引區分度最大的字段排首位(可用count(distinct column)/count(*)計算)
還有個大家往往會忽略的問題,誰排最後呢?答案是:将可能存在範圍條件檢索的字段放最後。來個案例
假設建立的複合索引為[avg_cms_weekly,trade_id, ,balance],那麼由于在avg_csm_weekly上存在範圍條件,mysql不會使用剩餘的索引。
mysql不支援hash聚合,僅支援流聚合。流聚合會先根據group by的字段進行排序,然後流式通路排序好的資料,進行分組聚合。如果在explain的extra列中看到using temporary和using filesort,說明聚合使用了臨時表和檔案排序操作,這可能導緻性能低下。最佳優化目标是讓聚合操作使用covering index,即完全不用查詢表資料,隻在索引上完成聚合查詢。
下面查詢語句會使用複合索引 [trade_id,product_line_id]
觀察查詢計劃,在extra列顯示using index,說明該操作為covering
index查詢。
在olap分析中,時間範圍上的聚合操作非常普遍。下面以賬号每日消費表為示例,總結幾種常見的時間聚合查詢模闆
account_id(賬戶)
stdate(資料日期)
click_pay(點選消費)
1
2013-08-01
100
2013-08-02
150
2
125
1)累計聚合
傳回賬戶加入某度以來累計消費和平均值。
2)滑動累計
傳回賬戶固定視窗時間内累計消費和平均值
3)mtd累計
傳回賬戶月初以來累計消費和平均值
再探讨下rollup和cube。假設使用者需要對n個次元進行聚合操作,需要進行n次group by再将結果進行union,而使用rollup可以一次查詢出n次group by 操作的結果。下面的兩條語句查詢結果一緻,執行計劃上卻不同,前者隻需要掃描一次,後者則需要掃描表四次。
語句1:
語句2:
與rollup隻在同一層次上對次元進行彙總不同,cube對所有次元進行彙總,n個次元cube需要2的n次方分組操作。目前版本的mysql還不支援cube操作,但和用多個group操作union模拟rollup同理,也可以用多個rollup操作union模拟cube。
複雜的需求場景導緻某些子查詢場景不可避免。關于子查詢,存在不少性能陷阱和認識誤區值得關注。
1)mysql子查詢性能差的主要原因是子查詢産生臨時表嗎?不完全正确,臨時表并不可怕,一個完整的sql語句,from/join/group/where/order等操作,不考慮索引優化的情況下,都有可能産生臨時表。是以更嚴格的表述是在子查詢産生的臨時表上查詢無法利用索引導緻性能低下。
2)in子查詢往往性能不佳的真實原因是什麼?是in查詢的臨時表資料量太大,mysql太弱,隻能支援極少數量的in子查詢嗎?不一定,顯示清單in(a,b,c)查詢的性能并不算差,in子查詢真正的性能陷阱在于mysql優化器往往将in獨立子查詢優化成exists相關子查詢!是以當觀察select * from table1 where table1.id in(select id from table2)的查詢計劃,會發現table2的查詢為depedentsubquery,原因其實是mysql優化政策+曆史原因。
3)子查詢的性能一定弱于join嗎?未必,由于mysql不支援semi join(注),是以在某些需要場景下,使用子查詢性能優于join。比如a表和b表一對多關系,如果僅僅想查詢在b表中存在對應記錄的a表記錄,如果使用join,需要用distinct或者group操作進行去重操作。使用關聯子查詢可以避免這部分開銷。select id from table1 where exists(select table2.id from table2where table2.id=table1.id)
關于join,mysql使用nested loop算法(注)。在典型的星型次元模型中,次元表資料量遠小于事實表,join操作往往是大小表連接配接,性能問題不大,這方面不多講。結合前面提到的covering index,介紹一個利用join提高分頁效率的歪招:
分頁往往需要用到limit offset,在偏移量很大的時候,比如limit 100000,50,mysql需要檢索100050資料,性能嚴重下降。常見的處理方式是a)增加排序輔助列,将limit轉化為在輔助列上範圍查找操作
b)應用層緩存機制 c)需求折中,沒有人會翻到100000頁。以上皆不靈的時候,可以選擇covering
index+join。
這種方式效率較高,因為臨時表a僅在索引上進行操作(innodb索引葉子節點上存儲了主鍵值),取得所需行id之後,再和完整的表進行join擷取其他所需列。
注:mysql的著名分支mariodb支援semi
join和hash join
pivoting&unpivoting主要關注行列旋轉變化,還可以用來對聚合資料進行格式化用于報表展現,在此不再複述