一天,阿冠帶著夥伴腦袋與毛吉,到 LaLaport 尋找夢寐以求的「太陽拉麵」

到了商場門口,三人抬頭一看

哇哩!這地方足足有六層樓,還超級大!


腦袋皺著眉說:「我們從一樓開始,一層一層慢慢找吧!」

c

毛吉在旁邊激動地汪汪叫:「阿烏旺旺旺旺!」看起來沒有甚麼幫助


阿冠搖搖頭,露出無奈的表情,仿佛已經習慣這一切


「Follow me」


三人走到一樓的樓層導覽地圖前,阿冠熟練地滑動著手指,找到了那行關鍵字:「🍜 太陽拉麵 → 3樓中區 C312」


「走吧!直奔三樓中區!」沒花幾分鐘,他們就聞到濃濃的豚骨香

sun

腦袋驚呼:太神了吧!我們沒繞半圈就找到了!」


阿冠微微一笑,扶著眼鏡:「這是 Index 的力量!」

在多數資料庫中(例如 SQL Server、MySQL、PostgreSQL),預設使用的索引結構就是 B-Tree(Balanced Tree)

假設你建立一個索引

1
CREATE INDEX IX_Customers_Email ON Customers(Email);

這時資料庫會做幾件事

  • 把 Customers 資料表裡所有 Email 欄位的值取出來
  • 幫這些值「排序」
  • 建立一棵 B-Tree(平衡樹) 結構來儲存它們
  • 每個節點會記錄:
    • 一個 Email 值
    • 對應這筆資料在資料表裡的位置(Row Pointer)

你可以想像一棵「有秩序的資料樹」

1
2
3
            [brian@yahoo.com]
/ \
[andy@gmail.com] [charlie@outlook.com]

這棵樹有個簡單但超關鍵的規則

  • 左邊的值比節點小
  • 右邊的值比節點大

當你要查某個 Email,比如 ‘charlie@outlook.com‘,資料庫就會這樣走:

  • 從樹的「根節點」開始比對
  • 發現 charlie > brian → 往右邊走
  • 到右邊節點一比對 → 找到了 ✅ 耶比

索引的排序方式,完全取決於你在建立索引時指定的欄位,每次比較都能「排除掉一半的可能範圍」,也就是 log(n),所以查詢起來很快

f

c

更新與插入成本會提高,可能導致效能反而下降,因為索引會隨著資料的變動而更新。如果某欄位更新非常頻繁,會導致

  • 索引更新成本高(尤其是在高併發下)
  • 索引碎片增加,查詢效能下降

所以不建議在以下欄位加索引

  • 使用率低且經常變動的欄位(如「登入次數」)
  • 隨時更新的狀態欄位(如「IsOnline」)

所以建議是儘量只在查詢頻繁且變動不大的欄位上加索引

s

類型 本質是什麼 資料放哪裡
Clustered Index 資料本身就照索引排序儲存 資料本體就是索引
Non-Clustered Index 索引是額外的導覽表 索引指向資料的位置
  • Clustered Index 資料本身就按照索引的順序儲存,沒有「再指過去」的過程
  • Non-Clustered Index 索引表是獨立的,它只記錄「欄位值」 + 「資料所在位置(Row Pointer)」,查完索引後,還要「再跳一次」才能拿到資料

c




今天我們要找太陽拉麵在哪…




方法一:Clustered Index 模式

「商場的店就是照樓層與店號排列,所以我們只要看店號 301,就直接走到那裡就好。」不用再查地圖,因為商場本身就是按照順序排好的

aaa

方法二:Non-Clustered Index 模式

「但如果你只記得店名太陽拉麵,那就得先去一樓查地圖(索引表),看太陽拉麵在哪個店號 → 然後再走過去。」多了一個中間步驟,但仍比亂找快得多

aaa

Index Scan 就是,將某個索引的葉節點(資料入口)從頭到尾一路讀,中間可能還是會套用 filter,它不是「完全不走索引」,但「走索引但整本翻完」

假設我們有個 Non-Clustered Index

1
CREATE INDEX IX_Customers_Email ON Customers(Email);
1
SELECT Name, City FROM Customers WHERE Email LIKE '%gmail.com';

查詢條件(Email)在索引中 ✅
但要回傳的欄位 Name、City 不在索引裡 ❌

所以引擎會

  • 掃完整個索引(從第一筆 Email 到最後一筆)
  • 找到符合條件的 Email
  • 透過索引裡記錄的 Row Pointer(或 Clustered Key)

再去主表取回 Name、City → 這動作就是 Key Lookup

因為索引裡沒有完整資料,要「回表」補資料,雖然也是全掃,但是掃「索引表」,再連回原表

面向 Index Scan Clustered Index Scan
掃描對象 Non-Clustered Index(小) Clustered Index(大)
讀取欄位 只有索引欄位 所有欄位
是否需回表 可能需要(Key Lookup) 不需要
掃描範圍 全索引 全表(因索引=表)
I/O 成本 較低(索引較小) 較高(資料列完整)
常見原因 條件使用索引欄位,但篩選範圍大 條件用不到任何索引

資料以「頁(Page)」為基本單位(SQL Server 8KB/頁,8頁 = 1個 Extent)。

  • Sequential I/O:一次連續讀很多頁(配合 Read-Ahead),吞吐量高
  • Random I/O:不停跳位置取頁,IO 數多、延遲高

Clustered Index Scan 也是「全表掃描」,但因為資料在葉節點按 Clustered Key 連續排列,引擎能用 Sequential I/O(連續讀)+ Read-Ahead(預先抓一大批頁),整體吞吐量高


如果有 Clustered Index(資料在葉節點照鍵值有序),走 Clustered Index Scan 步驟會是這樣

  • 定位起點:從 Clustered Index 的最左葉節點開始(最小鍵值那頁)
  • Read-Ahead:引擎看見葉節點間有「雙向鏈結」且鍵值連續,直接向儲存層一次預取一大批連續頁(例如一次抓數十~數百頁;實際大小由引擎決定)
  • Sequential I/O:儲存層順順地把 Extent A1、A2、A3… 連續送進 Buffer Pool
  • 逐頁輸出:每頁的資料行已在同一頁,要的欄位也都在葉節點(Clustered 的葉節點存整列),不必 Key Lookup
  • 持續直線前進:下一批 Read-Ahead 繼續取 A4~A8、A9~A16… 幾乎不需要磁碟頭(或邏輯位址)大跳
  • 完成掃描:整體 I/O 成本接近「線性大吞吐量模式」

👉 連續、可預測、少跳轉 → 吞吐量高 → 大表更有感

aa

1️⃣ Index Seek 是「精準找幾筆」

資料庫利用索引快速定位 → 再用 Key Lookup 取資料。查少量資料時快。每筆都要回表(多次 I/O)若結果太多,回表次數暴增 → 效能反而下降
所以當結果很多時,Seek 的「多次跳來跳去」會拖慢速度。

但不是「Seek」一定慢,也不是「比數多」一定慢,而是要看「要的資料是不是都在索引裡」!

類型 說明 是否要回表 (Key Lookup)
Covering Index Seek 查詢要的欄位都在索引裡 ❌ 不用回表
⚠️ Non-Covering Index Seek 查詢要的欄位不全在索引裡 ✅ 需要 Key Lookup

aa

2️⃣ Index Scan 是「整份掃一遍」

資料庫直接從索引的第一筆掃到最後一筆。邏輯簡單、連續讀(Sequential I/O)、不需回表太多次,但一定會掃完整份索引,即使結果只要少數幾筆。所以當結果筆數很多(例如整張表的 30~50% 以上),掃一次反而比「跳幾萬次 Seek」快

aa

aa

表:Orders(OrderDate, CustomerId, Total, … )
索引:IX_Orders_OrderDate 在 OrderDate

1
2
3
SELECT *
FROM Orders
WHERE OrderDate >= '2026-02-01' AND OrderDate < '2026-02-02';

為什麼是 Seek

  • 條件是對索引 key(OrderDate)的「連續範圍」
  • SQL Server 可以直接跳到 2026-02-01 那段,抓到 < 2026-02-02 就停
  • 很像「翻到日曆 2/1 那頁開始抄,抄到 2/2 前就收工」

查詢條件不是索引的 leading key(最左欄位)

索引是 (A, B),你只用 B=… → 常見 Scan

a


對索引欄位做函數/運算

1
2
3
4
SELECT *
FROM Orders
WHERE YEAR(OrderDate) = 2026;

為什麼容易變 Scan,因為 YEAR(OrderDate) 是對欄位做函數加工,而索引是按 OrderDate 排序,但你現在問的是「加工後的值」,SQL Server 很難直接跳到「YEAR=2026 的區段」,結果就是把整個 IX_Orders_OrderDate 從頭掃到尾,一筆筆算 YEAR 再判斷,因為這是問「所有年份是 2026 的頁面」,這只好從頭翻,翻到每一頁都看年份是不是 2026

a


LIKE 前面有萬用字元

LIKE ‘%abc’ 通常不容易 Seek


命中比例太高

估算會撈很多筆 → Scan 比 Seek + Lookup 便宜


統計資訊判斷會撈很多

就算你以為會少,統計覺得會多,也可能 Scan

1
2
3
SELECT OrderId, OrderDate, CustomerId, Total, ShippingAddress
FROM Orders
WHERE OrderDate >= '2026-01-01';

假設這會命中 60% 的資料,而且 ShippingAddress 不在那個索引裡,如果走 Seek 會先抓到一堆符合的 key(可能幾千萬筆),每一筆都要做 Key Lookup 回到 clustered index / heap 拿 ShippingAddress,變成「找到名單很快,但要一筆筆去倉庫搬貨」造成超多隨機 I/O,SQL Server 可能改選 Index Scan / Clustered Index Scan 直接順著讀(比較連續)或掃 clustered index(因為反正要讀很多行)

r

g