MongoDB:$unwind能否利用索引?聚合查询覆盖索引问题咨询
关于MongoDB含$unwind聚合查询的索引利用问题
我不清楚怎么在带$unwind的聚合查询里用上索引。本来以为保留内嵌数组的文档结构更高效,但如果实在做不到,只能考虑拆成两个带索引的集合,用$lookup来实现。我想搞清楚两个问题:
- MongoDB能不能对包含内嵌数组的文档用索引实现覆盖查询?
- 是不是必须拆分两个集合才能让下面的聚合查询用上覆盖索引?
集合结构
{ _id: {$oid:''}, clientId: 1005, displayName: 'my client', divisions: [ {_id: 'division1', skillsRequired: []} ] }
创建的索引
{ v: 2, key: { clientId: 1, displayName: 1, 'divisions._id': 1, 'divisions.skillsRequired': 1 }, name: 'client_skills' }
聚合查询
db.client.aggregate([ {$unwind:"$divisions"}, { $project: { clientId: 1, displayName: 1, divisionId: "$divisions._id", skillsRequired: "$divisions.skillsRequired" } } ])
查询执行计划
{ explainVersion: '1', stages: [ { '$cursor': { queryPlanner: { namespace: '63ac857fbaf9e86e3f3f9bc1_gps.client', indexFilterSet: false, parsedQuery: {}, queryHash: '5CF751FE', planCacheKey: '9AA9418D', maxIndexedOrSolutionsReached: false, maxIndexedAndSolutionsReached: false, maxScansToExplodeReached: false, winningPlan: { stage: 'PROJECTION_SIMPLE', transformBy: { _id: 1, clientId: 1, displayName: 1, divisions: 1 }, inputStage: { stage: 'COLLSCAN', direction: 'forward' } }, rejectedPlans: [] }, executionStats: { executionSuccess: true, nReturned: 181, executionTimeMillis: 1, totalKeysExamined: 0, totalDocsExamined: 181, executionStages: { stage: 'PROJECTION_SIMPLE', nReturned: 181, executionTimeMillisEstimate: 0, works: 183, advanced: 181, needTime: 1, needYield: 0, saveState: 1, restoreState: 1, isEOF: 1, transformBy: { _id: 1, clientId: 1, displayName: 1, divisions: 1 }, inputStage: { stage: 'COLLSCAN', nReturned: 181, executionTimeMillisEstimate: 0, works: 183, advanced: 181, needTime: 1, needYield: 0, saveState: 1, restoreState: 1, isEOF: 1, direction: 'forward', docsExamined: 181 } }, allPlansExecution: [] } }, nReturned: Long("181"), executionTimeMillisEstimate: Long("0") }, { '$unwind': { path: '$divisions' }, nReturned: Long("185"), executionTimeMillisEstimate: Long("0") }, { '$project': { _id: true, displayName: true, clientId: true, divisionId: '$divisions._id', skillsRequired: '$divisions.skillsRequired' }, nReturned: Long("185"), executionTimeMillisEstimate: Long("0") } ], serverInfo: { host: 'ac-553hexy-shard-00-02.rvkcvj5.mongodb.net', port: 27017, version: '5.0.14', gitVersion: '1b3b0073a0b436a8a502b612f24fb2bd572772e5' }, serverParameters: { internalQueryFacetBufferSizeBytes: 104857600, internalQueryFacetMaxOutputDocSizeBytes: 104857600, internalLookupStageIntermediateDocumentMaxSizeBytes: 16793600, internalDocumentSourceGroupMaxMemoryBytes: 104857600, internalQueryMaxBlockingSortMemoryUsageBytes: 33554432, internalQueryProhibitBlockingMergeOnMongoS: 0, internalQueryMaxAddToSetBytes: 104857600, internalDocumentSourceSetWindowFieldsMaxMemoryBytes: 104857600 }, command: { aggregate: 'client', pipeline: [ { '$unwind': '$divisions' }, { '$project': { clientId: 1, displayName: 1, divisionId: '$divisions._id', skillsRequired: '$divisions.skillsRequired' } } ], cursor: {}, '$db': 'gps' }, ok: 1, '$clusterTime': { clusterTime: Timestamp({ t: 1675954493, i: 23 }), signature: { hash: Binary(Buffer.from("1bb78d5fc18f3164d4e5d60704192be26fbd2572", "hex"), 0), keyId: Long("7163328034601173016") } }, operationTime: Timestamp({ t: 1675954493, i: 23 }) }
问题解答
1. MongoDB能否对包含内嵌数组的文档使用索引来覆盖查询?
可以,但需要满足两个核心条件:
- 必须有索引前缀匹配的过滤条件:比如在聚合开头加
$match阶段筛选clientId这类索引前缀字段,让优化器认为使用索引的成本更低。 - 所有返回字段都包含在索引中:你的索引已经覆盖了查询所需的
clientId、displayName、divisions._id、divisions.skillsRequired,这部分是符合要求的。
如果没有过滤条件,MongoDB会优先选择全表扫描——尤其是文档量较小时,直接读取文档的开销比遍历索引条目更低。
2. 是否需要拆分两个集合才能覆盖下方的聚合查询?
不需要。只要调整查询逻辑,让优化器有理由使用索引即可:
- 在聚合前添加
$match阶段,利用索引前缀过滤文档,示例:
db.client.aggregate([ {$match: {clientId: 1005}}, // 匹配索引前缀字段 {$unwind:"$divisions"}, { $project: { clientId: 1, displayName: 1, divisionId: "$divisions._id", skillsRequired: "$divisions.skillsRequired" } } ])
此时优化器会调用client_skills索引,通过$match快速定位目标文档,后续的$unwind和$project直接从索引中提取数据,实现覆盖查询。
只有当集合文档量极大,且频繁针对divisions做独立查询或聚合时,拆分集合(比如clients和divisions两个集合)才会带来明显的性能收益。
当前查询未使用索引的原因
你的查询没有任何过滤条件,MongoDB优化器评估后认为:遍历181个文档的全表扫描,成本远低于遍历索引再提取数据的开销,因此直接选择了COLLSCAN。当文档量增长到一定规模(比如上万条),优化器才会考虑使用索引,但全表扫描场景下索引的优势始终有限。
内容的提问来源于Stack Exchange,提问作者Jose
相关产品推荐
相关产品推荐

