2026年9月18日 星期五

面試被問 MySQL 24 個高頻坑點,為什麼全答對的工程師依然拿到感謝信?

在資深 Laravel 工程師或架構師的面試中,資料庫(MySQL)永遠是決勝負的主戰場。

很多工程師為了準備面試,把 MySQL 的各項機制背得滾瓜爛熟。但你可能不知道:背出這 24 個知識點,只代表你「讀過書」;面試官在找資深工程師時,看重的是「你能不能在複雜業務中做權衡(Trade-off),以及你有沒有一套解決問題的底層思考鏈」。

如果只會死記硬背,這 24 個陷阱就會從「你的武器」變成「面試官發你感謝信的理由」。

以下整理了 24 個從實務到面試都極度高頻的 MySQL 陷阱,並附上「拿到感謝信的背誦回答」與「拿Offer 的資深架構師思考」的強烈對比:

一、 索引(Index)的盲點與陷阱

1. 有 Index ≠ 一定會使用 Index

  • 坑點: 對欄位做了運算或函式處理(如 WHERE LOWER(email) = '...' 或 WHERE DATE(created_at) = '2026-09-18'),會讓 B-Tree 索引失效;字串欄位傳數字(WHERE phone = 0912345678)觸發隱式轉型,也會導致全表掃描。

  • ❌ 感謝信回答: 「遇到運算或轉型,MySQL 就不走索引了,所以程式碼絕對不能這樣寫。」

  • ⭕ 架構師思考: 說明轉型導致索引失效的原理,並提出解法:「若業務無法避免(如動態日期或模糊比對),應評估 MySQL 8.0+ 的 Functional Index(功能索引),或將運算移至應用層(如 PHP 預處理參數),從根源保持欄位乾淨。」

2. LIKE '%keyword%' 的 B-Tree 特性

  • 坑點: B-Tree 索引是「由左至右」排序。LIKE 'iphone%' 能利用前綴匹配快速定位;LIKE '%iphone%' 前面帶 %,B-Tree 無法利用排序特性,直接退化成全表掃描。

  • ❌ 感謝信回答: 「LIKE '%...' 沒救了,一定會全表掃描,只能禁止前端這樣查。」

  • ⭕ 架構師思考: 結合業務數據量做分流:「小數據量下全表掃描 IO 尚可接受;大數據量或高頻搜尋時,不該硬凹 B-Tree,應在 MySQL 引入 FULLTEXT 全文索引,或將搜尋需求解耦,引入 Elasticsearch / OpenSearch 等專用搜尋引擎。」

3. 最左匹配原則(Leftmost Prefix Rule)

  • 坑點: 組合索引 INDEX(country, status, created_at) 像是一棵樹:先排 country,再排 status,最後排 created_at。WHERE status = 'active' 跳過了最左邊的 country,無法使用該索引。

  • ❌ 感謝信回答: 「背熟最左匹配,第一個欄位沒查到,索引就失效。」

  • ⭕ 架構師思考: 理解原理並提醒邊界:「除了最左匹配,還要注意 Skip Scan(MySQL 8.0 針對低 Cardinality 欄位自動跳躍掃描)的優化機制,但實務上記錄查詢 Pattern、設計正確的欄位順序才是根本。」

4. 組合索引的順序不是隨便排的

  • 坑點: 不能只因為「這三個欄位都會查」就隨意排 INDEX(A, B, C)。

  • ❌ 感謝信回答: 「只要把常用的欄位放在最左邊就好了。」

  • ⭕ 架構師思考: 綜合評估三要素:

    1. Query Pattern: 等值查詢(=)放在範圍查詢(>、LIKE)前面。

    2. Cardinality & Selectivity(選擇性): 高 Cardinality(重複值少,如 user_id)放前面,低 Cardinality(如 status)放後面。

    3. Order By / Group By: 順便利用 B-Tree 的排序特性消除耗時的 filesort。

5. ORDER BY 有 Index ≠ 不需要排序

  • 坑點: INDEX(user_id, created_at) 在 WHERE user_id = 100 ORDER BY created_at DESC 時表現完美。但如果是 WHERE status = 'active' ORDER BY created_at DESC,只要 status 篩選出的資料量過大,Optimizer 評估後仍可能選擇內建排序算法,跳出 Using filesort。

  • ❌ 感謝信回答: 「只要在 Order By 的欄位加上索引,就絕對不會出現 Using filesort。」

  • ⭕ 架構師思考: 理解成本模型(Cost Model):「Using filesort 是 Optimizer 權衡『讀 Index 後回表(Bookmark Lookup)』與『全表/範圍掃描後在記憶體 Sort Buffer 排序』兩種 IO 成本後的決策。資料量大時,應考慮 covering index(覆蓋索引)來避免回表。」

二、 執行計畫(EXPLAIN)與誤區

6. 看到 type = ALL 不代表一定有問題

  • 坑點: type = ALL 代表 Full Table Scan(全表掃描)。

  • ❌ 感謝信回答: 「type = ALL 是災難!看到 ALL 一定是 Bug,一定要加索引!」

  • ⭕ 架構師思考: 「如果 Table 只有 50 筆資料,MySQL 評估『全表掃描』的 IO 成本,比『先讀 Index 再回表』還要低,選擇全表掃描才是最適決策。評估 Execution Plan 必須結合資料規模與 Cost,不能只看指標硬套。」

7. COUNT(*) vs COUNT(id) 的迷思

  • 坑點: 在 InnoDB 中,「COUNT(id) 比 COUNT(*) 快」是常見錯誤傳言。

  • ❌ 感謝信回答: 「COUNT(id) 因為走主鍵(PK),所以一定比 COUNT(*) 快。」

  • ⭕ 架構師思考: 清楚語意與底層優化:「COUNT(*) 計算總行數,COUNT(col) 僅算非 NULL 行數。MySQL Optimizer 對 COUNT(*) 做過極度優化,會自動選擇最小的次級索引(Secondary Index)來掃描,效能通常等於或優於 COUNT(id)(因為主鍵 Tree 通常包含所有資料,葉子節點較大)。」

8. 寫成一行 SQL 不代表比較快

  • 坑點: 複雜的單行 SQL(過多 Subquery 或 JOIN)可能導致 Optimizer 產生極度低效的執行計畫。

  • ❌ 感謝信回答: 「盡量把邏輯寫在一行 SQL 裡發給 DB,減少網路往返(RTT)一定是最好的。」

  • ⭕ 架構師思考: 「SQL 是聲明式語言,Execution Plan 才是執行程序。過度複雜的 SQL 會讓 Optimizer 猜錯統計資訊。必要時在應用層(Laravel)拆成 2~3 個簡單查詢,或利用 Temporary Table 進行分步處理,可讀性更好,DB 負擔也更小。」

三、 三值邏輯與 JOIN 的語意陷阱

9. NULL 的三值邏輯(TRUE, FALSE, UNKNOWN)

  • 坑點: SQL 中的 NULL 代表「未知」。NULL = NULL 的結果是 UNKNOWN,而不是 TRUE。

  • ❌ 感謝信回答: 「NULL 就是空的,用 = 查不到是因為語法規定要寫 IS NULL。」

  • ⭕ 架構師思考: 從資料庫設計層面思考:「理解三值邏輯對條件判斷(如 WHERE、CASE WHEN)的影響。在 Table Schema 設計時,儘量將欄位宣告為 NOT NULL 並提供 Default Value,避免 NULL 帶來索引計算與三值邏輯的額外複雜度。」

10. NOT IN 遇到 NULL 的毀滅性陷阱

  • 坑點: SELECT * FROM users WHERE id NOT IN (SELECT user_id FROM orders); 如果 orders.user_id 中有一筆是 NULL,整個 NOT IN 子查詢結果會直接變成空集合(0 rows)!

  • ❌ 感謝信回答: 「遇過這個 Bug,背起來就好:改寫成 NOT EXISTS。」

  • ⭕ 架構師思考: 剖析邏輯底層:「NOT IN 遇到 NULL 會展開成 id != NULL(即 UNKNOWN),導致整個邏輯條件失效。防禦性編程的最佳實踐是:排除邏輯優先採用 NOT EXISTS 或 LEFT JOIN ... WHERE right.id IS NULL。」

11. LEFT JOIN 的 WHERE 條件會將其隱式轉為 INNER JOIN

  • 坑点: SELECT users.* FROM users LEFT JOIN orders ON orders.user_id = users.id WHERE orders.status = 'paid'; 中,WHERE orders.status = 'paid' 會強制排除沒有訂單的 NULL 列(NULL = 'paid' 為 false),導致 LEFT JOIN 實質退化成 INNER JOIN。

  • ❌ 感謝信回答: 「這就是寫錯地方,把條件貼到 ON 就解決了。」

  • ⭕ 架構師思考: 清楚區分 ON 與 WHERE 的執行順序:「ON 是在構建 JOIN 臨時表時的過濾條件;WHERE 是在臨時表構建完成後的結果集過濾。保留主表資料的條件必須放在 ON,過濾最終結果的才放 WHERE。」

12. 不要背「INNER JOIN 一定比 LEFT JOIN 快」

  • 坑點: 單純比較 JOIN 關鍵字的快慢是沒有意義的。

  • ❌ 感謝信回答: 「能用 INNER JOIN 就不要用 LEFT JOIN,因為 INNER JOIN 比較快。」

  • ⭕ 架構師思考: 理解驅動表(Driven Table)機制:「JOIN 效能取決於 Optimizer 對小表驅動大表(Driving Table Selection)的判斷、JOIN 欄位是否有 Index,以及 Block Nested-Loop / Hash Join 的利用。兩者核心差異在於業務語意(是否需要保留未匹配列),而非單純快慢。」

四、 交易(Transaction)、鎖(Lock)與併發控制

13. Transaction ≠ Lock

  • 坑點: 呼叫 DB::transaction() 只是開啟交易(ACID 邊界),不會自動鎖住所有讀取的資料。

  • ❌ 感謝信回答: 「只要把代碼包在 DB::transaction() 裡面,就不會有 Race Condition 了。」

  • ⭕ 架構師思考: 精準區分兩者:「Transaction 控制的是隔離性(Isolation)與原子性;Lock(或 Atomic Update)才是控制多個併發交易競爭同一筆 Row 存取順序的手段。只開 Transaction 不加鎖,1,000 個請求依然會讀到舊資料。」

14. SELECT ... FOR UPDATE 不是萬能鎖

  • 坑點: 使用悲觀鎖時,若未命中索引,InnoDB 可能會鎖住整個掃描範圍,甚至升級為類似 Table Lock 的效果;且受隔離層級(Isolation Level)影響會產生 Gap Lock / Next-Key Lock。

  • ❌ 感謝信回答: 「怕資料被改,查詢後面加上 lockForUpdate() 就萬無一失了。」

  • ⭕ 架構師思考: 評估悲觀鎖的副作用:「lockForUpdate() 必須確保:1. 在 Transaction 內、2. 查詢精準命中 Unique Index(退化為 Row Lock)。若條件不佳導致 Gap Lock,在高併發下會導致 Connection Pool 耗盡與大量死鎖。」

15. 死鎖(Deadlock)的根本解法

  • 坑點: 當 Transaction A 與 B 以相反順序獲取資源時,就會觸發 Deadlock。

  • ❌ 感謝信回答: 「遇過死鎖,用 try-catch 抓到 DeadlockException 然後做 retry 就好了。」

  • ⭕ 架構師思考: 由內而外的防禦策略:

    1. 業務/架構層面: 嚴格規範全站所有 Transaction 存取資源的固定順序(Lock Order)。

    2. 縮短臨界區間: 交易內只放必要的 DB 操作,剔除第三方 API/HTTP 呼叫,減少持有鎖的時間。

    3. 機制層面: 搭配 Laravel DB::transaction(..., attempts: 3) 進行有限次自動重試。

16. MVCC(多版本併發控制)與一致性讀取

  • 坑點: InnoDB 預設使用 MVCC 達成高併發讀寫。

  • ❌ 感謝信回答: 「不太清楚 MVCC,只知道 SELECT 拿到的都是最新資料。」

  • ⭕ 架構師思考: 區分 Consistent Read 與 Locking Read:

    • 一致性讀取(Consistent Read): 普通 SELECT 讀取的是 Undo Log 生成的快照(Snapshot),完全不加鎖,也不會被其他交易的 UPDATE 阻塞。

    • 鎖定讀取(Locking Read): FOR UPDATE 或 FOR SHARE 會讀取最新實時資料並加鎖。

五、 高併發實戰:超賣與一致性

17. 「資料庫有 Index,所以不會超賣」是經典謬誤

  • 坑點: if ($product->stock > 0) { $product->decrement('stock'); } 存在嚴重的時間差(Race Condition)。

  • ❌ 感謝信回答: 「只要 stock 欄位有加索引,或者有寫在 Transaction 裡,就不會超賣。」

  • ⭕ 架構師思考: 利用資料庫底層原子性(Atomic Update):

    SQL
    UPDATE products SET stock = stock - 1 WHERE id = 1 AND stock > 0;
    
    執行後檢查 affected_rows 是否為 1。利用 DB 底層的 Row Lock 隔離性,在 SQL 條件中直接做邊界檢查。

18. 分散式鎖(Redis Lock)不能取代 DB 資料庫一致性

  • 坑點: 過度依賴快取鎖。

  • ❌ 感謝信回答: 「我們前面擋了 Redis 分散式鎖,所以 DB 不需要做任何防護。」

  • ⭕ 架構師思考: 「Redis Lock 負責擋掉 99% 前端重複衝擊(流量削峰);但面對網路暫斷、Lock 到期釋放(Timeout)、程序崩潰(Crash)等極端狀況,Database Constraint(如 UNIQUE Key、Check Constraint)永遠是保證資料不髒掉的最後一道防線。」

19. Unique Index 是防重最後一道防線

  • 坑點: 在應用層寫 if (!Order::where('order_no', $no)->exists()) 擋不住高併發下的重複插入。

  • ❌ 感謝信回答: 「寫 code 前先 exists() 檢查一下,就不會重複建立了。」

  • ⭕ 架構師思考: 「在高併發下,exists() 檢查與 insert() 之間存在微秒級的時間差。唯有資料庫層級的 UNIQUE KEY 才能提供硬性保證,應用層只需優化 Duplicate Key Exception 的捕捉與提示。」

20. Soft Delete + Unique Constraint 的坑

  • 坑點: Laravel SoftDeletes 將資料標記 deleted_at != NULL。若 email 設為 DB 唯一索引,當使用者刪除帳號後重新註冊相同 email,DB 會因為被軟刪除的舊資料仍存在而報錯 Duplicate entry。

  • ❌ 感謝信回答: 「遇到這個問題,就把 Unique 索引拔掉,改在 PHP 用程式碼判斷。」

  • ⭕ 架構師思考: 設計層面的權衡:

    1. 將組合索引擴展為 UNIQUE(email, deleted_at)(需注意 MySQL 中 NULL 不相等的特性,或使用 Default Timestamp 替代 NULL)。

    2. 或是將軟刪除資料在刪除時進行 Hash 脫敏(如 deleted_email_timestamp),或直接歸檔至物理歷史表。

六、 大資料量與架構優化

21. N+1 問題不只是 Eager Loading

  • 坑點: 用 Order::with('user')->get() 能解決 N+1。但當數據量達到數百萬級時,with() 會產生巨大的 WHERE id IN (...) 查詢。

  • ❌ 感謝信回答: 「只要記得寫 with(),Laravel 就完全沒有 N+1 與效能問題了。」

  • ⭕ 架構師思考: 考慮記憶體與 SQL 解析極限:「過大的 IN (...) 會導致 PHP 記憶體溢出(Memory Limit Exceeded)或 MySQL 解析過大 SQL 語法。大數據量下應採用分批處理(chunk / lazy),或在架構設計時適度做欄位冗餘(Redundancy)。」

22. 深分頁(Offset Pagination)的效能黑洞

  • 坑點: SELECT * FROM orders ORDER BY id LIMIT 20 OFFSET 1000000; MySQL 必須先掃描並拋棄前面的 1,000,000 筆資料,IO 成本極高。

  • ❌ 感謝信回答: 「用 Laravel 內建的 paginate(20) 就能搞定分頁。」

  • ⭕ 架構師思考: 改用 Cursor / Keyset Pagination(Laravel 的 cursorPaginate()):

    SQL
    SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 20;
    
    利用主鍵或索引的範圍直接跳過前 100 萬筆資料,將 $O(N)$ 復雜度降為 $O(1)$。

七、 資深工程師的核心面試觀念

23. 避免給出「絕對答案」

  • ❌ 感謝信回答: 背誦各式各樣的「絕對鐵律」(如:JOIN 一定比 Subquery 快、索引一定比全表掃描快)。

  • ⭕ 架構師思考: 成熟的回答模式永遠是:「通常情況下……,但這取決於資料規模、Cardinality、Isolation Level、Query Pattern 以及 MySQL Optimizer 最終產生的 Execution Plan。」

24. 構建完整的交易架構思考鏈

  • ❌ 感謝信回答: 被問到問題時,只能像吐字機一樣拋出單點名詞(Redis、Lock、Queue)。

  • ⭕ 架構師思考: 將知識點串聯成一條邏輯嚴密的「思考鏈」:

Plaintext
[ 1. 業務場景與競爭點 ]
        │
        ▼
[ 2. 併發控制 (Atomic Update / lockForUpdate) ]
        │
        ▼
[ 3. 交易邊界與死鎖防護 (Transaction Boundary & Lock Order) ]
        │
        ▼
[ 4. 一致性與隔離層級 (MVCC & Consistent Read) ]
        │
        ▼
[ 5. 底層防禦 (DB Constraints & Idempotency) ]
        │
        ▼
[ 6. 流量削峰與補償機制 (Queue & Circuit Breaker) ]

💡 總結

在 Senior 面試中,24 個知識點是磚頭,而你的「架構思維與權衡能力」才是把磚頭蓋成大樓的水泥。

清楚知道這些底層陷阱「在什麼業務場景下會爆開」,以及「當系統在半夜爆開時你如何優雅降級」,這才是資深工程師真正無可替代的價值。

沒有留言:

張貼留言

【2026 最新】VS Code 導入 pstack AI Skills 完整教學:用 Copilot 打造嚴謹的 Agent 工作流

在 VS Code 使用 AI Skills 打造嚴謹工程工作流:從 pstack 到原生 AGENTS.md 實戰指南 「If you want to go fast, go deep first.」 — Lauren Tan(@poteto) 進入 2026 年, AI Co...