線上專案回報:`SELECT * FROM rosterusers ORDER BY username LIMIT 10000, 3` 極慢,`EXPLAIN` 顯示 `Using filesort`。但 `username` 明明有索引 (聯合主鍵 `i_rosteru_user_jid`)。深挖發現:**前綴索引 `username(75)` 導致排序無法使用索引**,被迫外部排序。本文完整還原排查過程、原理剖析與 Schema 設計教訓。
-- 不加 ORDER BY:極快
EXPLAIN SELECT * FROM rosterusers LIMIT 10000, 3;
-- type=ALL, rows=2010066, Extra= (無 filesort)
-- 加 ORDER BY username:極慢
EXPLAIN SELECT * FROM rosterusers ORDER BY username LIMIT 10000, 3;
-- type=ALL, rows=2010087, Extra=Using filesort ← 關鍵差異
CREATE TABLE `rosterusers` (
`username` VARCHAR(250) NOT NULL,
`jid` VARCHAR(250) NOT NULL,
UNIQUE KEY `i_rosteru_user_jid` (`username`(75), `jid`(75)), -- 前綴索引!
KEY `i_rosteru_jid` (`jid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
InnoDB **聚簇索引** (主鍵/隱式 Row ID) 決定資料物理順序。`LIMIT offset, n` 無 ORDER BY 時,優化器直接按聚簇索引順序掃描跳過 offset 行,**雖然掃描行數大,但順序 I/O、無排序開銷**,實測比 filesort 快得多。
SELECT * FROM rosterusers FORCE INDEX (i_rosteru_user_jid) ORDER BY username LIMIT 10000, 3;
-- 依然 Using filesort,優化器判斷索引不適用於完整列排序
1. 合理規劃欄位長度:`VARCHAR(250)` → 實際業務長度 (如 50/100),避免「以防萬一」過度設計。
2. 前綴索引僅作最後手段:當列長度確實超過引擎限制 (InnoDB 767/3072 bytes) 且**確定不需排序/分組/完整比較**時才用。
3. 任何遷移/整合第三方表結構時,必須審視索引定義**:前綴索引、缺失外鍵、字元集不一致是隱形效能殺手。
4. 定期 `pt-index-usage` / `pt-duplicate-key-checker` 掃描索引健康度**。
前綴索引**不是萬能藥**。它解決「長列建索引」但引入「排序/分組失效、唯一性語義弱化」副作用。**合理的欄位長度設計永遠優於前綴索引補救**。此案例中,`username(75)` 純屬多餘,導致核心查詢退化為 filesort,典型的「自己挖坑自己跳」。