1. 程式人生 > 實用技巧 >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

將這段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,也就是全表掃描。

再看優化後的SQL:

為了更好的比較它們究竟有何區別,需要理解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、記憶體相關)。