Java 後端底層 ch01 索引的真相:為什麼查詢會慢,加了索引又為什麼沒用
下一章→
CH 01 儲存引擎

索引的真相:為什麼查詢會慢,加了索引又為什麼沒用

磁碟 I/O 才是成本為什麼是 B+Tree 不是二元樹聚簇索引與回表覆蓋索引與最左前綴索引失效圖鑑加索引的代價

你大概遇過這個場景:某支 API 慢到爆,DBA 說「加個索引就好了」,你加了,果然快了。然後過幾個月,另一支 API 也慢,你如法炮製加了索引——這次完全沒有變快。

或者更糟:你在一張兩千萬列的表上加了索引,查詢是快了,但寫入從此變慢,而且慢得莫名其妙。

索引不是「加了就會快」的魔法開關。它是一個有明確結構、明確代價、也有明確失效條件的資料結構。這一章要把它拆開來看:

  • 為什麼資料庫的效能問題幾乎都是 I/O 問題——先講清楚成本在哪,後面的設計才有意義。
  • 為什麼所有關聯式資料庫都選了 B+Tree,而不是課本上教的二元搜尋樹或 Hash 表。
  • InnoDB 的聚簇索引:一個會顛覆直覺的事實——在 InnoDB 裡,表本身就是索引。
  • 回表、覆蓋索引、最左前綴:為什麼 SELECT * 比 SELECT id 慢,為什麼複合索引 (a, b, c) 單獨查 b 完全用不到。
  • 索引失效圖鑑:你明明建了索引,EXPLAIN 卻顯示全表掃描的那幾種寫法。
  • 代價:什麼時候不應該加索引。

讀完之後,你看到一句慢 SQL 應該要能推理出「它為什麼慢」,而不是憑經驗猜「再加個索引試試」。

這一章假設你知道 SQL 怎麼寫、知道資料庫大概是什麼。如果對「資料庫是什麼、交易是什麼」還不熟,先看 IT 基礎知識 ch07 資料庫基礎。交易與 MVCC 的部分留到下一章,這章專心處理「資料怎麼被找到」。

在談索引之前,要先知道一件事:資料庫慢,慢的幾乎不是「計算」,而是「去磁碟拿東西」。

數量級的差距

  • 記憶體隨機存取:約 100 奈秒
  • SSD 隨機讀取:約 100 微秒 —— 是記憶體的 1,000 倍
  • 傳統硬碟隨機讀取:約 10 毫秒 —— 是記憶體的 100,000 倍

CPU 比較一百萬次的成本,可能還不如去磁碟拿一次資料。所以優化資料庫查詢,本質上就是想辦法少去磁碟幾次。

關鍵:讀取的單位是「頁」,不是「列」

InnoDB 不會只讀你要的那一列。它讀寫的最小單位是頁(page),預設 16KB。你只要一個欄位,它也是整頁搬進記憶體。

所以衡量一個查詢貴不貴,正確的單位是「讀了幾頁」=「做了幾次 I/O」,不是「掃了幾列」。

由此推導出索引存在的唯一理由

索引不是為了「比較得比較快」,是為了讓資料庫不必把整張表的頁都搬進記憶體。一張 10GB 的表全表掃描要讀 65 萬頁;如果索引能讓它只讀 3 頁就找到目標,那就是二十萬倍的差距。

後面所有的設計——B+Tree 為什麼長那樣、為什麼要有聚簇索引、為什麼回表很貴——全部都是在回答同一個問題:怎麼少讀幾頁。

資料結構課本會告訴你,有序查找用二元搜尋樹,複雜度 O(log₂ n),很漂亮。但沒有任何一個關聯式資料庫用它。為什麼?

先算一筆帳

假設一張表有 1.3 億列,用二元搜尋樹:

log₂(130,000,000) ≈ 27

27 次比較——聽起來很少。但問題是:樹有 27 層,就代表最壞情況要往下走 27 個節點,而每個節點可能在磁碟的不同位置。

27 次比較不貴,但 27 次隨機磁碟 I/O 非常貴。用 SSD 算就是 2.7 毫秒,用傳統硬碟是 270 毫秒——一次查詢。

解法:把樹壓矮

既然每往下一層就是一次 I/O,那就想辦法減少層數。方法是讓每個節點裝更多的 key,分支數變多,樹自然變矮。

一個節點正好就是一頁 16KB。假設 key 是 BIGINT(8 bytes)+子節點指標(6 bytes)= 14 bytes:

16384 bytes ÷ 14 bytes ≈ 1170 個分支

算算三層能裝多少

  • 第 1 層(根):1,170 個分支
  • 第 2 層:1,170 × 1,170 = 約 137 萬個分支
  • 第 3 層(葉節點,放實際資料,假設一列 1KB,一頁放 16 列):137 萬 × 16 = 約 2,190 萬列
✅ 三層 B+Tree 就能裝下兩千萬列,代表任何一列都在 3 次 I/O 之內找到。而且根節點幾乎永遠在記憶體裡(Buffer Pool),實際磁碟 I/O 通常只有 2 次。

記住這一句

樹高就是 I/O 次數。整章後面所有的內容,都是這句話的推論。

B+Tree 之所以贏過 B-Tree,靠的是兩個看起來很小、影響卻很大的設計。

設計一:非葉節點只放 key,不放資料

B-Tree 的每一層都可以放資料。B+Tree 不行——資料只存在最底層的葉節點,上面全部只是路標。

好處很直接:一頁 16KB 如果要放資料,一列 1KB 就只能放 16 個;如果只放 key,可以放 1170 個。分支數差了 70 倍,樹高就從 5 層降到 3 層。

非葉節點不存資料 → 每頁能放更多 key → 分支更多 → 樹更矮 → I/O 更少。這是同一條因果鏈。

設計二:葉節點用雙向鏈結串起來

所有葉節點之間有指標互相連著,形成一條由小到大的有序鏈。這解決了範圍查詢:

SELECT * FROM orders WHERE id BETWEEN 1000 AND 2000;
  • B+Tree:從根往下找到 id = 1000 的葉節點,然後順著鏈結一路往右讀,讀到 2000 為止。
  • B-Tree:資料散在各層,必須中序遍歷,讀完一個節點要回頭找父節點再往下——來回跳,磁碟位置也跳。

一個常被忽略的推論

因為葉節點本來就是排好序的鏈結,所以:

✅ ORDER BY 索引欄位 幾乎是免費的——資料庫不用排序,順著葉節點走就已經是有序的。反之,ORDER BY 非索引欄位 會觸發 filesort,那才是真的貴。

這是最違反直覺、也最重要的一件事。很多人以為表資料存在一個地方,索引是另外一份「目錄」指過去。在 InnoDB 裡不是這樣。

主鍵索引的葉節點,直接存整列資料

不是指標,不是位址——就是那一列本身。所以主鍵索引這棵樹,同時就是這張表的全部資料。這棵樹叫聚簇索引(Clustered Index)。

推論:一張表只能有一個聚簇索引——因為資料只有一份,不可能同時按兩種順序物理排列。

如果沒有指定主鍵會怎樣

InnoDB 一定要有聚簇索引,所以它會依序找:

  • 有主鍵 → 用主鍵
  • 沒主鍵 → 找第一個 NOT NULL 的唯一索引
  • 都沒有 → 自己偷偷生一個 6 bytes 的隱藏欄位 row_id,你看不到也用不到

真正的代價:主鍵決定了整張表的物理排列

因為資料是按主鍵順序存放的,主鍵選什麼,就決定了新資料要插在哪裡。

失敗場景:拿 UUID 當主鍵。
UUID 是隨機的,所以每次 INSERT 的位置都是隨機的——可能要插進某個已經滿了的頁中間。這時 InnoDB 必須做頁分裂(page split):把那一頁切成兩半、搬移資料、更新父節點指標。

徵狀:寫入 TPS 隨資料量增長而持續下滑;表的實際佔用空間遠大於資料量(頁只裝了一半);OPTIMIZE TABLE 之後空間掉一大截。

所以自增主鍵為什麼被推薦

AUTO_INCREMENT 保證新資料永遠是最大的,只會往最後一頁追加,不會插進中間、不會頁分裂。這不是什麼慣例,是由聚簇索引的結構直接推導出來的結果。

如果業務上非要用 UUID(例如分散式系統要避免 ID 衝突),常見做法是:主鍵仍用自增,UUID 當作一個有唯一索引的普通欄位。

除了主鍵之外,你自己建的索引都叫二級索引(Secondary Index),也叫輔助索引。它們也是 B+Tree,但葉節點放的東西不一樣。

二級索引的葉節點放的是「主鍵值」

不是整列資料,也不是磁碟位址,是主鍵。所以一次二級索引查詢,其實走了兩棵樹:

SELECT * FROM users WHERE email = 'andy@example.com';

① 在 email 索引樹裡找 'andy@example.com'  →  拿到 id = 42
② 拿著 id = 42 回到主鍵樹(聚簇索引)再查一次  →  拿到整列資料
   ↑ 這一步就叫「回表」(back to table)

為什麼要設計成這樣

如果二級索引直接存資料,那有 5 個索引就有 5 份資料副本,更新一列要改 5 個地方。存主鍵值只佔幾 bytes,而且資料搬動時(例如頁分裂)不用更新每個二級索引——因為主鍵值不會變。這是空間與維護成本的取捨。

回表有多貴

回表是隨機 I/O——找到的每一個主鍵值,都要各自到主鍵樹走一趟。如果一個查詢命中 10,000 列,那就是 10,000 次回表。

這就是優化器有時候「明明有索引卻不用」的原因:它估算過,與其回表一萬次,不如老老實實全表掃描(順序 I/O 反而快)。這不是 bug,是正確的決策。

怎麼看出有沒有回表

EXPLAIN SELECT * FROM users WHERE email = ?;
  • Extra: Using index → 沒有回表(用了覆蓋索引,見下一張卡)
  • 沒有這一行,只有 Using where → 回表了

覆蓋索引:讓查詢不必回表

如果你 SELECT 的所有欄位,索引裡通通都有,那就不用回表了——這叫覆蓋索引(Covering Index)。

-- email 索引的葉節點裡有:email(key)+ id(主鍵值)

SELECT id FROM users WHERE email = ?;      -- ✅ 覆蓋,不回表
SELECT id, email FROM users WHERE email = ?; -- ✅ 覆蓋,不回表
SELECT * FROM users WHERE email = ?;       -- ❌ 要 name、phone…,得回表
💡 這是「不要隨手寫 SELECT *」最實際的理由——不是為了少傳幾個欄位的網路流量,是為了避開回表。

複合索引的排序規則

建立 INDEX idx (a, b, c) 時,B+Tree 是這樣排的:先按 a 排序;a 相同的,再按 b 排;a 和 b 都相同的,才按 c 排。

最左前綴原則

因為是這種排法,索引只能從最左邊開始連續使用:

  • ✅ WHERE a = 1
  • ✅ WHERE a = 1 AND b = 2
  • ✅ WHERE a = 1 AND b = 2 AND c = 3
  • ❌ WHERE b = 2 —— 用不到
  • ❌ WHERE c = 3 —— 用不到
  • ⚠️ WHERE a = 1 AND c = 3 —— 只有 a 用得到,c 用不到(中間斷了)
用電話簿理解:電話簿按「姓,名」排序。你可以查「所有姓王的」(✅ 最左),也可以查「王小明」(✅ 連續)。但你沒辦法查「所有名字叫小明的」——因為叫小明的散落在整本書裡(❌ 跳過最左)。

範圍查詢會截斷後面的欄位

-- 索引 (a, b, c)
WHERE a = 1 AND b > 5 AND c = 3

a 和 b 用得到,但 c 用不到。因為 b 是範圍,符合 b > 5 的那一堆列裡面,c 的順序是亂的(c 只在 b 相同時才有序)。

💡 推論:建複合索引時,等值查詢的欄位要放前面,範圍查詢的欄位放最後。

這是實務上最常遇到的狀況。索引在那裡,但資料庫就是不用。以下是五種最常見的原因,全部都有同一個本質:索引是對「欄位原值」排序的,只要你動了欄位,順序就失效了。

1. 用函數包住了索引欄位

-- ❌ 索引失效
WHERE DATE(created_at) = '2026-08-03'

-- ✅ 改寫成範圍
WHERE created_at >= '2026-08-03'
  AND created_at <  '2026-08-04'

索引是按 created_at 原值排的,不是按 DATE(created_at) 排的。資料庫沒辦法在有序結構裡找一個「被加工過的值」,只能每一列都算一次。

2. 隱式型別轉換(最陰險的一個)

-- phone 欄位是 VARCHAR
-- ❌ 傳了數字
WHERE phone = 13800138000

-- ✅ 傳字串
WHERE phone = '13800138000'

型別不一致時 MySQL 會把欄位轉成數字再比較,等於對每一列都套了一個函數——退化成第 1 種情況。SQL 完全合法、結果也正確,只是慢了幾百倍。

3. 前綴模糊查詢

WHERE name LIKE 'abc%'   -- ✅ 用得到(知道從哪開始找)
WHERE name LIKE '%abc'   -- ❌ 用不到
WHERE name LIKE '%abc%'  -- ❌ 用不到

索引是按開頭排序的。不知道開頭是什麼,就沒有起點可以定位。真的需要全文檢索,該用的是 Elasticsearch 或全文索引,不是 B+Tree。

4. OR 連接了沒有索引的欄位

-- email 有索引,nickname 沒有
WHERE email = ? OR nickname = ?   -- ❌ 整條失效

因為 nickname 那一半無論如何都要全表掃描,掃都掃了,優化器乾脆整條走全表。

5. 選擇性太低(這個不是錯,是正確決策)

WHERE gender = 'M'   -- 全表 50% 都符合

命中一半的列,代表要回表幾百萬次。優化器算過之後認為全表掃描(順序 I/O)比回表幾百萬次(隨機 I/O)更快,於是放棄索引。這是對的。

Java / Spring Data JPA 的陷阱
衍生查詢方法(findByCreatedAtBetween、findByPhone…)產生的 SQL 你看不到,型別是由 Entity 欄位決定的。務必打開:
spring.jpa.show-sql=true
把實際 SQL 抓出來丟進 EXPLAIN 看,不要假設 JPA 幫你產生的 SQL 一定會用到索引。

前面都在講索引怎麼讓查詢變快。這一張卡講反面——因為「再加個索引試試」是實務上最常見的誤判。

每個索引都是一棵獨立的 B+Tree

它要佔空間、要維護、要跟著資料變動而更新。一張大表上的一個索引,實際大小可能是幾百 MB 到幾 GB。

代價一:寫入放大

INSERT INTO users VALUES (...);

實際發生的事:
  ① 主鍵樹(聚簇索引)寫入一次
  ② email 索引寫入一次
  ③ phone 索引寫入一次
  ④ created_at 索引寫入一次
  → 一次 INSERT = 四次 B+Tree 寫入

UPDATE 更糟:改到哪個欄位,那個欄位的索引就要刪掉舊值、插入新值(因為位置變了)。

代價二:頁分裂

只要插入位置不是遞增的,就有機會撐爆某一頁而觸發分裂——搬資料、改指標,而且分裂後兩頁都只有一半滿,空間利用率直接掉到 50%。

什麼時候不該加

  • 選擇性低的欄位:性別、狀態旗標、是否刪除——重複值太多,加了也不會被用。
  • 寫多讀少的表:log 表、事件表,每秒幾千筆寫入,索引是純負擔。
  • 已經被前綴涵蓋:已經有 (a, b, c) 就不需要再建 (a) 和 (a, b)——最左前綴已經涵蓋了。這是很常見的重複索引。
  • 整張表很小:幾百列的設定表,全表掃描可能只要 1 次 I/O,比走索引還快。
🚨 正確的流程是:先用 EXPLAIN 看它現在走什麼路 → 判斷慢在哪 → 再決定要不要加索引、加在哪個欄位。不是先加了再看有沒有變快。

還有一個常被忘記的選項

有時候正確答案不是加索引,而是改寫查詢(避開失效條件)、改資料模型(拆表、加冗餘欄位),或接受它就是慢(後台報表跑 3 秒沒關係)。索引是工具之一,不是唯一解。

資料結構選型:為什麼最後是 B+Tree
結構找單筆範圍查詢為什麼資料庫不選它
Hash 表O(1),最快完全不支援只能等值比對,>、BETWEEN、ORDER BY 全部失效
二元搜尋樹 BST1.3 億筆約 27 層要中序遍歷樹太高 = 磁碟 I/O 次數太多;還可能退化成鏈結串列
B-Tree3~4 層要中序遍歷、來回跳父節點非葉節點也放資料 → 每頁能放的 key 變少 → 樹變高
B+Tree(InnoDB 採用)3~4 層葉節點串成鏈結,順著走即可✅ 就是它:樹最矮、範圍查詢最快、ORDER BY 幾乎免費
聚簇索引 vs 二級索引
面向聚簇索引(主鍵)二級索引(自建)
葉節點存什麼整列資料本身主鍵值(不是資料)
一張表有幾個只能有一個(資料只有一份)可以有很多個
查詢要走幾棵樹一棵,找到就是資料兩棵(回表),除非命中覆蓋索引
誰決定資料的物理排列它 —— 資料按主鍵順序存放不影響資料排列
選錯的代價UUID 主鍵 → 頁分裂、空間浪費、寫入持續變慢索引開太多 → 寫入放大,一次 INSERT 寫 N 棵樹
索引失效圖鑑:寫法 → 原因 → 改法
失效的寫法為什麼失效改成
WHERE DATE(created_at) = '2026-08-03'索引按原值排序,函數加工後順序就不成立改成範圍:>= '2026-08-03' AND < '2026-08-04'
WHERE phone = 13800138000(欄位是 VARCHAR)隱式轉型 → 等於對每一列套函數,SQL 合法但慢幾百倍傳字串:WHERE phone = '13800138000'
WHERE name LIKE '%abc'不知道開頭,索引沒有起點可以定位改前綴 LIKE 'abc%';真要全文檢索用 ES 或全文索引
WHERE email = ? OR nickname = ?(nickname 無索引)OR 的另一半一定要全表掃,掃都掃了乾脆整條走全表拆成兩段 UNION,或替 nickname 也建索引
WHERE gender = 'M'選擇性太低,回表成本高於全表掃描——優化器是對的不用改;這種欄位本來就不該單獨建索引
索引 (a,b,c) 但查 WHERE b = ?違反最左前綴,b 在整棵樹裡是散落的調整索引順序,或另建以 b 開頭的索引

練習題 點選選項查看解析

0 / 10
01 / 10
為什麼資料庫不用二元搜尋樹當索引結構?
A 因為二元搜尋樹的時間複雜度太高
B 因為樹太高,每往下一層就可能是一次隨機磁碟 I/O
C 因為二元搜尋樹不支援排序
D 因為二元搜尋樹佔用的記憶體太大
解析
O(log₂ n) 的比較次數其實很少(1.3 億筆約 27 次)。問題在於 27 層代表最壞要做 27 次隨機磁碟 I/O,而磁碟比記憶體慢一千到十萬倍。B+Tree 靠增加分支數把樹壓到 3~4 層,本質上是在減少 I/O 次數,不是減少比較次數。
02 / 10
InnoDB 的主鍵索引(聚簇索引),葉節點裡存的是什麼?
A 指向資料列的磁碟位址
B 整列資料本身
C 該列的主鍵值
D 資料列的 hash 值
解析
聚簇索引的葉節點直接存整列資料,所以「主鍵索引」和「表資料」是同一棵樹、同一份東西。這也是為什麼一張表只能有一個聚簇索引——資料只有一份,不可能同時按兩種順序物理排列。
03 / 10
二級索引的葉節點存的是什麼?這造成什麼後果?
A 存整列資料,所以二級索引查詢最快
B 存主鍵值,所以查非索引欄位時需要「回表」再查一次主鍵樹
C 存磁碟位址,所以資料搬動時所有二級索引都要更新
D 存指向聚簇索引葉節點的指標,所以不用回表
解析
二級索引葉節點存主鍵值。查詢時先在二級索引找到主鍵,再拿主鍵回到聚簇索引查一次拿完整資料,這一步就是「回表」。存主鍵而不是磁碟位址的好處是:資料因頁分裂而搬動時,主鍵值不變,二級索引不用跟著改。
04 / 10
已知索引 (a, b, c),下列哪一個查詢「完全用不到」這個索引?
A WHERE a = 1 AND b = 2
B WHERE a = 1 AND c = 3
C WHERE b = 2 AND c = 3
D WHERE a = 1
解析
最左前綴原則:索引先按 a 排序,a 相同才按 b 排。跳過 a 直接查 b,b 的值在整棵樹裡是散落的,無法定位。選項 B 的 a 用得到(c 用不到,因為中間斷了),選項 A 和 D 都符合最左連續。
05 / 10
phone 欄位是 VARCHAR 且有索引,執行 WHERE phone = 13800138000(不加引號)會發生什麼?
A SQL 語法錯誤,直接報錯
B 查詢結果錯誤,會查不到資料
C 結果正確,但索引失效退化成全表掃描
D MySQL 會自動把數字轉成字串,索引照常使用
解析
型別不一致時 MySQL 會把「欄位」轉成數字再比較,等於對每一列都套了一層函數,索引因此失效。可怕的地方在於 SQL 完全合法、結果也完全正確,只是慢了幾百倍——不看 EXPLAIN 根本發現不了。
06 / 10
為什麼用 UUID 當主鍵在 InnoDB 裡是個壞主意?
A UUID 太長,索引放不下
B UUID 是隨機的,插入位置隨機,會頻繁觸發頁分裂
C UUID 不能當主鍵,InnoDB 會報錯
D UUID 沒辦法建立索引
解析
資料按主鍵順序物理存放。UUID 隨機代表新資料可能要插進某個已滿的頁中間,觸發頁分裂(切頁、搬資料、改指標),分裂後兩頁都只有一半滿。徵狀是寫入 TPS 隨資料量下滑、表佔用空間遠大於實際資料量。自增主鍵永遠往尾端追加,沒有這個問題。
07 / 10
EXPLAIN 的 Extra 欄位出現「Using index」代表什麼?
A 有用到索引,但需要回表
B 命中覆蓋索引,不需要回表
C 索引失效,走了全表掃描
D 使用了索引下推優化
解析
「Using index」= 覆蓋索引,SELECT 要的欄位在索引裡全都有,不用回表。這是「不要隨手 SELECT *」最實際的理由——不是為了省網路流量,是為了避開回表這個隨機 I/O。
08 / 10
查詢 WHERE gender = 'M',gender 上有索引但優化器不用它,為什麼?
A 這是 MySQL 優化器的 bug,應該用 FORCE INDEX 強制使用
B 索引壞了,需要 ANALYZE TABLE 重建統計資訊
C 選擇性太低,回表幾百萬次比全表順序掃描更慢,不用索引是正確決策
D 字串型別的欄位無法使用索引
解析
命中全表一半的資料,代表要回表幾百萬次(隨機 I/O)。優化器估算後認為全表掃描(順序 I/O)反而更快,於是放棄索引——這是正確判斷。推論:選擇性低的欄位(性別、狀態旗標)本來就不該單獨建索引。
09 / 10
已經有索引 (a, b, c),再建一個索引 (a, b) 會怎樣?
A 查詢會變快,因為索引更精準
B 是重複索引,(a,b,c) 的最左前綴已經涵蓋,只增加寫入成本
C 會報錯,MySQL 不允許重複的索引前綴
D 沒有影響,MySQL 會自動忽略
解析
最左前綴原則代表 (a,b,c) 已經能服務所有以 a 開頭的連續查詢,包含 (a) 和 (a,b)。多建的索引不會讓查詢變快,但每次 INSERT/UPDATE 都要多維護一棵 B+Tree。這是實務上很常見的冗餘索引。
10 / 10
索引 (a, b, c) 上執行 WHERE a = 1 AND b > 5 AND c = 3,哪些欄位用得到索引?
A a、b、c 全部用得到
B 只有 a 用得到
C a 和 b 用得到,c 用不到
D 都用不到,因為有範圍查詢
解析
範圍查詢會截斷它後面的欄位。c 只有在 b 相同時才有序;一旦 b 是範圍(b > 5),符合條件的那一堆列裡面 c 的順序是亂的,無法用索引定位。推論:建複合索引時,等值查詢的欄位放前面,範圍查詢的欄位放最後。

點擊卡片翻面查看答案,共 13 張。

QUESTION
索引存在的唯一理由是什麼?
點擊翻面
ANSWER
減少要從磁碟讀取的「頁」數。資料庫慢的不是計算而是 I/O,磁碟比記憶體慢 1,000~100,000 倍,而讀寫的最小單位是 16KB 的頁,不是單一列。
點擊翻回
QUESTION
「樹高就是 ___」
點擊翻面
ANSWER
I/O 次數。每往下一層就可能是一次隨機磁碟存取,所以 B+Tree 的全部設計都在壓低樹高。三層就能裝約 2,190 萬列。
點擊翻回
QUESTION
B+Tree 的兩個關鍵設計是什麼?
點擊翻面
ANSWER
① 非葉節點只放 key 不放資料 → 每頁能放 1170 個 key 而不是 16 個 → 樹更矮。② 葉節點用雙向鏈結串起來 → 範圍查詢順著走就好,ORDER BY 索引欄位幾乎免費。
點擊翻回
QUESTION
在 InnoDB 裡,「表資料」和「主鍵索引」是什麼關係?
點擊翻面
ANSWER
是同一棵樹、同一份東西。聚簇索引的葉節點直接存整列資料,不是指標。所以一張表只能有一個聚簇索引。
點擊翻回
QUESTION
沒有指定主鍵時 InnoDB 會怎麼做?
點擊翻面
ANSWER
依序尋找:① 主鍵 → ② 第一個 NOT NULL 的唯一索引 → ③ 都沒有就自己生一個 6 bytes 的隱藏欄位 row_id(你看不到也用不到)。
點擊翻回
QUESTION
什麼是「回表」?為什麼貴?
點擊翻面
ANSWER
二級索引葉節點只存主鍵值,找到後要拿主鍵回聚簇索引再查一次拿完整資料。貴在它是隨機 I/O——命中一萬列就回表一萬次。
點擊翻回
QUESTION
什麼是覆蓋索引?EXPLAIN 怎麼看?
點擊翻面
ANSWER
SELECT 要的欄位在索引裡全都有,不用回表。EXPLAIN 的 Extra 顯示「Using index」就是命中了。這是不要隨手寫 SELECT * 的真正理由。
點擊翻回
QUESTION
最左前綴原則怎麼用電話簿理解?
點擊翻面
ANSWER
電話簿按「姓,名」排。可以查「所有姓王的」(最左)、「王小明」(連續),但沒辦法查「所有叫小明的」——他們散落在整本書裡。
點擊翻回
QUESTION
索引 (a,b,c) 遇到 WHERE a=1 AND b>5 AND c=3,c 為什麼用不到?
點擊翻面
ANSWER
範圍查詢會截斷後面的欄位。c 只在 b 相同時才有序,b 一旦是範圍,那堆列裡 c 的順序就是亂的。推論:等值欄位放前面,範圍欄位放最後。
點擊翻回
QUESTION
索引失效的五種常見寫法?
點擊翻面
ANSWER
① 函數包住欄位 DATE(created_at) ② 隱式型別轉換(VARCHAR 欄位傳數字)③ LIKE '%abc' 前綴模糊 ④ OR 連接無索引欄位 ⑤ 選擇性太低(gender)——第 ⑤ 種不是錯,是優化器的正確決策。
點擊翻回
QUESTION
為什麼 UUID 當主鍵會讓寫入越來越慢?
點擊翻面
ANSWER
資料按主鍵物理排列,UUID 隨機 → 插入位置隨機 → 撐爆已滿的頁 → 頁分裂(切頁、搬資料、改指標),分裂後兩頁都只有半滿。徵狀:寫入 TPS 隨資料量下滑、表空間遠大於資料量。
點擊翻回
QUESTION
一次 INSERT 實際上寫了幾棵 B+Tree?
點擊翻面
ANSWER
1 + 二級索引的數量。主鍵樹一次,每個二級索引各一次。這就是「寫入放大」,也是不能隨便加索引的主因。
點擊翻回
QUESTION
什麼時候不該加索引?
點擊翻面
ANSWER
① 選擇性低(性別、狀態旗標)② 寫多讀少的表(log、事件表)③ 已被現有索引的最左前綴涵蓋(有 (a,b,c) 就不用建 (a)、(a,b))④ 整張表很小。正確流程是先 EXPLAIN 再決定,不是先加了看有沒有變快。
點擊翻回