網站愈跑愈慢,第一個被懷疑的往往是資料庫,於是很多人打開外掛按下「最佳化」,看著磁碟用量數字往下掉,覺得任務完成。問題是,那串數字變小,多半跟頁面快不快沒什麼關係。真正吃掉每一次查詢時間的,是少數幾個你沒注意到的設定與資料表結構,而不是那幾千筆被刪掉的修訂版。
MySQL 資料庫調校之所以難,是因為網路上流傳的建議大半過時,甚至有些在十年前就是錯的,套到現在的 InnoDB 引擎只會幫倒忙。這篇用 WordPress / WooCommerce 站的實際情境,把調校拆成三條真正有效的主軸:把熱資料留在記憶體裡的緩衝池、找出拖垮頁面的慢查詢、以及針對 wp_options 與 wp_postmeta 這兩張肥大資料表的索引處理。不談那些只會讓 phpMyAdmin 數字變漂亮、頁面卻沒變快的儀式性操作。
InnoDB 緩衝池該設多大才不會反效果
緩衝池是整個 MySQL 調校裡唯一一個「設對就有感、設錯會出事」的參數,先把它弄對。InnoDB 是現行 WordPress 預設的儲存引擎,它把資料頁與索引快取在一塊叫做 buffer pool 的記憶體區,命中愈高,查詢就愈不需要回頭去敲硬碟。對讀取為主的 WordPress 站來說,緩衝池命中率高,整體反應速度就會明顯不同。
控制它大小的參數是 innodb_buffer_pool_size。很多教學直接給一句「設成系統記憶體的 70% 到 80%」,這句話只在一種情況下成立:那台機器是專門跑資料庫的獨立伺服器。一般的 WordPress 主機上同時還跑著 PHP-FPM、Nginx 或 Apache、可能還有 Redis 與作業系統本身,把 80% 記憶體丟給 InnoDB,等於把其餘程序逼到牆角。記憶體一旦不夠,Linux 核心的 OOM killer 就會開始殺行程,常常是 PHP-FPM 先倒,或是 MySQL 自己被殺掉,網站直接 502。
實務上的抓法是看「這台機器扣掉其他程序後,還能安心給資料庫多少記憶體」,再回頭看資料量。以下是常見的安全起點,實際值仍要依流量與資料庫總大小調整:
| 伺服器記憶體 | 緩衝池建議起點 | 備註 |
|---|---|---|
| 2 GB(共享或入門 VPS) | 256–512 MB | PHP 與網頁伺服器同機,別貪心 |
| 4 GB | 1–1.5 GB | 多數中小型 WooCommerce 站的甜蜜點 |
| 8 GB | 2–3 GB | 資料量大可往上加 |
| 16 GB 以上 | 4–6 GB | 接近獨立資料庫機才考慮逼近上限 |
判斷有沒有設夠,不要憑感覺,看命中率。連進 MySQL 後查詢 SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';,用 Innodb_buffer_pool_reads(真的去敲硬碟的次數)除以 Innodb_buffer_pool_read_requests(總讀取請求)。這個比值愈接近 0 愈好;若硬碟讀取占比偏高,代表熱資料塞不進緩衝池,這時加大才有意義。反過來說,若資料庫總大小只有 800 MB,緩衝池開到 4 GB 也是浪費,多出來的記憶體該留給其他程序。
另一個常被一起調的是 innodb_log_file_size(MySQL 8 改用 innodb_redo_log_capacity)。重做日誌太小,寫入頻繁時會逼迫 InnoDB 頻繁刷新,拖慢寫入;對有訂單寫入的 WooCommerce 站,把它從預設往上拉到數百 MB 到 1 GB 等級,通常比硬調緩衝池更有感。
為什麼刪修訂版和最佳化資料表多半沒用
先講結論,省得你白花力氣:在 InnoDB 上跑 OPTIMIZE TABLE、大量刪修訂版、清垃圾留言,這三件最常被推薦的事,對查詢速度幾乎沒有幫助。理解原因之後,你就知道該把時間花在哪裡。
OPTIMIZE TABLE 的設計初衷是替 MyISAM 這種舊引擎重整碎片、回收空間。但 WordPress 從 MySQL 5.5 之後就不再預設使用 MyISAM,現在你的資料表幾乎都是 InnoDB。InnoDB 用的是叢集索引,資料就存在索引結構裡,刪除資料後它會把空間標記為可重用,並不會產生需要重整的傳統碎片。對 InnoDB 下 OPTIMIZE TABLE,實際上是觸發一次整張表重建:建新表、複製全部資料、交換、刪舊表,過程中整張表被鎖住、磁碟 I/O 暴增,還需要與原表等量的暫存空間。換來的效能提升?通常是零。唯一值得做的時機,是你剛刪掉某張表九成以上的資料、想把磁碟空間要回來,那也是為了空間,不是為了速度。
刪修訂版同理。文章修訂版跟已發布文章一起存在 wp_posts,但前台查詢文章時,WHERE 條件會用 post_status 與 post_type 把修訂版排除在外。換句話說,那些修訂版根本不參與一般查詢,刪掉一千筆修訂版,查詢一樣不會變快,你只是讓資料表小了一點。垃圾留言(comment_approved = 'spam')與垃圾桶文章(post_status = 'trash')也是相同道理,前台查詢的 WHERE 早就把它們濾掉了。
這些清理唯一會有感的場景,是後台特定畫面:單篇文章如果累積了上百個修訂版,編輯頁要把它們全載出來會卡;留言審核頁若有十萬筆垃圾留言也會慢。對應做法是治本而非治標——在 wp-config.php 設 define('WP_POST_REVISIONS', 15); 限制每篇修訂版上限,而不是定期全站亂刪。至於那些號稱「一鍵刪除孤兒 postmeta」的清理外掛要特別小心,有些外掛會刻意寫入沒有對應文章的 postmeta 當作快取或設定,被當成孤兒刪掉後功能就壞了;真要清,先在測試環境確認你刪的到底是什麼。
wp_options 自動載入:最被忽略卻最有感的優化
如果只能做一件資料庫優化,做這件。wp_options 裡每一筆 autoload 設為 yes 的設定,WordPress 在每一次頁面請求都會全部載入記憶體。這份資料包含站台設定、啟用中的外掛清單、佈景主題選項,還有不少外掛塞進來的快取。一旦某個寫得不講究的外掛把幾 MB 的序列化資料以自動載入方式存進來,等於每開一頁就先從資料庫搬幾 MB 出來,這個成本攤在每一個訪客身上。
先量再說。連進資料庫跑這句看自動載入總量:
SELECT SUM(LENGTH(option_value)) / 1024 / 1024 AS autoload_mb
FROM wp_options WHERE autoload = 'yes';
健康的站台這個值通常在 800 KB 以下;超過 2 MB 就該處理。接著揪出元凶,看哪些自動載入選項最肥:
SELECT option_name, LENGTH(option_value) / 1024 AS size_kb
FROM wp_options WHERE autoload = 'yes'
ORDER BY LENGTH(option_value) DESC LIMIT 20;
排在前面的通常是外掛留下的快取或落漆設定。確認某筆不需要每頁載入後,可以把它的 autoload 改成 no,例如 UPDATE wp_options SET autoload = 'no' WHERE option_name = '某個肥選項';。把自動載入量從 3 MB 降到 800 KB 以內,每次頁面產生時間實測可省下數十毫秒,這是真實、可量測的提升,偏偏多數最佳化外掛完全不碰這塊。
過期的瞬態快取(transient)是另一類真正值得清的資料。瞬態本該自動到期,但外掛有 bug 或被棄用時常常留下一堆過期卻沒被回收的紀錄,而它們往往也掛著自動載入。這類過期 transient 清掉是有實質效益的,因為它直接減少每次請求要載入的資料。清理時用 SQL 比對 _transient_timeout_ 開頭、值已小於目前時間戳的紀錄即可。這裡的差別要分清楚:清過期 transient 與降低自動載入量是真的有用,刪修訂版與跑 OPTIMIZE TABLE 是做心安的。
慢查詢日誌怎麼開、怎麼讀出問題
調設定的天花板有限,真正能換來十倍、百倍差距的,是揪出並修掉拖垮頁面的那幾條查詢。一條對兩百萬列做全表掃描的查詢,緩衝池調得再漂亮它都會慢,因為問題在查詢本身,不在設定。
第一步是讓慢查詢現形。在設定檔(MySQL 的 my.cnf,MariaDB 多半在 mariadb.conf.d/50-server.cnf)的 [mysqld] 區段加入:
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow-queries.log
long_query_time = 1
log_queries_not_using_indexes = 1
long_query_time = 1 表示執行超過 1 秒的查詢會被記下;診斷初期可以先設寬一點抓大魚,再慢慢往下調到 0.5 甚至更低。log_queries_not_using_indexes 會額外記下沒用到索引的查詢,這類查詢就算當下不到 1 秒,量一大照樣是隱患。改完重啟服務讓設定生效,跑一段時間累積樣本。
第二步是讀日誌。原始日誌一條條看會瘋掉,伺服器上若能裝 Percona Toolkit,用 pt-query-digest /var/log/mysql/slow-queries.log 會把相似查詢歸類、依總耗時排序,一眼看出「哪一種查詢被跑了幾千次、總共吃掉多少時間」。沒有 root 權限的共享主機,退而求其次可以在 WordPress 後台裝 Query Monitor 這類開發者工具外掛,它會在每個頁面底部列出該頁實際發出的查詢、各自耗時、以及是哪個外掛或主題觸發的,定位 N+1 問題(同一種查詢在迴圈裡被跑上百次)特別好用。
第三步是用 EXPLAIN 看執行計畫。把抓到的慢查詢前面加上 EXPLAIN 再跑一次,重點看 type 欄位與 rows 欄位:type 出現 ALL 代表全表掃描,rows 數字很大代表它掃了大量資料列才找到答案。理想情況是 type 顯示 ref 或 range、有用到 key(索引)、rows 數字小。如果一條前台關鍵查詢落在全表掃描,那就是下一節要處理的對象。修查詢的優先順序永遠高於調設定——把八成力氣放在查詢與索引、兩成放在設定檔,方向才對。
WordPress 兩張肥大資料表的索引與結構處理
WordPress 核心資料表針對核心查詢本來就建好了索引,會出問題的幾乎都是 wp_options 與 wp_postmeta 這兩張被外掛養肥的表,針對它們動手才划算。索引就像書的目錄,有目錄資料庫能直接翻到對應頁,沒目錄就得從頭翻到尾(全表掃描)。
wp_options 除了前面講的自動載入瘦身,舊版 WordPress 在 autoload 欄位上沒有合適索引,導致每次撈自動載入選項時效率不佳。較舊的站可以補一個複合索引幫忙:
CREATE INDEX autoload_idx ON wp_options (autoload, option_name);
新版 WordPress 已內建相關索引,下手前先用 SHOW INDEX FROM wp_options; 確認還沒有同性質的索引再建,避免重複。
wp_postmeta 是更常見的痛點。WordPress 以文章為中心,文章、頁面、附件、WooCommerce 商品與訂單全寫進 wp_posts,而它們的自訂欄位(含 ACF 建立的欄位)通通塞進 wp_postmeta,內容一多這張表肥得飛快。它的 meta_key 欄位若用了過長的字元長度,建索引時可能撞上索引長度上限而失敗。在確認站上沒有任何 meta_key 長度超過 191 字元的前提下,可以把欄位收斂,讓索引建得起來:
ALTER TABLE wp_postmeta MODIFY meta_key VARCHAR(191);
更根本的問題出在用 WP_Query 的 meta_query 來篩文章。因為 wp_postmeta 的 meta_value 是 longtext 型別,理論上限接近 4 GB,幾乎等於不設限,不少外掛還會把序列化或 JSON 資料整包塞進去。當資料量一大,拿 meta_query 去比對這種欄位,資料庫負擔會非常重,這也是 WooCommerce 持續把訂單與商品資料從 postmeta 拆成獨立資料表(HPOS 高效能訂單儲存)的原因。
如果你有大量、頻繁、條件複雜的自訂欄位查詢需求,治本的做法是替那些欄位另建專屬資料表,並透過 posts_where 這類勾點接回 WP_Query,讓使用方式不變、底層卻改查自己那張精簡的小表。社群已有實作驗證,這種自訂資料表的查詢速度約為 meta_query 的十倍。用 ACF 的人也不必全手刻,市面上有外掛能把 ACF 欄位一鍵拆分到獨立資料表並相容原本的 get_field()、update_field() API,使用習慣不用改。要留意的是,多數此類外掛預設仍會在 wp_postmeta 留一份相容用的資料,且拆分後既有舊資料不會自動搬移,需要手動更新一次文章才會寫入新表,資料量大的站要先評估遷移成本。
MySQL 與 MariaDB 的調校差異與一份務實的設定
MySQL 與 MariaDB 對 WordPress 來說九成設定通用,但有幾個地方別搞混。最容易踩的是查詢快取(query cache)。早年的教學幾乎都叫你開 query_cache_size = 128M,這個建議現在是有害的:MySQL 8.0 已經把查詢快取整個移除,5.7 也標為棄用;而它需要全域鎖,網站只要有寫入(瀏覽次數、留言、外掛更新都算),就會不斷讓快取失效並引發鎖競爭,對 WordPress 這種讀寫混合的站反而拖慢全體。MySQL 上請設 query_cache_type = 0 直接關掉。MariaDB 仍保留查詢快取且預設多為關閉,沒有特殊理由不要去開它,頁面層級的快取交給快取外掛或 CDN 處理更恰當。
另一個常見的錯誤調整是看到連線數快滿就把 max_connections 往上加。每條連線都要吃掉排序、讀取等緩衝區的記憶體,把上限從 151 拉到 1000,光連線開銷就可能吃掉數百 MB,而這些記憶體是從緩衝池挖來的——你用換不到的連線容量,換掉了真正影響查詢速度的快取。連線會逼近上限通常代表 PHP-FPM 行程數沒控好或有慢查詢卡住,治本是去修那個源頭,而不是調高上限把問題藏起來。
下面這份設定以「一台 8 GB 記憶體、同時跑 WordPress 與資料庫」的中型站為例,作為起點而非標準答案,每個值都要依你的監測結果回頭調整:
[mysqld]
innodb_buffer_pool_size = 2G
innodb_buffer_pool_instances = 2
innodb_log_file_size = 512M
innodb_flush_method = O_DIRECT
innodb_flush_log_at_trx_commit = 2
innodb_file_per_table = ON
max_connections = 100
thread_cache_size = 16
table_open_cache = 2000
tmp_table_size = 64M
max_heap_table_size = 64M
query_cache_type = 0
query_cache_size = 0
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow-queries.log
long_query_time = 1
其中 innodb_flush_log_at_trx_commit = 2 是用一點點崩潰時的資料安全性換寫入效能,對部落格、內容站通常可接受;但金流、訂單這類每一筆都不能掉的寫入,建議維持預設值 1,以資料完整性為先。tmp_table_size 與 max_heap_table_size 兩者要一起調且設成相同值,這控制 MySQL 把排序、暫存資料留在記憶體裡的上限,設太小複雜查詢會頻繁寫暫存表到硬碟,反而變慢。
調完不要憑感覺判斷有沒有效。伺服器若有權限,可以跑 MySQLTuner 這支 Perl 腳本,它會分析目前的設定與實際運行狀態,給出一份參數建議與安全提醒;但要記住它是依「當下狀態」給建議,不能照單全收,尤其它常會建議調高連線數或重開查詢快取,這兩項對 WordPress 多半要反著做。把它當作健檢報告參考,最終仍以你自己的慢查詢日誌與緩衝池命中率為準。
從哪裡開始動手,又該在哪裡收手
把力氣按效益排序,順序大致是這樣:先量 wp_options 自動載入大小、清掉過期 transient,這是最快見效又零風險的一步;接著把 innodb_buffer_pool_size 依實際可用記憶體與資料量設到合理值,並用命中率驗證;然後打開慢查詢日誌跑一段時間,用 EXPLAIN 找出全表掃描的查詢,針對它補索引或改寫;最後才回頭微調設定檔,並把查詢快取關掉、別亂加連線數。至於 OPTIMIZE TABLE、全站刪修訂版、各種一鍵清理外掛,除非你清楚知道自己在解決哪一個被量測出來的問題,否則放著就好。
資料庫調校的價值不在於把 phpMyAdmin 裡的數字改漂亮,而在於讓每一次查詢更快回到訪客眼前。先量、再改、改完再量,是貫穿緩衝池、慢查詢、索引這三條主軸的同一個原則。下次網站變慢時,先用 Query Monitor 看一眼這頁到底發了哪些查詢、慢在哪,再決定要不要動資料庫——很多時候你會發現真正的瓶頸根本在沒裝頁面快取或圖片沒壓縮,那又是另一條更該優先處理的路了。