SQL優化之部落格案例
問題背景:
部落格首頁隨著資料量的增加,最初是幾百上千的資料,訪問正常,這是開發環境,當切換測試環境(通過爬蟲已有資料六萬多),這時候訪問非常緩慢,長達一分鐘。
問題SQL:
SELECT DISTINCT post.`ID` AS postId,post.`post_title`,post.`post_content`,post.`post_excerpt`,u.`display_name`, (SELECT IFNULL(GROUP_CONCAT(term.name),'') FROM wp_term_relationships AS r LEFT JOIN wp_term_taxonomy AS t ON(r.term_taxonomy_id = t.term_taxonomy_id) LEFT JOIN wp_terms AS term ON(term.term_id = t.term_id) WHERE r.object_id= post.ID AND t.taxonomy = 'category') AS categoryName, (SELECT IFNULL(GROUP_CONCAT(term.name),'') FROM wp_term_relationships AS r LEFT JOIN wp_term_taxonomy AS t ON(r.term_taxonomy_id = t.term_taxonomy_id) LEFT JOIN wp_terms AS term ON(term.term_id = t.term_id) WHERE r.object_id = post.ID AND t.taxonomy = 'post_tag') AS tagName, post.`comment_count`,post.`post_status`,post.`post_date` FROM wp_posts AS post LEFT JOIN wp_users AS u ON(post.`post_author` = u.`ID`) LEFT JOIN wp_term_relationships AS relation ON(relation.`object_id` = post.`ID`) LEFT JOIN wp_term_taxonomy AS taxonomy ON(taxonomy.`term_taxonomy_id`=relation.`term_taxonomy_id`) LEFT JOIN wp_terms AS term ON(term.`term_id` = taxonomy.`term_id`) WHERE post.`post_type` = 'post' AND post.`post_status` IN ('publish') ORDER BY post.`post_date` DESC LIMIT 0,10
將這段sql放在sqlyog裡執行,結果花費時間如下:
執行:59.204sec 總數:59.239 10行(僅僅顯示10條資料)
優化後的SQL:
SELECT DISTINCT post.`ID` AS postId,post.`post_title`,post.`post_content`,post.`post_excerpt`,u.`display_name`, (SELECT IFNULL(GROUP_CONCAT(term.name),'') FROM wp_term_relationships AS r LEFT JOIN wp_term_taxonomy AS t ON(r.term_taxonomy_id = t.term_taxonomy_id) LEFT JOIN wp_terms AS term ON(term.term_id = t.term_id) WHERE r.object_id = post.ID AND t.taxonomy = 'category') AS categoryName, (SELECT IFNULL(GROUP_CONCAT(term.name),'') FROM wp_term_relationships AS r LEFT JOIN wp_term_taxonomy AS t ON(r.term_taxonomy_id = t.term_taxonomy_id) LEFT JOIN wp_terms AS term ON(term.term_id = t.term_id) WHERE r.object_id = post.ID AND t.taxonomy = 'post_tag') AS tagName, post.`comment_count`,post.`post_status`,post.`post_date` FROM ( SELECT * FROM wp_posts WHERE `post_type` = 'post' AND `post_status` IN ('publish') ORDER BY `post_date` DESC LIMIT 0,10 ) AS post LEFT JOIN wp_users AS u ON(post.`post_author` = u.`ID`) LEFT JOIN wp_term_relationships AS relation ON(relation.`object_id` = post.`ID`) LEFT JOIN wp_term_taxonomy AS taxonomy ON(taxonomy.`term_taxonomy_id` =relation.`term_taxonomy_id`) LEFT JOIN wp_terms AS term ON(term.`term_id` = taxonomy.`term_id`)
將這段sql放在sqlyog裡執行,結果花費時間如下:
執行:0.056sec 總數:0.288sec 10行(僅僅顯示10條資料)
優化過後,直接是毫秒級。結果專案在測試環境下訪問不卡了。
主要的改動把查詢和過濾條件從最後面嵌入到主表的子查詢裡。
那麼問題SQL為什麼會這麼慢?而優化過後的SQL為什麼會突然一下如此迅速到毫秒級呢?
先看問題一,為什麼問題SQL會這麼慢,問題SQL:
SELECT DISTINCT post.`ID` AS postId,post.`post_title`,post.`post_content`,post.`post_excerpt`,u.`display_name`, (SELECT IFNULL(GROUP_CONCAT(term.name),'') FROM wp_term_relationships AS r LEFT JOIN wp_term_taxonomy AS t ON(r.term_taxonomy_id = t.term_taxonomy_id) LEFT JOIN wp_terms AS term ON(term.term_id = t.term_id) WHERE r.object_id = post.ID AND t.taxonomy = 'category') AS categoryName, (SELECT IFNULL(GROUP_CONCAT(term.name),'') FROM wp_term_relationships AS r LEFT JOIN wp_term_taxonomy AS t ON(r.term_taxonomy_id = t.term_taxonomy_id) LEFT JOIN wp_terms AS term ON(term.term_id = t.term_id) WHERE r.object_id = post.ID AND t.taxonomy = 'post_tag') AS tagName, post.`comment_count`,post.`post_status`,post.`post_date` FROM wp_posts AS post LEFT JOIN wp_users AS u ON(post.`post_author` = u.`ID`) LEFT JOIN wp_term_relationships AS relation ON(relation.`object_id` = post.`ID`) LEFT JOIN wp_term_taxonomy AS taxonomy ON(taxonomy.`term_taxonomy_id` =relation.`term_taxonomy_id`) LEFT JOIN wp_terms AS term ON(term.`term_id` = taxonomy.`term_id`) WHERE post.`post_type` = 'post' AND post.`post_status` IN ('publish') ORDER BY post.`post_date` DESC LIMIT 0,10
程式自上而下,從左到右執行,先SELECT 再LEFT JOIN 多個表,最後再WHERE 以及 ORDER BY 和 LIMIT,咋看一下也沒有問題啊,但實際上很有問題。
問題分析???
- 首先,我並沒有使用SELECT * 而是列舉我需要的欄位。
- 使用explain 關鍵字看問題SQL,結果如下:
以type欄位為主要的來說,表掃描方式:system>const>eq_ref>ref>range>index>all,最慢的是all,也就是全表掃描。
為了更好的比較它們究竟有何區別,需要理解explain獲取引數的含義。
explain關鍵字含義
(1)id
MySQL QueryOptimizer選定的執行計劃中查詢的序列號,表達查詢中執行select子句或操作表順序。id值越大優先順序越高,優先順序越高就會先被執行。id相同,執行順序由上至下。
(2)select_type
- SIMPLE(簡單的select查詢(不使用union及子查詢))
- PRIMARY(最外層的select查詢,如果兩表存在則查詢,則外層的表操作為PRIMARY,內層(子查詢)的操作為SUBQUERY)
- SUBQUERY(子查詢中首個SELECT(如果有多個子查詢存在),不依賴於外層的表。除from子句中包含的子查詢外,其他地方出現的子查詢都可能是SUBQUERY)/DEPENDENT SUBQUERY(子查詢中首個SELECT(如果有多個子查詢存在),就依賴於外層的表)
(3)table
輸出行所引用的表。顯示的查詢表名,如果查詢使用了別名,那麼這裡顯示的是別名,如果不涉及對資料表的操作,那麼這顯示為null,如果顯示為尖括號括起來的就表示這個是臨時表,後邊的N就是執行計劃的id,表示結果來自這個查詢產生。如果是尖括號括起來的,與類似,也是一個臨時表,表示這個結果來自於union查詢的id為M,N的結果集。
(4)type
從優到差的順序如下:system->const->eq_ref->ref->fulltext->ref_or_null->index_merge->unique_subquery->index_subquery->range->index->all
一般來說,開發人員寫的SQL基本要求是eq_ref級別。
(5)possible_keys
指出能再該表中使用哪些索引有助於查詢,查詢可能使用的索引都會再這裡列出來。如果為空,說明沒有可用的索引。
(6)key
實際從possible_key選擇使用的索引,如果為null,則沒有使用索引。select_type為index_merge時,這裡可能出現兩個以上的索引,其他的select_type這裡只會出現一個。很少的情況下,MySQL會選擇優化不足的索引。這種情況下,可以再SELECT語句中使用USE INDEX來強制使用一個索引或者用IGNORE INDEX來強制MySQL忽略索引。
(7)key_len
用於處理查詢的索引長度,再不損失精確性的情況下,長度越短越好。如果是單列索引,那就整個索引長度算進去,如果是多列索引,那麼查詢不一定都能使用到所有的列,具體使用了多少個列的索引,這裡就會計算進去,沒有使用的列,這裡不會計算進去。key_len只計算where條件用到的索引長度,而排序和分組就算用到了索引,也不會計算到ken_len中。
(8)ref
顯示索引的哪一列被使用。如果使用的常數等值查詢,這裡會顯示const,如果是連線查詢,被驅動表的執行計劃這裡會顯示驅動表的關聯欄位,如果條件使用了表示式或者函式,或者條件列發生內部隱式轉換,這裡可能會顯示func。
(9)rows
認為必須檢查的用來返回請求資料的行數,即需要掃描的次數。
(10)extra
這個列可以顯示的資訊很,如果出現Using filesort、Using temporary兩項意味著不能使用索引,效率會受到重大影響。應儘可能對其進行優化。
- distinct:在select部分使用了distinct關鍵字
- using filesort:排序時無法使用到索引時,就會出現這個。常見於order by 和group by語句中。沒有辦法利用現有索引進行排序,需要額外排序,建議:根據排序需要,建立相應合適的索引。
- using index:查詢時不需要回表查詢,直接通過索引就可以獲取查詢的資料。利用覆蓋索引,無需回表即可取得結果資料,這種結果是好的。
- using temporay:表示使用了臨時表儲存中間結果。
- using where:表示儲存引擎返回的記錄並不是所有的都滿足查詢條件,需要在server層進行過濾。
理解完explain後,用explain重要引數來解釋這段問題SQL:
即key、type 、rows、extra。以其中的rows來看,該段sql執行之初直接就掃描28386次數。而優化後的SQL僅僅掃描10次,由此可知慢在該地方,針對次進行修改。這是問題SQL慢的根本原因。
但最後我發現優化後的SQL還算很冗餘,因為作為首頁展示,其實沒必要這麼多表關聯,如果是檢視詳情的話還可以通過拆分,然後分段執行即可。
最終首頁SQL如下(也相當於另外一種解法,優化為單表):
SELECT DISTINCT post.`ID` AS postId,post.`post_title`,post.`post_content`,post.`post_excerpt`, (SELECT `display_name` FROM wp_users WHERE ID = post_author) AS display_name, post.`comment_count`,post.`post_status`,post.`post_date` FROM wp_posts AS post WHERE `post_type` = 'post' AND `post_status` IN ('publish') ORDER BY `post_date` DESC LIMIT 0,10
另外除此之外,歸檔查詢也是用的這段SQL,這樣一來也需要優化,於是我將其分離寫成不同的DAO,針對性優化(如果不優化,資料量過大也會有問題)。
問題SQL(歸檔,消耗8.183sec):
SELECT DISTINCT post.`ID` AS postId,post.`post_title`,post.`post_content`,post.`post_excerpt`,u.`display_name`, post.`comment_count`,post.`post_status`,post.`post_date` FROM ( SELECT * FROM wp_posts WHERE `post_type` = 'post' AND `post_status` IN ('publish') AND DATE_FORMAT(`post_date`, '%Y年%m月') = '2020年06月' ORDER BY `post_date` DESC LIMIT 0,10 ) AS post LEFT JOIN wp_users AS u ON(post.`post_author` = u.`ID`)
問題SQL再度優化(這次執行時間是55.496sec):
SELECT DISTINCT post.`ID` AS postId,post.`post_title`,post.`post_content`,post.`post_excerpt`,u.`display_name`, post.`comment_count`,post.`post_status`,post.`post_date` FROM ( SELECT * FROM wp_posts WHERE `post_type` = 'post' AND `post_status` IN ('publish') AND DATE_FORMAT(`post_date`, '%Y年%m月') = '2020年06月' ORDER BY `post_date` DESC LIMIT 0,10 ) AS post LEFT JOIN wp_users AS u ON(post.`post_author` = u.`ID`)
最終優化版:
SELECT DISTINCT post.`ID` AS postId,post.`post_title`,post.`post_content`,post.`post_excerpt`, (SELECT `display_name` FROM wp_users WHERE ID = post_author) AS display_name, post.`comment_count`,post.`post_status`,post.`post_date` FROM wp_posts AS post WHERE ID IN (SELECT `ID` FROM wp_posts WHERE `post_type` = 'post' AND `post_status` IN ('publish') AND DATE_FORMAT(`post_date`, '%Y年%m月') = '2020年06月' ORDER BY `post_date` DESC) LIMIT 0,10
這個優化版本,我的思路是歸檔抽取為一個子查詢條件查詢和排序獲取ID,獲取ID這段SQL是毫秒級,然後再在外層LIMIT即可。
通用規律和方法
- 通過explain關鍵字理解SQL走向和慢的原因(explain中的id可以瞭解sql是如何執行的)
- 學會拆分,一分為二寫(以檢視文章詳情為例,可分為兩部分,一部分為獲取詳情,另外一部分獲取文章對應的分類或標籤,這樣一來sql基本上都可以確保為eq_ref級別且毫秒級)
- 合理使用子查詢(例如歸檔這部分查id,拿獲取的id作為where查詢條件,背後的原理走主鍵索引,那麼為什麼主鍵索引快,因為主鍵索引比普通索引快是因為主鍵索引只檢索一次)
以上述方法為例,解決資料量大分頁效能問題(解決部落格系統點選尾頁載入慢問題(本質上還是SQL原因,優化了下,主要利用主鍵索引)),優化後的程式碼如下:
SELECT DISTINCT post.`ID` AS postId,post.`post_title`,post.`post_content`,post.`post_excerpt`, (SELECT `display_name` FROM wp_users WHERE ID = post_author) AS display_name, post.`comment_count`,post.`post_status`,post.`post_date` FROM wp_posts AS post JOIN (SELECT ID FROM wp_posts WHERE `post_type` = 'post' AND `post_status` IN ('publish') LIMIT 2340,10) AS post_b ON(post.ID = post_b.ID) WHERE `post_type` = 'post' AND `post_status` IN ('publish') ORDER BY `post_date` DESC
FAQ
為什麼SQL查詢緩慢?
通常可歸納為如下:
- 沒有索引或沒有用到索引
- I/O吞吐量小
- 記憶體不足
- 網路速度慢
- 查詢出的資料量過大
- 鎖或死鎖
- 返回不必要的行和列
以我本次為例,首頁之所以慢,是因為最開始那段SQL掃描行數大。等到掃描完後再關聯表,再子查詢。
而優化過後的掃描行數僅僅就10行,然後再關聯再子查詢。
兩者的區別是前者是全部掃描一遍再關聯再條件,後者直接根據條件過濾再關聯。
子查詢(內嵌查詢)的執行過程是什麼?
由內向外處理,對應本文舉的首頁文章優化語句。
為什麼SQL查單個欄位不分頁同樣也是六萬條資料,消耗時間卻是毫秒級?
因為IO的消耗(輸入/輸出),輸出資料量大會導致吞吐量小(吞吐量與磁碟、CPU、記憶體相關)。