面对千万 / 亿级数据集,MongoDB 查询慢绝大多数根源:缺少索引、集合设计不合理、查询全表扫描、内存不足以容纳工作集。下面从索引、建模、查询优化、集群、读写分离、实操避错完整说明。
一、索引优化(最核心)
MongoDB 查询不走索引会做集合扫描 (COLLSCAN),大数据集直接超时。
1. 基础索引原则
高频过滤字段建立单键索引;多条件查询使用复合索引,遵循规则:
等值字段 → 排序字段 → 范围字段
javascript
运行
// 示例:条件 status=1,按createTime排序,price范围过滤
db.order.createIndex({status:1, createTime:-1, price:1})
覆盖索引:索引包含查询全部返回字段,直接读索引,不用回表读取文档(
IXSCAN,无FETCH)
javascript
运行
// 查询只需要 status、createTime、amount,索引包含全部字段
db.order.createIndex({status:1, createTime:-1, amount:1})
db.order.find({status:1},{_id:0,status:1,createTime:1,amount:1})
避免大量索引:索引会拖慢写入,每个集合建议索引数量控制在8 个以内。
2. 特殊索引
分片键索引:分片集群分片键必须有索引;
文本索引:全文检索;
地理索引:$geoNear;
TTL 索引:自动清理过期数据,适合日志、时序数据。
3. 分析查询执行计划
javascript
运行
db.order.find({status:1}).explain("executionStats")
重点看:
stage: COLLSCAN→ 全表扫描,必须加索引executionTimeMillis查询耗时totalDocsExamined扫描文档数,尽量贴近返回结果数
大数据下
totalDocsExamined >> nReturned代表索引失效。
二、数据建模优化(Mongo 是非关系库,建模远比 SQL 重要)
1. 嵌入 vs 引用
一对少:嵌入文档,减少 $lookup 联表查询;
一对多(百万级子文档):不要嵌入,子文档数组无限膨胀会导致文档过大,拆分成独立集合,使用引用;
避免超大文档,单文档上限 16MB。
2. 分片场景:分片键设计(水平扩展)
分片集群是处理TB 级大规模数据的核心。
分片键直接决定查询性能,差的分片键会导致所有请求打到单个分片(热点分片)。
✅好分片键特征:
查询条件经常携带该字段;
数据分布均匀,避免写热点;
不要使用单调自增时间(所有新数据落在同一个分片,写热点)。
两种分片策略:
哈希分片:打散数据,适合随机读写;
范围分片:适合按时间区间范围查询(时序数据)。
业务大量按时间查询,范围分片;随机查询优先哈希分片。
3. 时序数据最佳实践(日志、监控)
使用时间序列集合 Timeseries Collections,MongoDB 原生优化时序,自动压缩,查询性能远高于普通集合;
按时间做分片,配合 TTL 自动删除旧数据。
三、查询语句本身优化
禁止大结果集全量返回,一定要分页 / 游标,不要不加 limit
javascript
运行
// ❌ 危险,大数据集拉取全部,内存打爆
db.order.find({status:1})
// ✅ 正确分页,避免skip大偏移(skip在百万偏移性能极差)
// 不要用 skip() 做深分页,改用【基于索引的游标分页】
db.order.find({status:1}).limit(100).hint({status:1, _id:1})
// 游标分页:用上次最后一条的_id作为查询条件,代替skip
db.order.find({status:1,_id:{$gt: lastId}}).limit(100)
⚠️
skip(100000)在亿级集合性能极差,会扫描前面十万条文档。
避免低效操作
正则前缀非固定:
{name:/xxx/}不走索引,{name:/^xxx/}前缀正则可以走索引;避免
$where,完全不能用索引;避免
$nin、$not,索引效率差,尽量改写查询条件;聚合管道
aggregate尽早$match过滤数据,管道越早过滤,后续处理数据量越小。
聚合管道优化
$match放在管道最前面做过滤;尽量
$project只保留需要字段,减少内存占用;大数据聚合开启允许磁盘使用
{allowDiskUse:true},但磁盘聚合性能差,优先用索引优化,避免落盘;$group如果量大,尽量利用索引。
javascript
运行
db.order.aggregate([
{$match:{status:1}}, // 第一步过滤
{$group:{_id:"$userId",total:{$sum:"$amount"}}}
],{allowDiskUse:true})
四、内存、硬件与配置调优
MongoDB 性能高度依赖内存,工作集尽量放入内存。
工作集:索引 + 热点访问文档。如果内存装不下,大量磁盘 IO,查询直接变慢。
WiredTiger 缓存:默认占用机器内存 50%,
wiredTigerCacheSizeGB参数调优;SSD 磁盘:大规模数据必须 SSD,机械盘 IO 会成为致命瓶颈;
避免大查询阻塞业务:慢查询会占用锁资源,开启慢查询日志 profile。
javascript
运行
// 记录超过100ms的慢查询
db.setProfilingLevel(1,{slowms:100})
五、集群架构方案
1. 副本集:读写分离
主节点负责写入,从节点分担大规模查询压力。
readPreference 设置
secondaryPreferred,把报表、统计、离线查询打到从节点;
注意:从节点有数据延迟,对实时性有要求不能走从库。
2. 分片集群 Sharded Cluster
当单台机器内存磁盘扛不住,做水平拆分:
Router (mongos):路由,接收应用查询;
Config Server:元数据;
Shard 节点:每个 shard 本身是副本集。
注意:
查询条件不带分片键,mongos 会广播查询到所有分片,性能暴跌;
业务查询尽量带上分片键,实现定向到单个分片,而不是广播查询。
六、针对超大统计 / 报表查询
报表统计会消耗大量资源,不要在线业务库直接跑。
方案:
预聚合:通过变更流 Change Stream,提前把统计结果计算好存入汇总集合,业务直接读汇总集合;
数据同步到数仓:Mongo → Kafka → ClickHouse,复杂统计离线数仓执行;
使用 Atlas Data Federation 做离线分析。
七、常见踩坑总结
简短总结整体流程
使用
explain("executionStats")定位慢查询,消灭 COLLSCAN;设计合适复合索引,优先覆盖索引;
建模适配访问模式,时序数据使用 Timeseries 集合;
禁止大 skip 分页,改用游标分页;
海量数据部署分片集群,合理选择分片键,查询尽量携带分片键;
报表统计不要在线库计算,预聚合或者同步外部数仓;
大查询卸载到副本集从节点做读写分离。
原文链接
欢迎访问 小易撩挨踢