
一、索引优化:构建高效查询的基石
(一)索引类型与适用场景
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()
和慢查询日志定位瓶颈 - 验证:小范围测试优化方案(如灰度环境)
- 部署:滚动更新索引或分片配置,避免服务中断
- 监控:对比优化前后性能指标,持续迭代