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 indexes | Incorrect or missing indexes are the main cause of performance issues(錯誤或缺失的索引,是效能問題的頭號原因) |
| Every field is automatically indexed | Indexes must be manually created. The choice and order of the indexed fields is essential for performance(索引必須手動建立;選哪些欄位、順序如何,對效能至關重要) |
| NoSQL uses hashes, not indexes | MongoDB 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:
- Picks all candidate indexes —— 挑出所有候選索引。
- Runs query using them to score which is most efficient —— 用它們實際跑,評分哪個最有效率(勝出者 = winning plan)。
- 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% 的情況用
executionStats。queryPlanner(預設)只給勝出計畫、不跑;allPlansExecution會把所有候選計畫都跑一遍收集統計,是 MongoDB 內部評分用的資料,一般用不到。
解讀 Explain Plan 結果(p.36、p.43)
範例(沒有索引時,sample_airbnb 的 listingsAndReviews):
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 筆,花 10ms。
totalKeysExamined: 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+state、country+city、或只有country;但不能只給city、state而缺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(訂單狀態)→ 等值查詢 = Epostcode(郵遞區號)→ 也偏向等值 = Eorder_date(訂單日期,-1 降冪)→ 常用來排序(要最近的)= Snumber_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交替,實際只會有0、1兩個索引項指向這份 document,索引佔用很小;但如果陣列是 1..100 共 100 個不同值,就會建 100 個索引項,索引就大了。- 兩個限制:
- 同一個複合索引裡,最多只能有一個 array 欄位。若一個 collection 裡有兩個陣列欄位、想把它們放進同一個 multikey 複合索引,
insert/create時會失敗報錯。- 要多個陣列各自被索引,就分開建多個索引。
特殊索引
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 個左右(實務建議 4
8 個;看到 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