在所有資料庫維運操作裡,「加一個索引」大概是最常被當成無腦安全動作的一種——不改資料、不改邏輯,理論上只是幫查詢加速,怎麼想都不該出事。今天的案例,就是這個「理論上」翻車的具體過程。
PROCESSLIST 的 STATE 欄位分辨「還在跑」跟「被鎖卡住」情境是這樣:某個開發環境的資料庫實例,buffer pool 只設定了 512MB——這是一個相對受限的規格。對一張已經累積到千萬列以上的大表下 ADD INDEX,這個操作直接把整台實例的 I/O 資源吃光,連完全不相關的其他資料庫、簡單到只是 MAX() 這種聚合查詢,都撞上了 120 秒逾時。
這裡最容易被忽略的判斷落差:「加索引是安全的維運操作」這句話本身沒有錯,錯的是把它當成一個放諸四海皆準的結論,而沒有把「這個操作在這個環境的資源規格下,是不是還安全」納入查證範圍。 索引本身的語意沒有風險,但建索引這個過程需要的 I/O 資源,是跟表的大小、索引的複雜度、當下實例的資源餘裕直接相關的——這正是主題句的另一種樣貌:「已確認安全」的範圍,只涵蓋了「這個操作語意上做什麼」,沒有涵蓋「這個操作在這個具體環境下要付出什麼代價」。
用一組對照來看差異:
❌ 籠統的安全判斷:
「加索引是安全操作,不改資料不改邏輯,可以直接下。」
→ 判斷只涵蓋了操作的語意,沒有把「這張表多大」
「這個環境的資源規格是什麼」納入考量
✅ 把環境規格納入判斷:
「加索引本身安全,但這張表有千萬列以上,
這個環境的 buffer pool 只有 512MB,
這個組合會不會讓建索引的 I/O 需求超過實例能承受的範圍?
先查一下這個環境的資源規格,再決定要不要直接下。」
→ 判斷範圍涵蓋了操作本身跟執行環境的交互影響
當時第一個直覺反應是調整跟 DDL 建構過程相關的某個 session 層級參數,期待能緩解 I/O 壓力——但實際調整後完全沒有改善。原因是瓶頸不在這個 session 變數控制的範圍內,而是實例本身的資源規格。 session 層級的參數能調整的是「這個連線在建構索引時,怎麼使用它被分配到的資源」,但如果問題根源是「整台實例的資源餘裕本來就不夠」,那不管怎麼調整這個連線內部的行為,都動不了那個更上層的瓶頸。
這個細節值得記住的原因,是它示範了另一種「診斷方向可能沒錯,但解法用錯層級」的情況——跟 Day 24 講的「加鎖降低症狀頻率但沒解決根因」是類似的教訓:調整一個看起來相關的參數、症狀有沒有改善,本身就是一個檢驗訊號——如果調了沒用,代表問題不在這個參數能控制的層級,該往上一層去找瓶頸,而不是繼續在同一個參數上打轉。
跟「建索引拖垮整台實例」形成強烈對比的是:刪除索引幾乎是瞬間完成的,因為 DROP INDEX 本質上只是一個中繼資料操作——資料庫只需要更新內部記錄「這個索引不存在了」,不需要真正去掃描、重建任何資料結構。建索引需要真正走過整張表的資料、排序、寫入索引結構,兩者在計算量上完全不對稱。
這個不對稱本身是個值得記住的技術細節:「加」跟「拿掉」同一個東西,代價可以天差地遠,不能用「反正是同一種操作的正反面」來類推風險。
事後回頭看,這次事件也留下一個實用的診斷技巧:透過 PROCESSLIST 檢查資料庫當下在做什麼時,光看 TIME(已經跑了多久)跟 INFO(執行的語句是什麼)這兩個欄位,沒辦法分辨「這個操作真的還在正常執行中」跟「這個操作其實已經卡住、在等一個拿不到的鎖」——兩種情況下 TIME 都會持續累加,INFO 也都會顯示同一句語句。
真正能分辨這兩種情況的,是 STATE 欄位:如果顯示的是類似「正在改表」這種狀態,代表操作確實還在進行中;如果顯示的是類似「等待資料表中繼資料鎖」這種狀態,代表這個操作根本沒有在推進,只是卡在等別的連線釋放鎖。這兩種狀況需要的處理方式完全不同——前者只能等,後者要去找是誰持有那個鎖、要不要介入處理。
回想你上一次執行一個「理論上安全」的維運操作:你有沒有先確認過,這個操作在當下環境的資源規格下,是不是也真的安全?還是只憑操作本身的語意(不改資料、不改邏輯)就判斷「應該沒問題」?
DROP INDEX 幾乎瞬間完成、ADD INDEX 可能拖垮整個實例,兩者計算量完全不對稱,不能用「反正是同一種操作」類推風險PROCESSLIST 的 STATE 欄位(不是 TIME/INFO)才能分辨操作是「還在跑」還是「被鎖卡住」明天是另一種樣貌的案例:一個日誌去重的分組鍵設計錯誤,只有在正式環境的真實資料規模跟樣態下才會踩到——本機測試環境完全看不出問題。