調整數據庫表結構,搞定 WordPress 數據庫查詢緩慢問題

同事的基於 WordPress 搭建的網站,由於數據愈來愈多,變得慢,我從 PHP slow log 裏面看出是 WordPress 有些查詢老是很慢,即便已經安裝了頁面緩存插件,可是因爲頁面衆多,命中率不高,因此加速效果也不明顯,並且因爲界面常常改版,頁面緩存須要清空從新生成,進一步下降了緩存的效果。反正就是不流暢,有點慢。
 
看了下服務器配置雖然不高,可是也不至於打開一個一面要 4 秒鐘吧,並且 CPU 佔用率奇高,雖說升級硬件能夠緩解,但根源仍是程序效率的問題,因此不妨先趁性能出現問題的狀況下,優化程序,解決程序的性能問題後,再升級服務器硬件,這樣效果才持久。
 
因而乎打算從表結構上做些優化。主要影響性能的,是兩張表:wp_postmeta、wp_term_relationships、wp_posts
 
先看一下最終結果:
能夠看到 CPU 明顯降低了很多(那兩個劇烈波動的折線請忽略,跟本文無關)。
 

優化過程

先介紹一下本次優化涉及到的數據庫表結構:

業務和表的關係

內容類型 數據表
文章 wp_posts
頁面 wp_posts
自定義文章類型 wp_posts
附件 wp_posts
導航菜單 wp_posts
文章元數據 wp_post_meta
分類目錄 wp_terms
標籤 wp_terms
自定義分類法 wp_term_taxonomy
 
 

表之間的關係

數據表 存儲的數據 關聯到
wp_posts 文章、頁面、附件、版本、導航菜單項目 wp_postmeta (經過post_id關聯)
wp_postmeta 每一個文章的元數據 wp_posts (經過 post_id關聯)
wp_term_relationships 文章和自定義分類法之間的關係

wp_posts (經過 post_id 關聯)數據庫

wp_term_taxonomy (經過term_taxonomy_id 關聯)緩存

wp_term_taxonomy 自定義分類法(包括默認的分類目錄和標籤) wp_term_relationships(經過 term_taxonomy_id關聯)
wp_terms 關聯到分類法中的分類目錄,標籤和自定義分類項目 wp_term_taxonomy (經過term_id 關聯) 
 
wp_postmeta 是查詢最慢的一張表,它存放文章/頁面/自定義內容(wp_posts)的元數據信息,所謂元數據,也包括如文章查看數、封面圖片,還有你自定義的字段。
按理說,一篇文章(wp_posts),對應 wp_postmeta 一行記錄,爲啥會慢呢?緣由是,WordPress 把 wp_postmeta 設計成了一張 縱表,並且沒有恰當的索引。
 
關於橫表和縱表,橫表是咱們作項目最經常使用的,不清楚這個概念的朋友,看下面的的小實驗就明白了:
 
普通橫表 STUDENT_SCORE 有語文成績、英語成績等7個KPI指標,三個學生的三條記錄:
SQL> SELECT * FROM STUDENT_SCORE;
 
        Id     CHINESE_SCORE ENGLISH_SCORE MATH_SOCRE PHYSICAL_SCORE SPORTS_SCORE CHEMICAL_SCORE BIOLOGICAL_SCORE
----------- ------------- ------------- ---------- -------------- ------------ -------------- ----------------
      10001          87.4            63         92             86           75             85               89
      10002            91             89         98             62           76             82               73
      10006            74             63         57             42           76             59               67
 
對應於 縱表/豎表,這三個學生的7個KPI指標須要21條記錄才能描述清楚:
SQL> SELECT * FROM STUDENT_SCORE;
 
Id               FieldName             Value
----------- --------------------- ----------
10001      CHINESE_SCORE       87.4
10001      ENGLISH_SCORE       63
10001      MATH_SOCRE             92
10001      PHYSICAL_SCORE     86
10001      SPORTS_SCORE        75
10001      CHEMICAL_SCORE    85
10001      BIOLOGICAL_SCORE 89
 
10002      CHINESE_SCORE       91
10002      ENGLISH_SCORE       89
10002      MATH_SOCRE             98
10002      PHYSICAL_SCORE     62
10002      SPORTS_SCORE        76
10002      CHEMICAL_SCORE    82
10002      BIOLOGICAL_SCORE 73
 
10006      CHINESE_SCORE       74
10006      ENGLISH_SCORE       63
10006      MATH_SOCRE             57
10006      PHYSICAL_SCORE     42
10006      SPORTS_SCORE        76
10006      CHEMICAL_SCORE    59
10006      BIOLOGICAL_SCORE 67
 
因此咱們從這個小實驗中能夠看到, 橫錶轉成縱表/豎表,對應的記錄會翻倍增加,這對應於數據量大的表或寬表,都是一件很差的消息。不少時候,數據量上去了,性能問題就出來了
 
 
分析獲得 WordPress 歷來是不會根據 meta_id 去查 postmeta 表的,都是根據 post_id 去查 post 的單個 meta 信息或者全部 meta key 和 value,因此本來的主鍵 meta_id 仍然保持自增(由於 的,它就僅僅是一個自增 ID)
提高性能的辦法是把 post_id 和 meta_key 改成主鍵,而後根據 post_id 作分區表,這樣,這樣有兩個好處,一是查詢時,能夠根據 post_id 去讀區分區表的數據了,不用再全表查找了,另外是這倆字段組成惟一約束和索引了,查詢速度天然會加快,而本來的主鍵 meta_id 仍然保持自增,不會影響到本來的業務邏輯。
 
WordPress 默認沒有爲 wp_postmeta 的表沒有設定 post_id 和 meta_key 的惟一約束,也就是說,是存在一個 post 再 postmeta 表有多個一樣的的 meta key 和 value 的狀況的,我驗證了一下:
 
SELECT *
FROM
    wp_postmeta pm
WHERE
    meta_id NOT IN (
       SELECT max(meta_id) FROM  wp_postmeta pm2 where  pm2.post_id=pm.post_id and pm2.meta_key=pm.meta_key
    )
 
SELECT distinct meta_key From wp_postmeta Group By post_id,meta_key Having Count(*)>1

 

返回內容大體以下:
 
/*
'_wp_old_slug'
'_thumbnail_id'
'_edit_lock'
*/
 
確實是這樣,可是看了下都是 WordPress 運行過程當中產生的垃圾數據,是能夠無反作用刪除的,那麼此路是可行的。
 
好,那麼,先先清理下垃圾數據:
DELETE FROM wp_postmeta WHERE meta_key = '_edit_lock';
DELETE FROM wp_postmeta WHERE meta_key = '_edit_last';
DELETE FROM wp_postmeta WHERE meta_key = '_revision-control';
DELETE FROM wp_postmeta WHERE post_id NOT IN (SELECT post_id FROM wp_posts);
DELETE FROM wp_postmeta WHERE meta_key = '_wp_old_slug';
DELETE FROM wp_postmeta WHERE meta_key = '_revision-control';
DELETE FROM wp_postmeta WHERE meta_value = '{{unknown}}’;

 

而後,刪除掉重複的 meta key 和 value 記錄,僅保留最新的一個
DELETE
FROM
    wp_postmeta
WHERE
meta_id  IN (
    select * from (
    select meta_id
    FROM
        wp_postmeta pm
    WHERE
        meta_id NOT IN (
           SELECT max(meta_id) FROM  wp_postmeta pm2 where  pm2.post_id=pm.post_id and pm2.meta_key=pm.meta_key
        )
    ) as g1
)

 

 
這裏存在一個問題,就是 WordPress 在開啓了文章的版本控制狀況下,是存在插入重複 post 和 meta key 的狀況的,數據庫改爲惟一約束後會報錯,或者其它插件會這麼作,解決辦法是,WordPress 裏面 Hook 一下 add metadata 函數,insert 前先 check 是否已經 exists,另外就是數據庫裏面加個 Trigger 作判斷,若是已存在,就更新。
 

數據清理完畢,那麼能夠開始創建分區表了

必須先 ADD UNIQUE(`meta_id`),才能 DROP meta_id 的 PRIMARY KEY。
ALTER TABLE `wp_postmeta`
ADD UNIQUE INDEX `UNQ_meta_id` (`meta_id` ASC);
ALTER TABLE `wp_postmeta`
DROP PRIMARY KEY (`meta_id`);
 
再 DROP 掉 meta_id 的 UNIQUE,這是由於後面分區,要求 RANGE 分區列的UNIQUE INDEX 必須包含全部 primary key ,即任意 UNIQUE INDEX 都要包含  post_id,meta_key 分區函數列,不然分區函數是沒法建立,會報錯誤:Error Code: 1503. A UNIQUE INDEX must include all columns in the table's partitioning function。
 
ALTER TABLE `wp_postmeta`
DROP UNIQUE INDEX `UNQ_meta_id` (`meta_id` ASC);
 
ALTER TABLE `wp_postmeta`
ADD PRIMARY KEY (`post_id`, `meta_key`);
 
ALTER TABLE `wp_postmeta`
CHANGE COLUMN `meta_key` `meta_key` VARCHAR(255) NOT NULL ,
CHANGE COLUMN `post_id` `post_id` BIGINT(20) UNSIGNED NOT NULL ;
 
ALTER TABLE `wp_postmeta`
ADD UNIQUE INDEX `UNQ_post_id_meta_key` (`post_id` ASC, `meta_key` ASC),/* 這句能夠加能夠不加,由於已是 PRIMARY KEY */
ADD UNIQUE INDEX `UNQ_meta_id_post_id_meta_key` (`meta_id` ASC, `post_id` ASC, `meta_key` ASC);

 

 
好了,先看下 post 表 id 的分佈,個人是從 id 從 5萬到11萬,先給 posts 表分好區:
SELECT id FROM wp_posts order by id asc;
ALTER TABLE wp_posts PARTITION BY RANGE(id) (
    PARTITION p0 VALUES LESS THAN (60000),
    PARTITION p1 VALUES LESS THAN (70000),
    PARTITION p2 VALUES LESS THAN (80000),
    PARTITION p3 VALUES LESS THAN (90000),
    PARTITION p4 VALUES LESS THAN (100000),
    PARTITION p5 VALUES LESS THAN (110000),
    PARTITION p6 VALUES LESS THAN MAXVALUE
);

 

 
wp_postmeta 表,也如法炮製,這樣再查詢 post 的 meta,不但不用全表掃描,只用掃分區內的數據了,並且還能夠走索引 :
ALTER TABLE wp_postmeta PARTITION BY RANGE COLUMNS(post_id,meta_key) (
    PARTITION p0 VALUES LESS THAN (60000,MAXVALUE),
    PARTITION p1 VALUES LESS THAN (70000,MAXVALUE),
    PARTITION p2 VALUES LESS THAN (80000,MAXVALUE),
    PARTITION p3 VALUES LESS THAN (90000,MAXVALUE),
    PARTITION p4 VALUES LESS THAN (100000,MAXVALUE),
    PARTITION p5 VALUES LESS THAN (110000,MAXVALUE),
    PARTITION p6 VALUES LESS THAN (MAXVALUE,MAXVALUE)
);

 

 
另外, 這個表的查詢也比較耗時,把 object_id,term_taxonomy_id 改成主鍵後,也分下區:
ALTER TABLE wp_term_relationships PARTITION BY RANGE COLUMNS(object_id,term_taxonomy_id) (
    PARTITION p0 VALUES LESS THAN (60000,MAXVALUE),
    PARTITION p1 VALUES LESS THAN (70000,MAXVALUE),
    PARTITION p2 VALUES LESS THAN (80000,MAXVALUE),
    PARTITION p3 VALUES LESS THAN (90000,MAXVALUE),
    PARTITION p4 VALUES LESS THAN (100000,MAXVALUE),
    PARTITION p5 VALUES LESS THAN (110000,MAXVALUE),
    PARTITION p6 VALUES LESS THAN (MAXVALUE,MAXVALUE)
);
 
 
最後,順便根據 MySQL 的統計信息,對 MySQL 的性能參數作了適當的調整:
 
性能調整對應的參數表格:
 
 
 
 
增大了 sort_buffer_size ,使得本來【建立臨時表到磁盤】有 51%,增長 tmp_table_size 調整後下降到 29.36% 。
 
分區後,本來未緩存的頁面打開要 4s-5s,如今 2-3s 就能夠打開啦。觀察一段時間再升級下服務器。
 
 
 
CPU 的使用率也降低了很多(那兩個劇烈波動的折線請忽略,那個是以前別的進程hang了,跟本次無關)。
 
 
而後找了個網站速度測試工具,輸入網址測試一下:
 
 
另外我原本是熟 SQL Server 數據庫優化的,MySQL 的數據庫優化其實一直都是以過去 SQL Server 優化經驗爲指導的,有些地方可能存在盲區和不足,若是有還請指出,謝謝!
相關文章
相關標籤/搜索