05 索引與優化

04 文件設計基礎 | 回到 MongoDB 資料建模設計 | 下一篇 → 06 設計模式 Design Patterns

這是整場最長的一段。涵蓋:索引用途、迷思、原理、查詢計畫(explain)、各種索引型別、複合索引的 ESR 法則、特殊索引(unique / partial / sparse / TTL)、以及索引的效能取捨與上限。

索引是做什麼的(p.27)

  • Speed up queries and update/sort operations —— 加速查詢,以及 update / sort 操作。
  • Avoid disk I/O —— 避免大量磁碟 I/O。
  • Reduce overall computation —— 降低整體運算量。
  • 為什麼加速 update? update 通常要先「找到」要改的 document,這個「找」就是一次查詢;有索引就能快速定位。但注意:如果你 update 的是「查詢條件用到的欄位本身」,則該索引也要跟著維護(見下方「索引與效能」)。
  • 為什麼加速 sort? 若排序欄位有索引,資料庫可直接依索引順序取出,不必把整批資料拉進記憶體再排序。沒索引的排序要「把所有符合條件的資料抓進記憶體排一次」,會產生大量 disk I/O。索引本身就是有序的,所以能省下排序成本。
  • 降低運算 / 省 I/O:本質上都是為了少讀磁碟、少算,把工作交給預先排好序的索引結構。

索引的三個迷思(p.28-29)

迷思(Misconception)事實(Reality)
MongoDB is so fast that it doesn’t need indexesIncorrect or missing indexes are the main cause of performance issues(錯誤或缺失的索引,是效能問題的頭號原因)
Every field is automatically indexedIndexes must be manually created. The choice and order of the indexed fields is essential for performance(索引必須手動建立;選哪些欄位、順序如何,對效能至關重要)
NoSQL uses hashes, not indexesMongoDB stores indexes in a B-Tree data structure with index keys in sorted order(MongoDB 用 B-Tree 存索引,key 是排序的)

講師特別澄清第三點:很多人以為 NoSQL 用 hash、不用索引,這是錯的。MongoDB 的索引是 B-Tree(不是 hash),所以是有序的——這也是它能支援範圍查詢、排序的原因。只有 _id 這個下劃線欄位會自動建唯一索引,其他欄位都要手動建。

索引怎麼運作(p.30)

  • Data in an index is ordered so it can be searched efficiently —— 索引裡的資料是排序的,所以能高效搜尋。
  • Values in an index point to document identity —— 索引裡的值指向 document identity(document 的身分識別,可理解為它在資料庫裡的 record ID / 定位)。
  • As a result, if the document moves the index doesn’t change —— 因為索引指向的是「document 身分」而非實體位置,所以即使 document 在磁碟上搬移了,索引也不用重建

講師展開:索引項存的是「這個 key → 對應到哪個 document 的身分(document identify / record ID)」。這個 record ID 的作用是識別文件。好處是:當磁碟做壓縮、整理、資料搬移(move)時,document 的物理位置變了,但它的身分 ID 不變,所以索引不需要因為資料搬移而大量改寫——只有在真正新增/刪除/改到索引欄位時才動索引。

Index Prefix Compression(索引前綴壓縮,p.31)

  • MongoDB Indexes use a special compressed format —— 用特殊的壓縮格式。
  • Each entry is a delta from the previous one —— 每一筆是相對於前一筆的 delta(差異)
  • If there are identical entries, they need only one byte —— 相同的項只需要 1 byte。
  • As indexes are inherently sorted, this makes them much smaller —— 因為索引本身有序,壓縮後小很多。
  • Smaller indexes require less RAM —— 索引越小,佔用的 RAM 越少。

白話:因為 B-Tree 索引是排序的,相鄰的 key 很相似,只存「跟前一個差多少」;完全相同的就用 1 byte 帶過。索引越小 → 越能整個放進記憶體 → 查詢越快。

什麼時候該用索引(p.32)

  • Every query should use an index! —— 每個查詢都該用到索引。
    • Scanning records is very inefficient, even if it is not all of them —— 掃描 record 非常沒效率,即使只掃一部分也一樣。
  • MongoDB can be configured to disallow queries without indexes(notablescan parameter) —— 可以設定 notablescan 參數,禁止「沒有用到索引」的查詢。

講師:如果查詢很少見(例如每週/每月才跑一次的報表),可以斟酌不為它建索引;但只要是生產環境每次都會跑的查詢,就一定要有索引支撐。

查詢計畫:Plan Cache 與 Explain

MongoDB 怎麼選索引 —— Plan Cache(p.33)

MongoDB 在 PlanCache 裡檢查「這個查詢之前有沒有選過最佳索引」:

  • 有(Active Entry Found)→ 直接用快取的計畫。
  • 沒有(Missing / Inactive)→ 進入 Query Planner:
    1. Picks all candidate indexes —— 挑出所有候選索引。
    2. Runs query using them to score which is most efficient —— 用它們實際跑,評分哪個最有效率(勝出者 = winning plan)。
    3. Adds its choice of best index to the PlanCache —— 把選出的最佳索引加入 PlanCache。

講師的完整流程(對照投影片流程圖):

  • 查詢進來 → 找 cache entry。
  • 找到 active entry → 直接用。
  • 找到 inactive entry → 評估效能(Evaluate Performance):通過就升級成 active;不通過就重新產生候選計畫評估。
  • 完全沒有(missing) → 產生並評估所有候選計畫 → 選出 winning plan → 以 inactive entry 加入 cache(之後再被驗證是否夠好,才轉 active)。

Plan cache 何時失效(p.34)

Plan cache entries 會**自動被逐出(evicted)**當:

  • Using that index becomes less efficient —— 用該索引變得比較沒效率。
  • A new relevant index is added —— 新增了一個相關的索引。
  • The server is restarted —— server 重啟(cache 在記憶體裡,重啟就沒了)。

也可以手動清除

db.<collectionName>.getPlanCache().clear()

Query explain results show information on the Query Plan and execution statistics —— explain 可以看到查詢計畫與執行統計。

講師的實務手法(回答 Miles 的提問):想強制 MongoDB 換用新建的索引,可以手動 getPlanCache().clear() 清掉快取,讓它下次查詢時重新評估所有候選計畫、重新選一次最佳索引;否則它可能還用著舊的 active plan。生產環境不建議只靠人工介入,但驗證/測試時很有用。另外,performance advisor(Atlas 上)也會建議該補的索引。

Explain verbosity —— 三種詳盡度(p.35)

  • queryPlanner —— 顯示 winning query plan,但不實際執行查詢。
  • executionStats —— 實際執行查詢並收集統計。
  • allPlansExecution —— 執行所有候選計畫並收集統計。
  • 不指定時,預設是 queryPlanner

講師:日常大約 80% 的情況用 executionStatsqueryPlanner(預設)只給勝出計畫、不跑;allPlansExecution 會把所有候選計畫都跑一遍收集統計,是 MongoDB 內部評分用的資料,一般用不到。

解讀 Explain Plan 結果(p.36、p.43)

範例(沒有索引時,sample_airbnblistingsAndReviews):

db.listingsAndReviews.find({ number_of_reviews: 50 }).explain("executionStats")
// executionStats:
//   nReturned: 11,              // 最終回傳 11 筆
//   totalKeysExamined: 0,       // 掃了 0 個索引 key(沒用索引)
//   totalDocsExamined: 5555,    // 卻掃了全部 5555 筆 document
//   executionTimeMillis: 10
  • 講師解讀:看了全部 5,555 筆 document,只回傳 11 筆,花 10mstotalKeysExamined: 0 + totalDocsExamined: 5555 = 沒用索引、全表掃描。
  • nReturned = 最終回傳筆數;totalDocsExamined = 掃過的 document 數;totalKeysExamined = 掃過的索引 key 數。
  • 理想狀態是三者接近(掃多少就回多少),代表索引精準。
  • executionTimeMillis = 執行毫秒數。

建索引後(p.43):

db.listingsAndReviews.find({number_of_reviews:50}).explain("executionStats")
// winningPlan: stage 'FETCH' → inputStage: stage 'IXSCAN', indexName: 'number_of_reviews_1'
// executionStats:
//   nReturned: 11, totalKeysExamined: 11, totalDocsExamined: 11, executionTimeMillis: 1
  • 三個數字都變 11、時間降到 1ms

三種執行 stage(p.36、p.41、p.43)

  • COLLSCAN(Collection Scan)——全表掃描,每筆 document 都看,非常沒效率。看到它通常就是要加索引的訊號(p.41)。
  • IXSCAN(Index Scan)—— 走索引掃描,回傳一批 document identities。
  • FETCH —— 依 document identity 去讀「document 本身」。
    • 講師補充:如果查詢只需要索引欄位本身(例如只回傳被索引的欄位、或只要 document identity),有機會不需要 FETCH(covered query);一旦要回傳索引以外的欄位,就得 FETCH 把整份 document 撈回來。

怎麼套用 explain(p.37-38)

  • explain 是送一個 flag 給 server,告訴它「這是一個 explain 命令」。
  • 若函式不回傳 cursor,flag 要設在 collection 物件上、在呼叫函式之前(If the function does not return a cursor, the explain flag needs to be set in the collection object instead before calling the function)。
  • 回傳 cursor 的(如 find)可以像 sort() / limit() 那樣掛在 cursor 上:...find(...).explain(...)
  • 不回傳 cursor 的(如 count() / update()),要先在 collection 物件設 flag 再呼叫:db.coll.explain().count(...) / db.coll.explain().update(...)

可 explain 的操作(Explainable Operations,p.38)find()aggregate()update()remove()findAndModify()

講師補充 aggregation 的小訣竅:aggregate 是 pipeline,.explain() 在前面各 stage 都能加,但如果放最後(例如接在某個 stage 後)也可以。實務上放前面、放後面都行,記不住就試一下。

索引型別(Index Types,p.39)

主要四種:Single-field、Compound、Multikey、Geospatial。 其他型別:Text、Hashed、Wildcard

Single-Field Indexes(單欄位索引,p.40)

  • Optimize finding values for a given field —— 針對單一欄位優化查找。
  • Specified as field name and direction —— 指定「欄位名 + 方向(1 / -1)」。
  • The direction is irrelevant for a single field index —— 單欄位索引的方向不影響(升冪降冪都能用,因為 B-Tree 兩個方向都能走)。
  • The field itself can be any data type —— 欄位可以是任何型別。
  • Avoid indexing an Object type(避免索引整個 Object 型別):
    • Indexes the whole object as a comparable blob —— 會把整個物件當成一坨可比較的 blob 索引。
    • Can use it for range searches, but better to index individual fields as less error prone —— 雖然能做範圍搜尋,但索引個別欄位更好、更不易出錯
  • Array 的索引留待 Multikey(見下)。

建立範例(p.42):

db.listingsAndReviews.createIndex({ number_of_reviews: 1 })
// → number_of_reviews_1(索引名 = 欄位_方向)

講師:開發時可以直接在 primary 上用 createIndex() 建。生產環境要評估影響(建索引對線上服務的衝擊),有更好的做法(rolling build 等)。

列出索引 / 看索引大小(p.44-45)

db.listingsAndReviews.getIndexes()            // 列出這個 collection 的所有索引定義
db.listingsAndReviews.stats().indexSizes      // 看每個索引佔多少 bytes
// { _id_: 143360, property_type_1_room_type_1_beds_1: 65536,
//   name_1: 253952, 'address.location_2dsphere': 98304, number_of_reviews_1: 45056 }

講師補充:getIndexes() 顯示的是 B-Tree 索引(一般索引與複合索引);vector search / text search 的索引不在這裡,要用 getSearchIndexes() 看(那是走另一套 API)。 看資料大小:stats() 的結果同時含「索引大小」與「資料大小、集合大小、單筆前後資料大小」等;只想看索引大小用 .indexSizes,想看整個 collection 用 db.coll.stats()

Compound Indexes(複合索引,p.46-48)

  • Compound indexes are indexes based on more than one field —— 基於多個欄位的索引。
  • The most common type of index —— 最常用的索引型別。
  • Same concept as indexes used in an RDBMS —— 概念同 RDBMS 的複合索引。
  • Up to 32 fields in a compound index —— 一個複合索引最多 32 個欄位(實務上通常 2、3 個,很少接近上限)。
  • Created like single-field index but specifying all the index fields
  • The field order and direction is very important —— 欄位順序與方向非常重要
db.people.createIndex({lastname:1, firstname:1, score:1})

前綴法則(Prefix,p.47-48)

  • Can be used as long as the first field in index is in the query —— 只要查詢用到索引的「第一個欄位」,這個複合索引就能被用上。
  • Other fields in the index do not need to be in the query —— 其他欄位不一定要出現在查詢裡。

範例:

db.x.createIndex({country:1, state:1, city:1})
db.x.find({country:"UK", city:"Glasgow"})   // 用到 country(第一欄),能用此索引…
  • …但因為跳過了 state,MongoDB 得「掃過該 country 下的每一個 state 的索引 key」才能比對 city,掃了很多 key,不夠理想(p.48)。
  • 更好的索引:把查詢真正會用的欄位排在前面、連續:
db.x.createIndex({country:1, city:1, state:1})   // country + city 連續匹配,理想

講師強調:複合索引「越往前的欄位越重要」,且必須是連續的前綴才能高效匹配。{country, city, state} 這個索引能支援的查詢條件是 country+city+statecountry+city、或只有 country;但不能只給 citystate 而缺 country(缺了第一個前綴欄位就用不上)。

欄位順序法則:ESR(Field Order Matters,p.49)

複合索引的欄位排序遵循 ESR 法則:Equality → Sort → Range

  • E(Equality)First —— 等值查詢的欄位放最前面。
    • What fields, for a typical query, are filtered the most —— 哪些欄位在典型查詢裡最常被拿來精確過濾。
    • Selectivity is NOT cardinality, selective can be a boolean choice —— 「選擇性」不等於「基數」;即使是布林值也可能很有選擇性。
    • Normally Male/Female is not selective(性別通常沒選擇性);Dispatched vs Delivered IS selective though(訂單「已出貨/已送達」這種狀態就很有選擇性)。
  • Then S(Sort)and R(Range)
    • Sorts are much more expensive than range queries when no index is used —— 沒索引時,排序比範圍查詢貴很多,所以 Sort 欄位優先於 Range 進索引。
    • The directions matter when doing range queries —— 做範圍查詢時,方向(1/-1)會有影響

投影片練習:這個複合索引支援什麼查詢?

db.orders.createIndex({order_status:1, postcode:1, order_date:-1, number_of_items:-1})

講師拆解:

  • order_status(訂單狀態)→ 等值查詢 = E
  • postcode(郵遞區號)→ 也偏向等值 = E
  • order_date(訂單日期,-1 降冪)→ 常用來排序(要最近的)= S
  • number_of_items(品項數量)→ 常用範圍查(大於/小於某數)= R
  • 對照 ESR:前面兩個是 Equality,再來 Sort(date),最後 Range(items)。
  • 等值查詢的方向不重要,但排序/範圍的方向很重要:例如要「最近 7 天、由近到遠」,date 就要對應正確方向。排序欄位在索引裡的方向要跟查詢的排序方向一致(或整體相反也行),否則用不上索引排序。
  • 按 ESR 排好後再 createIndex,就不會有大問題。

32 欄位上限

一個複合索引最多 32 個欄位(p.46)。這是資料庫層級的限制,實務上一個複合索引通常就 2~3 個欄位。

Multikey Indexes(多鍵索引,p.50)

  • A multikey index is an index that has an array as one of the fields being indexed —— 被索引的欄位裡有一個是陣列,就是 multikey index。
    • Can index primitives, documents, or sub-arrays —— 可索引陣列裡的基本型別、物件、子陣列。
    • Are created using createIndex() just as single-field indexes —— 建法跟一般索引一樣(語法相同,MongoDB 自動判定它是 multikey)。
  • An index entry is created on each unique value found in an array —— 陣列裡每個唯一值都會產生一個索引項。

講師展開(重點):

  • 你不用特別宣告 multikey,語法跟一般索引一樣;只要被索引的欄位是陣列,MongoDB 就自動當它是 multikey。
  • 索引項只針對陣列裡出現的「值」建立,記的是「值 → 該值出現在哪個 document」的對應,不記陣列裡有幾個、順序、重複幾次。例:一個陣列有 100 個元素但值是 0/1 交替,實際只會有 01 兩個索引項指向這份 document,索引佔用很小;但如果陣列是 1..100 共 100 個不同值,就會建 100 個索引項,索引就大了
  • 兩個限制
    1. 同一個複合索引裡,最多只能有一個 array 欄位。若一個 collection 裡有兩個陣列欄位、想把它們放進同一個 multikey 複合索引,insert / create 時會失敗報錯。
    2. 要多個陣列各自被索引,就分開建多個索引。

特殊索引

Hashed Indexes(雜湊索引,p.51)

  • Hashed Indexes index a 20 byte md5 of the BSON value —— 索引的是 BSON 值的 20-byte md5 雜湊。
  • Support exact match only —— 只支援精確匹配(雜湊後就沒有大小順序,不能範圍查、不能排序)。
  • Cannot be used for unique constraints —— 不能拿來做唯一性約束。
  • Can potentially reduce index size if original values are large —— 若原值很大,雜湊後可縮小索引。
  • Downside: random values in a BTree use excessive resources —— 缺點:雜湊值是隨機的,塞進 B-Tree 會耗費較多資源(因為 B-Tree 喜歡有序)。
db.people.createIndex({ name : "hashed" })

講師:hashed index 主要用途是**分片(sharding)**時做雜湊分佈(把資料均勻打散到各 shard)。因為它把值變成固定長度的雜湊、失去順序,只能做等值匹配。它跟一般索引的差別就在方向/順序——一般 B-Tree 有序,hashed 是固定雜湊。

Unique Index(唯一索引,p.52)

  • Indexes can enforce a unique constraint —— 索引可強制唯一性約束。
db.a.createIndex({custid: 1}, {unique: true})   // 加 {unique: true}
  • NULL is a value and so only one record can have a NULL in unique field —— NULL 也算一個值,所以唯一欄位裡最多只能有一筆是 NULL

講師的重要提醒與範例:

  • unique 是布林選項(true/false),加在既有欄位上就讓它唯一。
  • 空值(NULL)本身也是一個值,所以「多筆 document 缺這個欄位」= 多筆都是 NULL → 會違反唯一性,只能有一筆
  • 實例:customer 有 card_number 欄位,但有些客戶沒有卡號(欄位不存在 = NULL)。若對 card_number 建唯一索引,第二個沒有卡號的客戶就會因「第二筆 NULL」而衝突、插不進去。這種「欄位可能不存在、但存在時要唯一」的情境,要搭配 partial index(見下)處理。

Partial Index(部分索引,p.53)

  • Partial indexes index a subset of documents based on values —— 依條件只索引一部分 document。
  • Can greatly reduce index size —— 大幅減少索引大小。
db.orders.createIndex(
  { customer: 1 },
  { partialFilterExpression: { archived: false } }   // 只索引 archived=false 的
)

講師範例:訂單有幾千萬筆,但實際上使用者只查「最近 7 天 / 未封存(archived: false)」的資料。用 partialFilterExpression 只對這部分建索引:

  • 已封存(archived: true)的舊資料不進索引 → 索引大小大幅縮小、更省記憶體。
  • archived 這種業務欄位當條件很自然;也解決了前面 unique 的痛點(可對「有卡號的子集」建唯一索引)。
  • 是否用 partial,依你的業務查詢範圍決定(例如只查最近 10%~20% 的資料時很划算)。

Sparse Index(稀疏索引,p.54)

  • Sparse Indexes don’t index missing fields —— 欄位不存在的 document 不進索引
  • Sparse Indexes are superseded by Partial Indexes —— sparse 已被 partial index 取代(partial 更通用)。
db.scores.createIndex({score: 1}, {sparse: true})

講師:sparse 的特性是「某些 document 沒有這個欄位(欄位為空/不存在),就不為它建索引項」——例如 school_id 這種欄位,沒有的 document 就跳過。這其實就是 partial index 的一個特例,所以現在用 partial index 就能涵蓋 sparse 的功能,不必再用 sparse。

Time to Live (TTL) Indexes(p.55-56)

  • Not a special Index, just a flag on an index on a date —— 不是特殊索引,只是「在一個日期欄位的索引上加一個 flag」。
  • MongoDB automatically deletes documents using the index based on time —— MongoDB 依時間自動刪除 document。
  • Background thread in server runs regularly to delete expired documents —— server 有背景執行緒定期跑,刪除過期文件。
db.t.createIndex({"create_date":1}, {expireAfterSeconds: 60})
// TTL set to auto delete where create_date > 1 minute

另一種用法(p.56)

  • Put the date you want it deleted in a field (expireAt) —— 在欄位放「想刪除的時間點」。
  • Add a TTL with expireAfterSeconds set to 0 —— TTL 設 expireAfterSeconds: 0,時間一到就刪。

注意事項(Watch out of unplanned write load)

  • Better to write your own programmatic data cleaner —— 自己寫程式化的清理器更好。
  • Schedule using a framework like cron/scheduler/Atlas —— 用 cron / scheduler / Atlas 排程。

講師的重要提醒:

  • TTL 靠 MongoDB 內部 background 執行緒依時間刪;但它不保證「時間一到就即刻刪」,只是定期跑。若同一時間有 500 萬筆同時到期,它會慢慢清、不會瞬間清完,因為它也是背景排程。
  • 負載風險:TTL 刪除是真實的寫入操作,會產生寫入負載,可能影響正常業務。若一次到期量巨大(幾百萬~幾千萬筆同時刪),會衝擊系統。
  • 設計建議
    • 讓刪除時間錯開(不要讓大量資料的到期時間卡在同一個整點,例如都設 00:00:00)——加一點抖動(jitter)。
    • 到期量很大時,寧可自己寫排程清理器(cron / scheduler / Atlas 排程),比純 TTL 更可控。
  • 刪除前務必考慮資料重要性:TTL 刪了就沒了,沒有辦法叫 MongoDB 幫你救回來。刪之前想清楚——資料重不重要、要不要先備份、刪錯能不能還原。若資料重要,最好先自己寫歸檔(archive)、記 log,確認無誤再刪,才能從 log 復原。

索引與效能(Indexes and Performance,p.57)

  • Indexes improve read performance when used —— 用到時提升讀效能。
  • Each index adds ~10% overhead for writes —— 每個索引約增加 10% 的寫入負擔(Hashed / multikey / text / wildcard 索引可能更多)。
  • An index is modified any time a document:
    • Is inserted(新增,影響大多數索引)
    • Is deleted(刪除,影響大多數索引)
    • Is updated in such a way that its indexed fields change(更新到「被索引的欄位」時)

講師展開:

  • insert:新增 document 時,90% 以上的索引都要跟著更新(新 document 的各欄位值要插進對應索引)。
  • delete:刪除時,該 document 在各索引裡的項也要移除,同樣影響大多數索引。
  • update只有當你改到「被索引的欄位」時,對應索引才需要維護。如果只是改一個沒被索引的欄位,就不影響索引維護——所以「查詢條件用到的欄位」與「更新會動到的欄位」要一起權衡。
  • 結論:索引不是越多越好。索引一定要被查詢用到才值得建;沒被用到的索引只是白白拖慢寫入、佔記憶體。判準是「這個欄位一定要被查」才加索引。

Index Limitations(索引上限,p.58)

  • Up to 64 indexes per collection - Avoid being close to this upper bound —— 一個 collection 最多 64 個索引,別接近上限。
  • Write performance degrades to unusable between 20 and 30 —— 索引數在 20~30 之間時,寫入效能會退化到不堪用。
  • 4 indexes per collection is a good recommended number —— 每個 collection 建議 4 個左右(實務建議 48 個;看到 1020 個就要警覺)。

Use Indexes with Care(謹慎用索引,p.59)

  • Every query should use an index —— 每個查詢都該用到索引。
  • Every index should be used by a query —— 每個索引都該被某個查詢用到(沒被用到的要刪掉)。
  • Indexes require server memory, be mindful about the choice of key —— 索引吃記憶體,慎選 key。

講師的收尾(實務清理索引):

  • 兩個原則互為表裡:每個 query 要有索引撐;每個索引要有 query 用。沒被用到的索引,白佔記憶體、拖慢寫入,該砍。
  • Atlas 上的 Performance Advisor 會標出「7 天內(或更長期間)沒被用到的索引」與「建議新增的索引」。可據此清掉沒用的、補上該建的。
  • 刪索引也要小心:先確認它真的長期沒被用到(例如某些每週/每月/每季才跑一次的查詢,可能剛好在觀察期外),再刪。
  • 索引維護的心法:每個查詢要有索引、每個索引要被查詢用,並持續 review。

小結

  • 索引解決三件事:加速 query / update / sort、避免 disk I/O、降低運算;底層是有序的 B-Tree(不是 hash),指向 document identity,所以資料搬移不用重建。
  • explain(executionStats)nReturned / totalKeysExamined / totalDocsExamined,判斷是 COLLSCAN(壞) 還是 IXSCAN + FETCH(好)
  • 複合索引記 ESR:Equality → Sort → Range;只有連續前綴能高效匹配;一個複合索引 ≤ 32 欄位、且最多一個陣列欄位(multikey 限制)。
  • 特殊索引:unique(NULL 也算值)、partial(只索引子集、取代 sparse)、hashed(只等值、多用於 sharding)、TTL(自動過期刪除,小心負載與不可還原)
  • 索引不是越多越好:每個索引約 +10% 寫入負擔,建議每 collection 4~8 個,用 Performance Advisor 定期 review。

→ 下一篇:06 設計模式 Design Patterns