
一、索引优化:构建高效查询的基石
(一)索引类型与适用场景
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
)
示例:
javascript
// 查询:status=paid + 按create_time降序 + price>100
db.orders.createIndex({ status: 1, create_time: -1, price: 1 })
(二)覆盖索引与避免回表
1. 覆盖索引原理
- 定义:索引包含查询所需的所有字段,无需访问文档数据
- 优势 :减少磁盘I/O,提升查询速度
示例:
javascript
// 创建覆盖索引(包含status、create_time、amount)
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. 前缀匹配优化
javascript
// 反例:后缀匹配(无法使用索引)
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. 游标分页(基于排序字段)
javascript
// 按时间戳排序,记录最后一条的时间戳
const lastTime = new Date("2023-10-01T00:00:00");
// 下一页查询
db.orders.find({
create_time: { $lt: lastTime }
}).sort({ create_time: -1 }).limit(10)
3. 键值分页(基于_id)
javascript
// 记录最后一条的_id
const lastId = ObjectId("6401f015f9b1b4f2a1c000001");
// 下一页查询
db.orders.find({
_id: { $gt: lastId }
}).sort({ _id: 1 }).limit(10)
(三)聚合管道优化
1. 管道阶段顺序优化
- 原则 :尽早过滤数据(
$match
前置),减少后续阶段处理量
示例:
javascript
// 反例:先分组再过滤(处理全量数据)
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. 使用索引加速聚合
javascript
// 创建复合索引
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路由节点 Shard1副本集 Shard2副本集 主节点 从节点 主节点 从节点
(二)分片集群查询流程
- 路由阶段:mongos解析查询,确定目标Shard
- 并行查询:各Shard执行本地查询(利用本地索引)
- 结果合并:mongos聚合各Shard结果,返回客户端
优化点:
- 确保分片键包含在查询条件中,避免全分片扫描
- 为每个Shard的本地集合创建复合索引
四、硬件与配置调优:释放底层性能
(一)内存配置最佳实践
1. WiredTiger引擎参数
yaml
# mongod.conf配置示例
storage:
wiredTiger:
engineConfig:
cacheSizeGB: 32 # 建议为物理内存的50%-80%,确保索引常驻内存
collectionConfig:
blockSize: 4096 # 减小块大小,提升小文档查询效率
2. 内存使用监控
bash
# 查看内存使用情况
db.serverStatus().mem
# 关键指标:
# - "resident":常驻内存大小(理想值接近cacheSizeGB)
# - "virtual":虚拟内存使用(应避免过高,否则触发swap)
(二)磁盘与文件系统优化
1. 存储介质选择
类型 | 随机IOPS | 延迟 | 适用场景 | 成本 |
---|---|---|---|---|
NVMe SSD | 20000+ | <1ms | 主节点、热数据 | 高 |
SATA SSD | 5000+ | 1-5ms | 从节点、温数据 | 中 |
HDD | 200+ | 10-20ms | 冷数据、备份 | 低 |
2. 文件系统配置
bash
# 禁用透明大页(THP)提升性能
echo never > /sys/kernel/mm/transparent_hugepage/enabled
# XFS文件系统挂载选项
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)
javascript
// 从Secondary节点读取(读扩展)
db.orders.find().readPreference("secondaryPreferred")
// 从最近的节点读取(全球部署)
db.orders.find().readPreference("nearest")
五、监控与分析:定位性能瓶颈
(一)执行计划分析(Explain)
1. 核心指标解析
javascript
const plan = db.orders.find({ status: "paid" }).explain("executionStats")
指标 | 含义 | 优化目标 |
---|---|---|
executionTimeMillis |
总执行时间 | <100ms |
totalDocsExamined |
扫描的文档数 | 尽可能接近查询结果数 |
nReturned |
返回的文档数 | 等于查询结果数 |
stage |
执行阶段(如IXSCAN/COLLSCAN) | 确保为IXSCAN(索引扫描) |
2. 优化示例
javascript
// 优化前:COLLSCAN(全表扫描)
db.orders.find({ customer: "Alice" }).explain()
// 优化后:IXSCAN(索引扫描)
db.orders.createIndex({ customer: 1 })
db.orders.find({ customer: "Alice" }).explain()
(二)慢查询日志
1. 配置慢查询监控
yaml
# mongod.conf
operationProfiling:
mode: slowOp
slowOpThresholdMs: 100 # 慢查询阈值(毫秒)
slowOpSampleRate: 1.0 # 采样率(1.0表示记录所有慢查询)
2. 分析慢查询日志
bash
# 查看慢查询统计
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
)
- 按用户ID查询最近100条订单(
(二)优化前性能指标
查询类型 | 平均延迟 | 扫描文档数 | 索引使用情况 |
---|---|---|---|
用户订单查询 | 800ms | 10000+ | 未命中索引 |
支付统计 | 1200ms | 全表扫描 | 无索引 |
范围查询 | 1500ms | 500万+ | 部分索引命中 |
(三)优化方案实施
1. 索引优化
javascript
// 用户订单查询索引(覆盖查询)
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. 分片策略
javascript
// 哈希分片(用户ID均匀分布)
sh.shardCollection("ecommerce.orders", { user_id: "hashed" })
3. 查询改写
javascript
// 优化后用户订单查询(游标分页)
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:-
原因:
skip(n)
需扫描前n条文档,时间复杂度O(n) -
优化:
- 使用
search_after
基于排序字段分页 - 记录最后一条的排序值,通过范围查询替代
skip
javascriptdb.orders.find({ create_time: { $lt: last_time } }).sort({ create_time: -1 }).limit(10)
- 使用
-
-
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()
和慢查询日志定位瓶颈 - 验证:小范围测试优化方案(如灰度环境)
- 部署:滚动更新索引或分片配置,避免服务中断
- 监控:对比优化前后性能指标,持续迭代