
一、索引優化:構建高效查詢的基石
(一)索引類型與適用場景
1. 五大核心索引類型
索引類型 | 適用場景 | 示例代碼 | 性能影響 |
---|
單字段索引 | 單條件查詢(如用戶ID、狀態字段) | db.users.createIndex({ user_id: 1 }) | 低 |
復合索引 | 多條件組合查詢/排序(如狀態+時間) | db.orders.createIndex({ status: 1, time: -1 }) | 中 |
多鍵索引 | 數組字段查詢(如標簽、商品規格) | db.products.createIndex({ specs.size: 1 }) | 中 |
文本索引 | 全文搜索(如文章內容、評論) | db.articles.createIndex({ content: "text" }) | 高 |
哈希索引 | 分片鍵/等值查詢(需均勻分布數據) | sh.shardCollection("data", { _id: "hashed" }) | 高 |
2. 復合索引設計黃金法則(ESR原則)
- E(Equality等值查詢):優先放置等值查詢字段(如
user_id
) - S(Sort排序):其次放置排序字段(如
create_time
) - R(Range范圍查詢):最后放置范圍查詢字段(如
price
)
示例:
db.orders.createIndex({ status: 1, create_time: -1, price: 1 })
(二)覆蓋索引與避免回表
1. 覆蓋索引原理
- 定義:索引包含查詢所需的所有字段,無需訪問文檔數據
- 優勢:減少磁盤I/O,提升查詢速度
示例:
db.orders.createIndex({ status: 1, create_time: -1 }, { amount: 1 })
db.orders.find({ status: "paid" }, { create_time: 1, amount: 1 }
).hint("status_1_create_time_-1")
2. 回表優化對比
操作 | 覆蓋索引(命中) | 非覆蓋索引(回表) |
---|
掃描類型 | 索引掃描(IXSCAN) | 索引掃描+文檔掃描(COLLSCAN) |
內存占用 | 低 | 高 |
示例延遲 | 20ms | 120ms |
二、查詢模式優化:減少數據掃描量
(一)規避全集合掃描
1. 低效操作符優化
反例(全掃描) | 正例(索引友好) | 性能提升 |
---|
db.users.find({ email: /@gmail$/ }) | db.users.find({ email: { $regex: "^user" } }) | 10倍+ |
db.orders.find({ qty: { $exists: true } }) | db.orders.createIndex({ qty: 1 }); db.orders.find({ qty: { $gt: 0 } }) | 5倍+ |
2. 前綴匹配優化
db.users.find({ email: /@gmail.com$/ })
db.users.find({ email: /^admin/ })
(二)分頁查詢性能優化
1. 傳統分頁(skip+limit)的缺陷
- 問題:
skip(n)
會掃描前n條文檔,深度分頁時性能驟降 - 示例:
db.orders.find().skip(100000).limit(10)
需掃描100010條文檔
2. 游標分頁(基于排序字段)
const lastTime = new Date("2023-10-01T00:00:00");
db.orders.find({ create_time: { $lt: lastTime }
}).sort({ create_time: -1 }).limit(10)
3. 鍵值分頁(基于_id)
const lastId = ObjectId("6401f015f9b1b4f2a1c000001");
db.orders.find({ _id: { $gt: lastId }
}).sort({ _id: 1 }).limit(10)
(三)聚合管道優化
1. 管道階段順序優化
- 原則:盡早過濾數據(
$match
前置),減少后續階段處理量
示例:
db.sales.aggregate([{ $group: { _id: "$product", total: { $sum: "$amount" } } },{ $match: { total: { $gt: 1000 } } }
])
db.sales.aggregate([{ $match: { amount: { $gt: 10 } } }, { $group: { _id: "$product", total: { $sum: "$amount" } } }
])
2. 使用索引加速聚合
db.sales.createIndex({ product: 1, amount: 1 })
db.sales.aggregate([{ $match: { product: "P001" } },{ $group: { _id: null, total: { $sum: "$amount" } } }
]).hint({ product: 1, amount: 1 })
三、分片集群優化:水平擴展查詢能力
(一)分片鍵選擇策略
1. 三大分片鍵類型對比
類型 | 適用場景 | 示例字段 | 數據分布 | 查詢性能 |
---|
哈希分片 | 高并發寫、均勻分布 | user_id、order_id | 均衡 | 等值查詢高效 |
范圍分片 | 時間序列、范圍查詢 | create_time、date | 可能熱點 | 范圍查詢高效 |
復合分片 | 混合查詢需求 | region+time | 較均衡 | 組合查詢高效 |
2. 分片鍵設計禁忌
- 避免低基數字段:如
status
(僅少數取值,導致數據傾斜) - 避免頻繁更新字段:如
last_login
(影響分片穩定性)
3. 分片集群部署示例
(二)分片集群查詢流程
- 路由階段:mongos解析查詢,確定目標Shard
- 并行查詢:各Shard執行本地查詢(利用本地索引)
- 結果合并:mongos聚合各Shard結果,返回客戶端
優化點:
- 確保分片鍵包含在查詢條件中,避免全分片掃描
- 為每個Shard的本地集合創建復合索引
四、硬件與配置調優:釋放底層性能
(一)內存配置最佳實踐
1. WiredTiger引擎參數
storage:wiredTiger:engineConfig:cacheSizeGB: 32 collectionConfig:blockSize: 4096
2. 內存使用監控
db.serverStatus().mem
(二)磁盤與文件系統優化
1. 存儲介質選擇
類型 | 隨機IOPS | 延遲 | 適用場景 | 成本 |
---|
NVMe SSD | 20000+ | <1ms | 主節點、熱數據 | 高 |
SATA SSD | 5000+ | 1-5ms | 從節點、溫數據 | 中 |
HDD | 200+ | 10-20ms | 冷數據、備份 | 低 |
2. 文件系統配置
echo never > /sys/kernel/mm/transparent_hugepage/enabled
mount -t xfs -o noatime,nodiratime /dev/nvme0n1p1 /data/mongodb
(三)讀寫關注調優
1. 寫入關注(Write Concern)
場景 | 配置 | 延遲(ms) | 數據可靠性 |
---|
日志寫入 | { w: 1 } | 1-5 | 弱一致 |
訂單創建 | { w: majority } | 5-20 | 強一致 |
資產變更 | { w: majority, j: true } | 20-50 | 最強一致 |
2. 讀取關注(Read Preference)
db.orders.find().readPreference("secondaryPreferred")
db.orders.find().readPreference("nearest")
五、監控與分析:定位性能瓶頸
(一)執行計劃分析(Explain)
1. 核心指標解析
const plan = db.orders.find({ status: "paid" }).explain("executionStats")
指標 | 含義 | 優化目標 |
---|
executionTimeMillis | 總執行時間 | <100ms |
totalDocsExamined | 掃描的文檔數 | 盡可能接近查詢結果數 |
nReturned | 返回的文檔數 | 等于查詢結果數 |
stage | 執行階段(如IXSCAN/COLLSCAN) | 確保為IXSCAN(索引掃描) |
2. 優化示例
db.orders.find({ customer: "Alice" }).explain()
db.orders.createIndex({ customer: 1 })
db.orders.find({ customer: "Alice" }).explain()
(二)慢查詢日志
1. 配置慢查詢監控
operationProfiling:mode: slowOpslowOpThresholdMs: 100 slowOpSampleRate: 1.0
2. 分析慢查詢日志
db.system.profile.find({ts: { $gt: new Date("2023-10-01") },millis: { $gt: 100 }
}).sort({ millis: -1 })
六、實戰案例:電商訂單系統性能優化
(一)場景描述
- 數據規模:訂單量10億條,日均新增100萬條
- 高頻查詢:
- 按用戶ID查詢最近100條訂單(
user_id + create_time
) - 統計已支付訂單總量(
status=paid
) - 按日期范圍查詢訂單金額分布(
create_time + amount
)
(二)優化前性能指標
查詢類型 | 平均延遲 | 掃描文檔數 | 索引使用情況 |
---|
用戶訂單查詢 | 800ms | 10000+ | 未命中索引 |
支付統計 | 1200ms | 全表掃描 | 無索引 |
范圍查詢 | 1500ms | 500萬+ | 部分索引命中 |
(三)優化方案實施
1. 索引優化
db.orders.createIndex({ user_id: 1, create_time: -1
}, { amount: 1, status: 1
})
db.orders.createIndex({ status: 1 })
db.orders.createIndex({ create_time: 1, amount: 1 })
2. 分片策略
sh.shardCollection("ecommerce.orders", { user_id: "hashed" })
3. 查詢改寫
const lastTime = new Date("2023-10-05T00:00:00");
db.orders.find({user_id: "U123",create_time: { $lt: lastTime }
}).sort({ create_time: -1 })
.limit(100)
.hint("user_id_1_create_time_-1")
(四)優化后性能指標
查詢類型 | 平均延遲 | 掃描文檔數 | 索引使用情況 |
---|
用戶訂單查詢 | 65ms | 100條 | 覆蓋索引命中 |
支付統計 | 45ms | 1200條 | 單字段索引命中 |
范圍查詢 | 180ms | 5000條 | 復合索引命中 |
七、面試核心考點與應答策略
(一)基礎問題
-
Q:如何判斷查詢是否使用了索引?
A:使用explain()
分析執行計劃,若stage
為IXSCAN
則命中索引;查看totalDocsExamined
是否接近查詢結果數,若遠大于則可能全表掃描。
-
Q:復合索引的字段順序如何影響性能?
A:遵循ESR原則:等值查詢字段→排序字段→范圍查詢字段。例如,查詢status=paid AND time>2023-01-01 AND sort by amount
,索引應為{status:1, time:1, amount:1}
。
(二)進階問題
-
Q:深度分頁為什么慢?如何優化?
A:
-
Q:分片集群中如何避免數據傾斜?
A:
- 選擇高基數分片鍵(如用戶ID哈希)
- 監控塊分布,通過
sh.rebalanceShard()
手動遷移熱點塊 - 啟用自動平衡器(默認開啟),調整塊大小(如256MB)
(三)架構設計問題
Q:設計一個千萬級數據的查詢系統,如何優化MongoDB性能?
回答思路:
- 索引層:
- 為高頻查詢字段創建復合索引,確保覆蓋查詢
- 使用文本索引優化全文搜索場景
- 集群層:
- 分片集群部署,哈希分片鍵均勻分布數據
- 獨立部署mongos節點,橫向擴展路由能力
- 存儲層:
- 使用SSD存儲熱數據,HDD存儲冷數據
- 調整WiredTiger緩存大小,確保索引常駐內存
- 查詢層:
- 避免
skip
深度分頁,改用游標分頁 - 聚合查詢前置過濾條件,減少數據處理量
八、性能優化的黃金法則
(一)索引優先原則
- 80%的性能問題可通過優化索引解決,優先分析查詢是否命中索引
- 定期清理冗余索引(
db.xxx.getIndexes()
),減少寫入開銷
(二)數據分片原則
- 單集合數據量超過1TB時啟用分片,分片鍵選擇需平衡查詢與分布
- 每個Shard節點數≥3(1主2從),確保高可用
(三)監控驅動原則
- 建立常態化監控:索引使用率、慢查詢頻率、分片負載均衡
- 使用
mongostat
實時監控節點狀態,mongotop
分析讀寫分布
(四)漸進優化原則
- 分析:通過
explain()
和慢查詢日志定位瓶頸 - 驗證:小范圍測試優化方案(如灰度環境)
- 部署:滾動更新索引或分片配置,避免服務中斷
- 監控:對比優化前后性能指標,持續迭代