網站變慢,多數人第一個念頭是換主機、裝快取外掛,或把圖片再壓小一點。這些都有用,但對一個資料量已經長大的 WordPress 站來說,真正的瓶頸常常藏在更底層的地方:資料庫每打開一頁就反覆跑的那幾條查詢。其中跑得特別久、或被呼叫得特別頻繁的,就是所謂的慢查詢。
慢查詢的麻煩在於它不會出現在前端,你看不到、瀏覽器開發者工具也抓不到,只能感覺到「後台儲存文章很卡」「商品列表頁載入要等好幾秒」。WordPress 慢查詢優化的第一步,不是急著動手調 MySQL 參數,而是先把元兇找出來,看清楚它慢在哪裡。這篇會帶你用 Query Monitor 這個免費外掛把問題查詢攤開來,再用 EXPLAIN 讀懂執行計畫,最後決定該加索引、改寫查詢,還是換掉整個資料儲存方式。
慢查詢是什麼,為什麼它比想像中更傷網站
慢查詢就是資料庫執行時間偏長、或被重複呼叫到拖累整體效能的 SQL 指令。WordPress 是動態網站,每一次頁面請求都會對 MySQL 送出一連串查詢,把文章、設定、選單、留言這些資料撈出來組成頁面。只要其中一條查詢卡住,整個頁面就得等它跑完才能輸出。
判斷一條查詢算不算慢,不能只看單次執行時間。比較實用的分級是:10 毫秒以內通常不用理會;10 到 100 毫秒之間,如果被頻繁呼叫就要注意;超過 100 毫秒已經值得調查;超過 1 秒則是必須立刻處理的等級。
但真正該盯的指標是「單次時間乘以呼叫次數」。一條 0.05 秒的查詢看起來無害,可是如果它在每次頁面載入時被呼叫一千次,累積下來就是 50 秒的資料庫負載。這種藏在迴圈裡反覆執行的查詢,比偶爾跑一次的 2 秒查詢更難發現,殺傷力也更大。多數人花好幾個小時調 MySQL 設定,卻放著一頁跑幾百次的查詢不管,方向其實是反的。
用 Query Monitor 抓出每一頁的問題查詢
要找出慢查詢,最直接的工具是 Query Monitor 這個免費的 WordPress 開發除錯外掛。它會記錄單一頁面請求中執行過的每一條資料庫查詢,告訴你各花了多少時間、總共花了多少時間,以及這條查詢是哪個外掛、佈景主題或 WordPress 核心觸發的。
裝好啟用後,前台與後台的管理列上會多出一段文字,顯示這一頁的產生時間、記憶體用量與資料庫查詢總時間。點下去,畫面底部會跳出一個面板,把所有查詢列出來。重點看以下幾個地方。
- Queries 主面板:列出每一條查詢與耗時,超過門檻的慢查詢會用紅色標示,一眼就能看到哪幾條最該處理。
- 依元件歸因(Queries by Component):把查詢依照觸發的外掛或佈景主題分組,加總各自的查詢數與時間。這是揪出「拖累元兇」最有效的視角,因為大多數慢查詢來自外掛而非 WordPress 核心。
- 重複查詢(Duplicate Queries):標出同一條查詢在這一頁被重複執行了幾次。同一筆資料被反覆撈,通常代表程式碼沒有善用快取,是優化時最容易拿到的成果。
Query Monitor 的好處是不必碰伺服器設定,缺點是它本身會帶來一點額外負擔,不適合長期掛在正式站上。如果你不想在正式環境裝除錯外掛,可以改用 MySQL 內建的慢查詢日誌,從伺服器層面記錄超過指定秒數的查詢,負擔比外掛小,但同樣用完就該關掉。
看懂 EXPLAIN,判斷查詢到底慢在哪裡
找到問題查詢後,下一步是搞清楚它為什麼慢,這時要靠 MySQL 的 EXPLAIN 指令。把 EXPLAIN 加在 SELECT 前面執行,MySQL 會告訴你它打算怎麼跑這條查詢:用了哪些索引、預計掃描幾筆資料、有沒有用到暫存表或檔案排序。
回傳結果裡最關鍵的是 type 欄位,它描述資料表是用什麼方式被存取的,從快到慢大致是這個順序。
- const、eq_ref:靠主鍵或唯一索引直接定位單筆,最理想。
- ref、range:用索引找出符合條件的多筆或一個範圍,表現良好。
- index:掃過整個索引,還可以接受。
- ALL:全表掃描,幾乎都是問題所在。
當你看到 type 是 ALL、而 key 欄位是 NULL,就代表 MySQL 沒有可用的索引,只能從頭到尾把整張表讀一遍。在小站上感覺不出來,但當這張表長到幾十萬、幾百萬筆,全表掃描會變得非常慢。
另一個要看的是 rows 欄位,它顯示 MySQL 預估要檢查多少筆資料才能得到結果。如果一條查詢只回傳 1 筆、卻要掃過 15 萬筆,這種「掃描多、回傳少」的比例就是該加索引的明確訊號。EXPLAIN 還會在 Extra 欄位提示像 Using filesort、Using temporary 這類額外開銷,通常跟 ORDER BY 或 GROUP BY 有關,也是調校的線索。
WordPress 最常見的幾種慢查詢模式
WordPress 的慢查詢來來去去就那幾種典型,認得出模式,處理起來就快。
postmeta 表的 JOIN 沒有對到索引是最常見的一種。WordPress 把自訂欄位資料以「鍵值對」的形式塞在 wp_postmeta 表裡,要依某個自訂欄位篩文章時,就得 JOIN 這張表並比對 meta_key 與 meta_value。麻煩在於 meta_value 欄位的型別是 longtext,沒辦法直接建立完整索引,一旦資料量大,這種查詢就會拖很久。
wp_options 表的全表掃描也很普遍。每次頁面載入幾乎都會跑一條撈出所有自動載入選項的查詢,當外掛裝多了、留下大量選項資料,這條查詢就會越來越重。
分類與標籤查詢在大型站上容易卡關,term_relationships 這類關聯表如果缺索引,撈分類時就會掃過大量資料列。
帶前置萬用字元的 LIKE 搜尋是另一個典型。像 LIKE '%關鍵字%' 這種開頭就放萬用字元的寫法,MySQL 無法使用索引,只能逐列比對整張表,站內搜尋慢通常就是這個原因。
WooCommerce 的多重 JOIN 查詢則是電商站的常客。商品列表要同時撈價格、庫存狀態等多個自訂欄位,每多一個沒有索引支撐的 JOIN,執行時間就會明顯往上疊。
替資料表加上正確的索引
索引是把慢查詢變快最直接的手段。它的作用就像書末的索引頁,讓資料庫能直接跳到對的資料列,而不必從第一頁翻到最後一頁。當查詢的 EXPLAIN 顯示全表掃描、而那張表又已經長大,加索引往往能把查詢時間砍掉一大截。
動手前先用 SHOW INDEXES FROM wp_postmeta; 看看現有索引,避免重複建立。針對 WordPress 常見瓶頸,幾個實用的索引方向是:在 wp_postmeta 的 post_id 與 meta_key 組合上建複合索引,幫助自訂欄位查詢;在 wp_term_relationships 的 term_taxonomy_id 上建索引,加速分類查詢;在 wp_posts 的 post_type 與 post_status 組合上建索引,改善依文章類型篩選的效能。如果站內搜尋是痛點,可以考慮在內容欄位上建立全文索引,用 MATCH … AGAINST 取代前置萬用字元的 LIKE,在大資料量下可以快上一個量級。
不過索引不是越多越好,它有幾個實際代價要先想清楚。
- 占用儲存空間:索引本身要存,雖然相對於整個資料庫通常不大,但仍是成本。
- 拖慢寫入:每次新增、修改、刪除資料,MySQL 都得同步維護索引,所以寫入頻繁的表加太多索引反而會變慢。
- 小表沒效益:資料量小的時候查詢本來就快,這時加索引可能讓 MySQL 多做白工。一般建議等表長到一定規模、且確認有查詢真的會用到它,再針對性地加。
- longtext 無法整欄索引:像 meta_value 這種長文字欄位只能建前綴索引(只索引前面若干字元),而且查詢的 WHERE 條件要同時帶上鍵與值,索引才用得上;只用 meta_value 單邊條件的查詢,索引幫不上忙。
換句話說,索引要對著「真的會被執行、而且掃描成本高」的查詢去加,而不是看到哪張表大就無腦灌一堆索引。
索引解決不了的問題,要改資料模型
有些慢查詢的根源不在缺索引,而在資料一開始就放錯地方,這種情況再怎麼加索引也只是治標。
最典型的就是把大量需要篩選的資料硬塞進 postmeta。postmeta 是鍵值結構,每多一個篩選條件就多一次 JOIN 或一次表查找,meta_value 又是 longtext 難以有效索引,資料一多查詢就會越來越重。遇到這種狀況,與其在 postmeta 上疊索引,不如重新想資料該怎麼存。
- 需要分類、篩選的資料改用 taxonomy(分類法):例如商品的顏色、尺寸這類用來過濾的屬性,用分類法儲存會比塞進自訂欄位查得快得多,因為分類法本身就有為查詢設計的表結構與索引。
- 結構複雜或資料量大的改用自訂資料表:當某種資料筆數動輒上百萬、又經常被複雜條件查詢,把它從 postmeta 搬到專屬的自訂表,針對查詢需求建好索引,效能差距會很明顯。實務上也有人選擇把多次 JOIN 才能組出來的結果,先在一張彙整表裡算好存著,查詢時直接撈,等於用一點寫入成本換大量讀取速度。
- 拆成多段查詢在程式裡組合:如果一條查詢因為太多 JOIN 而慢,有時把它拆成兩三條分開執行、再用 PHP 把結果合併過濾,反而比硬擠在一條 SQL 裡更快。
判斷原則很簡單:如果同一類慢查詢反覆出現、加索引也只是把時間從很慢壓到還是慢,那問題八成在資料模型,該往上層去改,而不是繼續在查詢層打補丁。
用快取讓重複查詢只跑一次
對於那些結果不太變、卻被反覆執行的查詢,最有效的優化不是讓它跑得更快,而是讓它根本不用一直跑。這就是快取的價值。Query Monitor 的重複查詢面板如果列出一堆同樣的查詢,通常就是快取沒做好的訊號。
WordPress 生態裡常見的兩種做法是物件快取與暫態快取。物件快取搭配 Redis 或 Memcached,把查詢結果存進記憶體,一條原本一天要打資料庫一千次的查詢,可以收斂成只打一次、其餘從記憶體取,對高流量站尤其有感。暫態快取(Transient)則適合那種計算昂貴、但結果允許過一段時間才更新的查詢,用 get_transient 先看有沒有快取,沒有才真的查資料庫、再用 set_transient 把結果存起來並設定有效期限。
要注意的是,快取改變的是「同一筆資料被讀幾次」,並不會讓一條本身就寫得很爛的查詢變好。正確順序是先把慢查詢本身改對(該加索引加索引、該改資料模型改模型),再用快取去降低它被執行的次數,兩者疊起來才是完整的優化。
WooCommerce 商店的慢查詢有哪些要特別處理
WooCommerce 站因為資料結構更複雜、商品與訂單量也大,慢查詢通常比一般部落格嚴重,有幾個地方值得特別處理。
第一是啟用高效能訂單儲存(HPOS)。傳統上 WooCommerce 把訂單當成自訂文章類型存在 wp_posts 與 wp_postmeta,資料一多查詢就吃力。HPOS 改用專為訂單設計的自訂資料表,附帶專屬索引,讀寫次數更少。從 WooCommerce 8.2(2023 年 10 月)起,新安裝預設就會啟用;既有商店要切換,得先在「WooCommerce、設定、進階、功能」裡開啟相容模式,讓訂單資料先同步到新舊兩套表,確認沒問題後再正式切過去。
第二是清理 wp_options 的自動載入資料。每次頁面載入都會撈出所有自動載入選項,偏偏這張表預設沒有為自動載入欄位建索引。WordPress 會把全部自動載入選項快取在一個叫 alloptions 的鍵裡,但只要任何一個外掛更新了其中一個自動載入選項,整包 alloptions 快取就會失效、得重新撈一次。經營一段時間的 WooCommerce 站,自動載入資料累積到好幾 MB 並不罕見,加上移除過的舊外掛常留下沒清乾淨的選項與資料表,會讓這條查詢越來越沉。定期檢查自動載入資料的大小、清掉孤兒選項,是電商站維護的基本功。
第三是前面提過的多重 JOIN 與 postmeta 查詢,在商品篩選、會員訂閱這類場景特別明顯,處理邏輯跟前面一致:先用 Query Monitor 或慢查詢日誌定位,再用 EXPLAIN 看執行計畫,能加索引就加,加不動就考慮 HPOS 或自訂表。如果商店有金流、訂閱等收款功能,相關查詢同樣會反映在資料庫負載上,但優化的著力點仍是查詢與索引本身,跟付款流程的設定是兩回事。
找出元兇之後,下一步該怎麼走
WordPress 慢查詢優化不是玄學,它有一條清楚的路徑:先用 Query Monitor 或慢查詢日誌把問題查詢攤開,用「時間乘以次數」找出真正的元兇,再用 EXPLAIN 讀懂它慢在全表掃描還是缺索引;接著判斷該加索引、改寫查詢、換資料模型,還是用快取讓它少跑幾次。順序對了,往往不必動到主機規格就能換來明顯的速度提升。
別等到網站卡到使用者抱怨才開始查。挑一個流量大或後台操作最有感的頁面,今天就裝上 Query Monitor 跑一遍,把紅色標示的那幾條查詢先抓出來。光是處理掉最拖累的一兩條,通常就足以讓整個站感覺輕快不少;剩下的,再依照上面的判斷邏輯一條條收拾即可。