You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

MongoDB:$unwind能否利用索引?聚合查询覆盖索引问题咨询

关于MongoDB含$unwind聚合查询的索引利用问题

我不清楚怎么在带$unwind的聚合查询里用上索引。本来以为保留内嵌数组的文档结构更高效,但如果实在做不到,只能考虑拆成两个带索引的集合,用$lookup来实现。我想搞清楚两个问题:

  1. MongoDB能不能对包含内嵌数组的文档用索引实现覆盖查询?
  2. 是不是必须拆分两个集合才能让下面的聚合查询用上覆盖索引?

集合结构

{
 _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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 06:16:08