數據倉庫中的 SQL 性能優化(MySQL篇)

作數據倉庫的頭兩年,使用高配置單機 + MySQL的方式來實現全部的計算(包括數據的ETL,以及報表計算。沒有OLAP)。用過MySQL自帶的MYISAM和列存儲引擎Infobright。這篇文章總結了本身和團隊在那段時間碰到的一些常見性能問題和解決方案。 html

P.S.若是沒有特別指出,下面說的mysql都是指用MYISAM作存儲引擎。 java

1.利用已有數據避免重複計算

業務需求中每每有計算一週/一個月的某某數據,好比計算最近一週某個特定頁面的PV/UV。這裏出現的問題就是實現的時候直接取整週的日誌數據,而後進行計算。這樣其實就出現了重複計算,某一天的數據在不一樣的日子裏被重複計算了7次。 mysql

解決辦法很是之簡單,就是把計算進行切分,若是是算PV,作法就是天天算好當天的PV,那麼一週的PV就把算好的7天的PV相加。若是是算UV,那麼天天從日誌數據取出相應的訪客數據,把最近七天的訪客數據單獨保存在一個表裏面,計算周UV的時候直接用這個表作計算,而不須要從原始日誌數據中抓上一大把數據來算了。 git

這是一個很是簡單的問題,甚至不須要多少SQL的知識,可是在開發過程當中每每被視而不見。這就是隻實現業務而忽略性能的表現。從小規模數據倉庫作起的工程師,若是缺少這方面的意識和作事規範,就容易出現這種問題,等到數據倉庫的數據量變得比較大的時候,纔會發現。需求決定能力。 github

2.case when關鍵字的使用方法

case when這個關鍵字,在作聚合的時候,能夠很方便的將一份數據在一個SQL語句中進行分類的統計。舉個例子,好比下面有一張成績表(表名定爲scores): 算法

 

如今須要統計小張的平均成績,小明的平均成績和小明的語文成績。也就是最終結果應該是: sql

SQL實現以下: 數據庫

若是如今這個成績表有1200萬條數據,包含了400萬的名字 * 3個科目,上面的計算須要多長時間?我作了一個簡單的測試,答案是5.5秒。 編程

而若是咱們在name列上面加了索引,而且把sql改爲下面的寫法: json

這樣的話,只須要0.05秒就能完成。

那麼若是有索引的話,前面的一種實現方法會不會變快?答案是不會,時間仍是跟原來同樣。

而若是沒有索引,後面一種寫法會用多少時間?測試結果是3.3秒。

把幾種狀況再理一遍:

之因此後面一種寫法老是比前面一種寫法快,不一樣之處就在因而否先在where裏面把數據過濾掉。用where有兩個好處:一個是有索引的話就能使用,而case when頗有可能用不到索引(關於索引的具體使用這裏就不詳細解釋了,至少在這個例子中前一種寫法沒有用到索引),第二是可以提早過濾數據,哪怕沒有索引,前一種寫法掃描了三遍全表的數據(作一個case when掃一遍),後面的寫法掃描一遍全表,把數據過濾了以後,case when就不用過這麼多數據量了。

而實際狀況是,開發常常只是爲了實現功能邏輯,而習慣了在case when中限制條件取數據。這樣在出現相似例子中的需求時,沒有把應該限制的條件寫到where裏面。這是在實際代碼中發現最多的一類問題。

編者注:

關於文中原做者認爲的:前一種寫法掃描了三遍全表的數據(作一個case when掃一遍)

我的存疑,有待考證,理論上執行計劃不會這樣弱:

explain select SQL_NO_CACHE 
       avg (case when cate1 ='二手市場' then pv end) as es,
       avg (case when cate1 ='寵物' then pv end) as cw,
       avg (case when cate1 ='房產信息' then pv end) as fc
from pagetype_lite_201408 where recDate >= '2014-08-01' and recDate <= '2014-08-07';

explain select SQL_NO_CACHE 
       avg (case when cate1 ='二手市場' then pv end) as fc,
       avg (case when cate1 ='寵物' then pv end) as es,
       avg (case when cate1 ='房產信息' then pv end) as zp
from pagetype_lite_201408 where recDate >= '2014-08-01' and recDate <= '2014-08-07' 
and cate1 in('二手市場','寵物','房產信息');
1	SIMPLE	t_lj_pagetype_lite_201408	ALL	Index_recDate_cate,Index_recDate_city				3438530	Using where
能夠看到,兩個 SQL 的執行計劃同樣,並無出現一個三次掃描,一個一次。

3.分頁取數方式

在數據倉庫中有一個重要的基礎步驟,就是對數據進行清洗。好比數據源的數據若是以JSON方式存儲,在mysql的數據倉庫就必須將json中須要的字段提取出來,作成單獨的表字段。這個步驟用sql直接處理很麻煩,因此能夠用主流編程語言(好比java)的json庫進行解析。解析的時候須要讀取數據,一次性讀取進來是不可能的,因此要分批讀取(至關於分頁了)。

最初的實現方式就是標記住每次取數據的偏移量,而後一批批讀取:

這樣的代碼,在開始幾句sql的時候執行速度還行,可是到後面會愈來愈慢,由於每次要讀取大量數據再丟棄,實際上是一種浪費。

高效的實現方式,能夠是用表中的主鍵進行分頁。若是數據是按照主鍵排序的,那麼能夠是這樣(這麼作是要求主鍵的取值序列是連續的。假設主鍵的取值序列咱們比較清楚,是從10001-1000000的連續值):

就算數據不是按主鍵排序的,也能夠經過限制主鍵的範圍來分頁。這樣處理的話,主鍵的取值序列不連續也沒有太大問題,就是每次拿到的數據會比理想中的少一些,反正是用在數據處理,不影響正確性:

這樣的話,因爲主鍵上面有索引,取數據速度就不會受到數據的具體位置的影響了。

4.索引使用

索引的使用是關係數據庫的SQL優化中一個很是重要的主題,也是一個常識性的東西。可是工程師在實際開發中每每是加完索引就以爲萬事大吉了,也不去檢查索引是否被正確的使用了,因此仍是簡單的提一下關於索引的案例。

仍是舉例說明。假若有一個電商網站,積累了某一天的訪問日誌表item_visits,每條記錄表示某一個商品(item)被訪問了一次,包括訪問者的一些信息,好比用戶的id,暱稱等等,有1200多萬條數據。示例以下:

商品自己有一個商品表items,包含800多種商品,表名了商品名字和所屬種類:

如今要計算每一個商品種類(item_type)被訪問的次數。sql的實現不難:

而後既然是join,那麼在join key上須要加索引。這時候有的工程師就隨手在items的item_id上面加了索引。跑了一下,須要95秒。(p.s.在個人測試場景中,這個日誌表有20多個字段,因此雖然這個表的記錄數跟問題2中的那個表的記錄數差很少,可是大小會差不少,瞭解這個背景能夠解釋這裏的計算用時爲何會遠遠超過問題2中的用時。)

前面說是隨手加的索引,其實就已經在暗示加的有問題。那咱們在item_visit的item_id上面再加個索引,須要跑多久?80秒。

用explain查一下執行計劃:

注意到這裏是以日誌表做爲驅動表的(即從日誌表開始掃描數據,而商品表是nest loop的內層嵌套),這樣的話兩個表的item_id都用到了,商品表的索引作join,日誌表的索引能夠作覆蓋索引(這個覆蓋索引就是比前面快的緣由)。看上去挺「划算」的,實際上因爲放棄了item小表驅動,速度反而慢了不少。

接下來用straight_join的鏈接方式把這個sql強制改爲小表驅動:

再來看執行計劃:

雖然這樣一來商品表的索引就用不到了,可是這實際上是正確的作法(固然若是條件容許,也未必要用straight join,把商品表上的索引去掉實際上是最合理的作法,這樣mysql就會本身選擇正確的執行計劃了。),測試下來只須要8秒。緣由就在於大表驅動時,根據標準的Block Nested Loop Join算法,小表的數據會被反覆循環讀取。固然實際上小表是能夠進cache而不用重複讀取的,可是因爲mysql只認索引有沒有用上,因此仍是會反覆讀取小表(這個問題在這個slides的35頁也有描述)。而若是小表驅動,就不會有這個問題。

後續更新:嚴格來講,這個場景有一個限制條件,就是大表中的商品item_id只佔所有item_id的一部分。若是大表中的商品item_id幾乎均勻覆蓋全部item_id,那麼不管join時用哪一個表的索引,其實運行時間都差很少。原來作實驗的時候忽視了這一點,後來從新嘗試的時候發現了這個問題。特此補充。

小結一下:這裏說了兩個問題,一個是添加索引的時候須要想一想如何去加,在不是很確定的時候能夠看看執行計劃,而不是教條式的知道「join要加索引」。學習sql優化切忌只是背幾個tips。另外就是mysql在選擇執行計劃的時候也不必定可以作到最好,若是發現mysql的執行計劃有很大問題,那麼就須要工程師進行調整,mysql中同樣有相似oracle中的hint幫助咱們達到想要的目的,就像例子中的straight_join。

最後還須要注意的是覆蓋索引強制索引的問題。

5.過多的join

在mysql中,須要join的表若是太多,會對性能形成很顯著的降低。一樣,舉個例子來講明。

首先生成一個表(命名爲test),這個表只有60條記錄,6個字段,其中第一個字段爲主鍵:

而後作一個查詢:

也就是說讓test表跟本身關聯。計算的結果顯然是60,並且幾乎不費時間。

可是若是是這樣的查詢(十個test表關聯),會花費多少時間?

答案是:確定超過5分鐘。由於作了實際測試,5分鐘尚未出結果。這裏的測試爲了方便起見,用了一個表本身關聯10次,實際上若是是不一樣的表,效果也是同樣的。

那麼mysql到底在幹什麼呢?用show processlist去看一下運行時狀況:

原來是處在statistics的狀態。這個狀態,根據mysql的解釋是在根據統計信息去生成執行計劃,固然這個解釋確定是沒有追根溯源。實際上mysql在生成執行計劃的時候,其中有一個步驟,是肯定表的join順序。默認狀況下,mysql會把全部join順序所有排列出來,依次計算各個join順序的執行代價而且取最優的那個。這樣一來,n個表join會有n!種狀況。十個表join就是10!,大概300萬,因此難怪mysql要分析半天了。

而在實際開發過程當中,曾經出現過30多個表關聯的狀況(有10^32種join順序)。一旦出現,花費在statistics狀態的時間每每是在1個小時以上。這還只是在表數據量都很是小,須要作順序分析的點比較少的狀況下。至於出現這種狀況的緣由,無外乎咱們須要計算的彙總報表的字段太多,須要從各類各樣的地方計算出來數據,而後再把數據拼接起來,報表在維護過程當中不斷添加字段,又因爲種種緣由沒有去掉已經廢棄的字段,這樣字段一定會越來愈多,實現這些字段計算就須要用更多的臨時計算結果表去關聯到一塊兒,結果須要關聯的表也愈來愈多,成了mysql沒法承受之重。

這個問題的解決方法有兩個。從開發角度來講,能夠控制join的表個數。若是須要join的表太多,能夠根據業務上的分類,先作一輪join,把表的數量控制在必定範圍內,而後拿到第一輪的join結果,再作第二輪全局join,這樣就不會有問題了。從運維角度來講,能夠設置optimizer_search_depth這個參數。它可以控制join順序遍歷的深度,進行貪婪搜索獲得局部最優的順序。通常有好多個表join的狀況,都是上面說的相同維度的數據須要拼接成一張大表,對於join順序基本上沒什麼要求。因此適當的把這個值調低,對於性能應該說沒有影響。

6.列存儲引擎Infobright

Infobright是基於mysql的存儲引擎,具備列存儲/列壓縮和知識網格等特性,比較適合數據倉庫的計算。使用起來也不須要考慮索引之類的問題,很是方便。不過通過一段時間的運用,也發現了個別須要注意的問題。

一個問題和MYISAM相似,不要取不須要的數據。這裏說的不須要的數據,包括不須要的列(Infobright的使用常識。固然行存儲也要注意,只不過影響相對比較小,因此沒有專門提到),和不須要的行(行數是能夠擴展的,行存儲一行基本上都能存在一個存儲單元中,可是列存儲一列明顯不可能存在一個存儲單元中)。

第二個問題,就是Infobright在長字符檢索的時候並不給力。通常來講,網站的訪問日誌中會有URL字段用來標識訪問的具體地址。這樣就有查找特定URL的需求。好比我要在cnblog的訪問日誌中查找到個人blog的訪問次數:

相似這樣在一個長字符串裏面檢索子串的需求,Infobright的執行時間測試下來是mysql的1.5-3倍。

至於速度慢的緣由,這裏給出一個簡要的解釋:Infobright做爲列式數據庫使用了列存儲的經常使用特性,就是壓縮(列式數據庫的壓縮率通常要能作到10%之內,Infobright也不例外)。另外爲了加快查找速度,它還使用了一種叫知識網格檢索方式,通常狀況下可以極大的減小須要讀取的數據量。關於知識網格的原理已經超出了本篇文章的討論篇幅,能夠看這裏瞭解。可是在查詢url的時候,知識網格的優勢沒法體現出來,可是使用知識網格自己帶來的檢索代價和解壓長字符串的代價卻仍然存在,甚至比查詢通常的數字類字段要來的大。

而後根據其原理能夠給出一個可以說明問題的解決方法(雖然實用度不算高):若是整個表裏面就有一個長字符串字段查詢起來比較麻煩,能夠把數據根據這個字段排序後再導入。這樣一來按照該字段查詢時,經過知識網格就可以屏蔽掉比較多的「數據包」(Infobright的數據壓縮單元),而未排序的狀況下符合條件的數據散佈在各個「數據包」中,其解壓工做量就大得多了。使用這個方法進行查詢,測試下來其執行時間就只有mysql的0.5倍左右了。

7.Refer:

[1] 數據倉庫中的sql性能優化(MySQL篇)

http://sunyi514.github.io/2013/04/30/%E6%95%B0%E6%8D%AE%E4%BB%93%E5%BA%93%E4%B8%AD%E7%9A%84sql%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%EF%BC%88mysql%E7%AF%87%EF%BC%89/

相關文章
相關標籤/搜索