聚合管道没走索引,首要检查$match是否为第一阶段且字段有对应复合索引;若$match位置错误、条件未覆盖索引前缀、含计算字段或类型不匹配,则索引失效;explain中totalDocsExamined远大于nReturned或出现COLLSCAN即为明确信号。

聚合管道没走索引?先看 $match 阶段写法是否合规
聚合慢,八成卡在第一阶段。MongoDB 只有在 $match 出现在管道最前端、且条件字段有对应索引时,才会下推索引扫描。如果写成 {$sort: {createdAt: -1}}, {$match: {status: "shipped"}},就彻底失去索引能力——排序完才过滤,等于全集合遍历。
必须保证:$match 是第一个阶段;查询字段已建索引(如 db.orders.createIndex({status: 1, createdAt: -1}));条件中不含无法下推的操作(如 $where、$expr 中的非索引字段计算)。
- 错误示例:
{$project: {year: {$year: "$date"} }}, {$match: {year: 2024}}→ year 是计算字段,无法用索引 - 正确写法:把时间范围直接写进首个
$match,例如{date: {$gte: ISODate("2024-01-01"), $lt: ISODate("2025-01-01")}} - 复合查询时,确保
$match条件覆盖索引前缀。比如索引是{userId: 1, status: 1, createdAt: -1},那{userId: "u123", status: "shipped"}能命中,但只写{status: "shipped"}就不行
内存超限报错 Sort exceeded memory limit 怎么破
Node.js 20 下驱动默认启用 useUnifiedTopology: true,但聚合阶段如 $sort 或 $group 若数据量大又没加 $limit,极易触发 100MB 内存限制并崩溃。这不是代码写错了,是 MongoDB 的硬限制。
解决路径很明确:能提前过滤的绝不留到后面;必须排序/分组的,优先加 $limit 控制中间结果集大小;实在绕不开,显式开启允许磁盘使用。
使用固定的 Next.js/Tailwind/Prisma 技术栈构建并记录独立 SaaS 项目,仅将 FastAPI 用于 Python 工作负载,并遵循严格的架构规范。
- 在
$sort后紧跟$limit,比如排行榜只取 Top 100,别等排序完百万条再截取 - 在
aggregate()调用时传入{ allowDiskUse: true }选项,避免内存爆掉(注意:磁盘排序比内存慢,仅作兜底) - 检查
$group的_id是否过于离散——比如按"$createdAt"(精确到毫秒)分组,会产生上百万桶,直接拖垮内存;应改用日期粒度:{ $dateToString: { format: "%Y-%m-%d", date: "$createdAt" } }
explain("executionStats") 返回里哪些字段真有用
别只扫一眼 executionTimeMillis。Node.js 20 驱动返回的 explain 结果里,关键要看三处:
-
nReturned和totalDocsExamined—— 如果两者差十倍以上,说明大量文档被扫描却未返回,$match没生效或索引失效 -
executionStages.stage是否出现COLLSCAN—— 出现就是没走索引,立刻回头查$match位置和索引定义 -
totalKeysExamined是否接近totalDocsExamined—— 如果远小于,说明索引被用了;如果几乎相等,可能是索引建错(比如单字段索引用于复合查询)或选择性太差(如对布尔字段建索引)
执行时加一句:db.orders.aggregate(pipeline).explain("executionStats"),在 Node.js 里用原生驱动调用即可,Mongoose 用户需通过 Model.collection.aggregate().explain() 访问底层。
分片集群下聚合为什么变慢?$lookup 和 $facet 是重灾区
在分片集群里跑聚合,$lookup 关联另一个集合时,如果被关联集合没按相同分片键分布,mongos 就得把请求广播到所有 shard,性能退化成 O(n)。同理,$facet 会为每个子管道单独执行一次全量扫描,放大问题。
- 确认
$lookup的from集合是否已分片,且localField/foreignField匹配分片键前缀(如分片键是{cityId: 1, userId: 1},那就只能用{cityId: "$cityId"}或完整两个字段做关联) - 避免在分片集合上用
$facet做多维度统计,改用多次独立聚合 + 应用层合并 - 聚合前先用
db.runCommand({splitVector: ...})检查分片键分布是否倾斜——热点分片会拖垮整个管道响应
真正容易被忽略的是:聚合管道一旦包含任何不能下推到分片的阶段(比如无分片键的 $match、跨分片的 $group),mongos 就会收拢全部数据到内存再处理,此时连接数、网卡带宽、客户端内存都可能成为瓶颈,而不是数据库本身。

















