【MongoDB】索引分类,使用案例讲解,explain三种模式讲解,及性能排查案例

在MongoDB中,索引(Index)是提升查询性能的关键机制,类似于关系型数据库中的索引。它通过创建数据的有序结构,帮助数据库快速定位和访问数据,避免全集合扫描(full collection scan),从而显著提高查询效率。

一、索引的基本概念

  1. 作用:加速查询操作,减少数据扫描范围。
  2. 代价:索引会占用额外存储空间,且在插入、更新、删除操作时需要维护索引,可能略微降低写性能。
  3. 默认索引 :MongoDB在创建集合时,会自动为_id字段创建唯一索引(_id_),确保文档唯一性。

二、索引的类型

MongoDB支持多种类型的索引,适用于不同的查询场景:

1. 单字段索引(Single Field Index)

对文档中的单个字段创建索引,最常用的索引类型。

创建语法

javascript 复制代码
db.collection.createIndex({ field: 1 })  // 1表示升序,-1表示降序

示例 :为users集合的name字段创建升序索引

javascript 复制代码
db.users.createIndex({ name: 1 })

适用场景:针对单个字段的查询、排序操作。

2. 复合索引(Compound Index)

对多个字段联合创建索引,索引的顺序会影响查询效率。

创建语法

javascript 复制代码
db.collection.createIndex({ field1: 1, field2: -1 })

示例 :为orders集合的userId(升序)和createTime(降序)创建复合索引

javascript 复制代码
db.orders.createIndex({ userId: 1, createTime: -1 })

注意

  • 遵循"最左前缀原则":查询条件中必须包含索引的第一个字段,才能有效使用复合索引。
  • 例如,上述索引对{userId: ...}{userId: ..., createTime: ...}的查询有效,但对{createTime: ...}的查询无效。
3. 多键索引(Multikey Index)

当字段值为数组时,MongoDB会自动为数组中的每个元素创建索引,无需显式指定。

示例products集合的tags字段是数组

javascript 复制代码
// 文档结构
{ _id: 1, name: "手机", tags: ["电子", "通讯", "智能"] }

// 创建索引(自动成为多键索引)
db.products.createIndex({ tags: 1 })

适用场景 :查询数组中是否包含某个元素(如db.products.find({ tags: "电子" }))。

4. 地理空间索引(Geospatial Index)

用于存储和查询地理空间数据(如经纬度)。

类型

  • 2dsphere:适用于球形表面的地理坐标(如GPS数据)。
  • 2d:适用于平面上的坐标(较少使用)。

示例 :为locations集合的coordinates字段创建地理空间索引

javascript 复制代码
db.locations.createIndex({ coordinates: "2dsphere" })

适用场景 :查询附近的地点(如$near操作符)。

5. 文本索引(Text Index)

用于全文搜索,支持对字符串字段进行分词和匹配。

创建语法

javascript 复制代码
// 单字段文本索引
db.collection.createIndex({ field: "text" })

// 多字段文本索引(合并多个字段的内容)
db.collection.createIndex({ field1: "text", field2: "text" })

示例 :为articles集合的titlecontent创建文本索引

javascript 复制代码
db.articles.createIndex({ title: "text", content: "text" })

查询示例:搜索包含"MongoDB"或"数据库"的文档

javascript 复制代码
db.articles.find({ $text: { $search: "MongoDB 数据库" } })
6. 哈希索引(Hashed Index)

对字段值进行哈希运算后创建索引,适用于基于哈希值的分片操作(Sharding)。

创建语法

javascript 复制代码
db.collection.createIndex({ field: "hashed" })

注意:哈希索引不支持范围查询,仅支持精确匹配。

7. 唯一索引(Unique Index)

确保索引字段的值唯一,类似于_id索引的特性。

创建语法

javascript 复制代码
db.collection.createIndex({ field: 1 }, { unique: true })

示例 :确保users集合的email字段唯一

javascript 复制代码
db.users.createIndex({ email: 1 }, { unique: true })

注意 :如果插入重复值,会抛出DuplicateKey错误。

三、索引的操作

1. 创建索引
javascript 复制代码
// 基础语法
db.collection.createIndex(
  { field: order },  // 索引字段及排序方向
  { options }        // 可选参数(如unique、name等)
)

// 示例:创建名为"age_idx"的唯一索引
db.users.createIndex({ age: 1 }, { unique: true, name: "age_idx" })
2. 查看索引
javascript 复制代码
// 查看集合的所有索引
db.collection.getIndexes()

// 示例:查看users集合的索引
db.users.getIndexes()
3. 删除索引
javascript 复制代码
// 按索引名称删除
db.collection.dropIndex("index_name")

// 按索引字段删除
db.collection.dropIndex({ field: order })

// 示例:删除name字段的索引
db.users.dropIndex({ name: 1 })

// 删除所有索引(保留默认的_id索引)
db.collection.dropIndexes()

四、索引的使用与优化

1. 分析查询是否使用索引

使用explain()方法查看查询计划,判断是否命中索引:

javascript 复制代码
db.collection.find(query).explain("executionStats")
  • executionStats.executionStages.stageIXSCAN,表示使用了索引。
  • 若为COLLSCAN,表示全集合扫描,需优化索引。
2. 索引优化建议
  • 只为常用查询创建索引:避免创建过多索引,影响写性能。
  • 优先使用复合索引覆盖多字段查询:减少索引数量。
  • 遵循最左前缀原则:复合索引的字段顺序需与查询条件匹配。
  • 避免索引过度冗余 :例如,已创建{a:1, b:1},无需再创建{a:1}
  • 定期清理无用索引 :通过getIndexes()检查并删除不常用的索引。

五、索引的限制

  • 每个集合最多支持64个索引
  • 索引键的总长度不能超过1024字节。
  • 某些操作(如$where$expr)可能无法使用索引。

索引使用案例

在企业级MongoDB应用中,复合索引(多列索引)是优化复杂查询的常用手段。以下结合真实业务场景,提供几个典型企业案例,并详细说明索引设计思路和注释。

案例1:电商平台订单查询系统

业务场景

电商平台需要频繁查询"某个用户在特定时间范围内的订单",并按订单创建时间倒序展示。
文档结构orders集合):

javascript 复制代码
{
  _id: ObjectId("..."),
  userId: "u12345",       // 用户ID
  orderNo: "ORD20230801", // 订单编号
  createTime: ISODate("2023-08-01T10:30:00Z"), // 创建时间
  status: "PAID",         // 订单状态
  amount: 199.99          // 订单金额
}

查询需求
db.orders.find({ userId: "u12345", createTime: { $gte: ISODate("2023-08-01"), $lte: ISODate("2023-08-31") } }).sort({ createTime: -1 })

复合索引设计

javascript 复制代码
// 创建复合索引:{ userId: 1, createTime: -1 }
db.orders.createIndex(
  { userId: 1, createTime: -1 },  // 索引字段及排序方向
  { name: "idx_userId_createTime" }  // 自定义索引名称(便于管理)
)

索引注释

  1. 字段顺序userId在前,createTime在后

    • 遵循"最左前缀原则":查询条件中必须包含userId才能触发索引,而createTime作为范围查询条件放在后面。
    • 若颠倒顺序(createTime在前),则无法通过userId快速过滤数据,索引失效。
  2. 排序方向createTime: -1(降序)

    • 与查询中的sort({ createTime: -1 })一致,避免数据库额外排序(索引本身已按时间倒序排列,可直接返回结果)。
  3. 业务价值

    • 原本需要扫描全表匹配userId,再过滤时间范围,索引将查询效率提升100倍以上(数据量100万级时)。

案例2:社交媒体动态流系统

业务场景

社交平台需要查询"某个用户关注的人发布的动态,且动态类型为'图文',并按发布时间倒序展示"。
文档结构posts集合):

javascript 复制代码
{
  _id: ObjectId("..."),
  authorId: "user890",    // 作者ID(被关注人)
  publisherId: "user123", // 发布者ID(关注人)
  type: "IMAGE_TEXT",     // 动态类型:图文/视频/纯文本
  publishTime: ISODate("2023-08-01T15:20:00Z"), // 发布时间
  content: "今天天气真好..."
}

查询需求
db.posts.find({ publisherId: "user123", type: "IMAGE_TEXT" }).sort({ publishTime: -1 })

复合索引设计

javascript 复制代码
// 创建复合索引:{ publisherId: 1, type: 1, publishTime: -1 }
db.posts.createIndex(
  { publisherId: 1, type: 1, publishTime: -1 },
  { name: "idx_publisher_type_publishTime" }
)

索引注释

  1. 字段顺序逻辑

    • 第一字段publisherId:过滤"关注人"的动态,是查询的核心条件,基数(不同值的数量)适中,适合作为前缀。
    • 第二字段type:进一步过滤"图文"类型,基数较小(通常只有3-5种类型),放在中间可减少索引扫描范围。
    • 第三字段publishTime: -1:匹配排序需求,避免额外排序开销。
  2. 为何不将type放在最后?

    • 若索引为{ publisherId: 1, publishTime: -1, type: 1 },查询时虽能通过publisherId过滤,但type条件需要在时间范围内再次筛选,效率低于先过滤type
  3. 业务效果

    • 支持每秒数万次的动态流查询,响应时间控制在10ms以内,满足社交平台高并发需求。

案例3:物流轨迹查询系统

业务场景

物流平台需要查询"某个物流单号在特定城市、特定状态下的轨迹记录",并按时间正序展示。
文档结构tracks集合):

javascript 复制代码
{
  _id: ObjectId("..."),
  waybillNo: "WB78901234", // 物流单号
  city: "SHANGHAI",        // 城市
  status: "IN_TRANSIT",    // 状态:运输中/已签收/异常
  updateTime: ISODate("2023-08-01T08:10:00Z"), // 更新时间
  location: "XX分拣中心"   // 位置详情
}

查询需求
db.tracks.find({ waybillNo: "WB78901234", city: "SHANGHAI", status: "IN_TRANSIT" }).sort({ updateTime: 1 })

复合索引设计

javascript 复制代码
// 创建复合索引:{ waybillNo: 1, city: 1, status: 1, updateTime: 1 }
db.tracks.createIndex(
  { waybillNo: 1, city: 1, status: 1, updateTime: 1 },
  { name: "idx_waybill_city_status_time" }
)

索引注释

  1. 字段优先级

    • waybillNo(物流单号):基数极高(几乎唯一),作为首字段可瞬间定位到某单的所有轨迹,过滤效率最高。
    • 后续字段citystatus:按查询条件的过滤粒度依次排列,逐步缩小范围。
    • updateTime: 1:匹配正序排序需求,索引直接有序,无需额外处理。
  2. 索引覆盖性

    • 若查询仅需返回updateTimelocation,可扩展为"覆盖索引":
      db.tracks.createIndex({ waybillNo: 1, city: 1, status: 1, updateTime: 1 }, { name: "...", include: { location: 1 } })
    • 覆盖索引可直接从索引返回结果,无需访问文档,性能再提升30%+。
  3. 业务价值

    • 支持百万级物流单号的实时轨迹查询,解决了原全表扫描时"查询超时"问题。

复合索引设计的核心原则总结

  1. 最左前缀匹配:查询条件必须包含索引的前N个字段,否则索引失效。
  2. 字段顺序依据:基数高(值唯一或多样)的字段放前面,过滤性强;排序字段放最后,且排序方向需与索引一致。
  3. 避免过度设计:字段数量不宜过多(建议≤5个),否则索引维护成本高,写性能下降。
  4. 结合查询频率:只为高频查询创建复合索引,低频查询可接受稍慢的响应。

MongoDB 的 explain() 方法是分析查询性能、优化索引设计的核心工具。它能展示查询的执行计划,帮助我们判断是否使用了索引、是否存在全表扫描等性能问题。

一、explain() 基本用法

explain() 需附加在查询操作后(如 find()aggregate() 等),用于生成执行计划。其语法如下:

javascript 复制代码
db.collection.find(query).sort(sort).limit(limit).explain(verbosity)
  • verbosity(可选) :指定执行计划的详细程度,常用取值:
    • queryPlanner(默认):返回查询计划的核心信息(推荐日常使用)。
    • executionStats:在查询计划基础上,增加实际执行的统计数据(如扫描文档数、耗时)。
    • allPlansExecution:展示所有可能的执行计划及各自的执行统计(用于复杂查询分析)。

二、核心参数解析(以 executionStats 为例)

执行计划中关键字段的含义:

字段路径 含义 优化目标
executionStats.executionStages.stage 查询执行的阶段 应出现 IXSCAN(索引扫描),避免 COLLSCAN(全表扫描)
executionStats.executedWork 执行的工作量(越低越好) 数值越小,性能越好
executionStats.nReturned 返回的文档数 totalDocsExamined 越接近越好
executionStats.totalDocsExamined 扫描的文档总数 越小越好(理想值 = nReturned
executionStats.totalKeysExamined 扫描的索引键总数 越小越好(接近 nReturned 最佳)

三、全流程案例:从查询到优化

假设场景:电商平台的 orders 集合(100万条数据),文档结构如下:

javascript 复制代码
{
  _id: ObjectId("..."),
  userId: "u123",         // 用户ID
  createTime: ISODate("2023-08-01T10:00:00Z"), // 创建时间
  status: "PAID",         // 订单状态
  amount: 299.99          // 金额
}
步骤1:执行原始查询并生成执行计划

需求:查询用户 u123 在 2023年8月 的已支付订单,并按创建时间倒序排列。

javascript 复制代码
// 原始查询(未创建索引)
db.orders.find({
  userId: "u123",
  status: "PAID",
  createTime: { 
    $gte: ISODate("2023-08-01"), 
    $lte: ISODate("2023-08-31") 
  }
}).sort({ createTime: -1 })
// 附加explain,获取执行统计
.explain("executionStats")
步骤2:分析执行计划(发现问题)

执行计划关键部分(简化):

javascript 复制代码
{
  "executionStats": {
    "executionStages": {
      "stage": "COLLSCAN", // 全表扫描!性能极差
      "nReturned": 15,     // 符合条件的文档数
      "totalDocsExamined": 1000000, // 扫描了所有100万条文档
      "executionTimeMillis": 850    // 耗时850ms(很慢)
    }
  },
  "queryPlanner": {
    "winningPlan": {
      "stage": "SORT",     // 需要额外排序
      "sortPattern": { "createTime": -1 },
      "inputStage": { "stage": "COLLSCAN" }
    }
  }
}

问题分析

  • stage: "COLLSCAN":未使用索引,全表扫描导致扫描文档数极高。
  • stage: "SORT":查询需要在内存中排序,增加耗时。
步骤3:创建合适的索引

根据查询条件(userIdstatuscreateTime)和排序(createTime: -1),设计复合索引:

javascript 复制代码
// 创建复合索引:优先过滤字段在前,排序字段在后
db.orders.createIndex(
  { userId: 1, status: 1, createTime: -1 }, 
  { name: "idx_user_status_time" }
)

索引设计逻辑

  • userId 放在最前:查询的核心过滤条件,基数高(区分度强)。
  • status 次之:进一步缩小范围(状态值有限,基数低)。
  • createTime: -1:与排序方向一致,避免额外排序。
步骤4:再次执行查询并验证优化效果
javascript 复制代码
// 相同查询,再次执行explain
db.orders.find({
  userId: "u123",
  status: "PAID",
  createTime: { $gte: ISODate("2023-08-01"), $lte: ISODate("2023-08-31") }
}).sort({ createTime: -1 })
.explain("executionStats")
步骤5:分析优化后的执行计划(确认改进)

优化后的关键结果:

javascript 复制代码
{
  "executionStats": {
    "executionStages": {
      "stage": "IXSCAN", // 索引扫描!成功使用索引
      "indexName": "idx_user_status_time", // 使用了我们创建的索引
      "nReturned": 15,
      "totalDocsExamined": 15,    // 扫描文档数 = 返回数(完美)
      "totalKeysExamined": 15,    // 扫描索引键数 = 返回数
      "executionTimeMillis": 5    // 耗时降至5ms(提升170倍)
    }
  },
  "queryPlanner": {
    "winningPlan": {
      "stage": "FETCH",  // 通过索引找到文档位置后,直接获取文档
      "inputStage": {
        "stage": "IXSCAN", // 索引扫描作为前置阶段
        "indexBounds": {   // 索引扫描的范围(精准匹配条件)
          "userId": ["[\"u123\", \"u123\"]"],
          "status": ["[\"PAID\", \"PAID\"]"],
          "createTime": ["[new Date(1690819200000), new Date(1693411199999)]"]
        }
      }
    }
  }
}

优化效果

  • 从全表扫描(COLLSCAN)变为索引扫描(IXSCAN)。
  • 扫描文档数从100万降至15,耗时从850ms降至5ms。
  • 排序操作消失(索引本身已按createTime: -1排序,无需额外处理)。

四、常见问题与解决方案

问题现象 原因 解决方案
stage: "COLLSCAN" 未创建合适索引,或查询条件未匹配索引前缀 按"最左前缀原则"创建复合索引
totalDocsExamined >> nReturned 索引过滤性差,扫描了大量无关文档 调整索引字段顺序,将高基数字段放前面
存在 SORT 阶段且耗时高 排序字段未包含在索引中,或排序方向与索引不一致 在索引中包含排序字段,并保持方向一致
executionTimeMillis 过高 索引设计不合理,或数据量过大 优化索引,或增加查询过滤条件缩小范围

五、explain() 在聚合管道中的使用

对于 aggregate() 聚合查询,explain() 同样适用,示例:

javascript 复制代码
db.orders.aggregate([
  { $match: { userId: "u123", status: "PAID" } },
  { $group: { _id: "$createTime", total: { $sum: "$amount" } } }
]).explain("executionStats")

分析重点 :关注 $match 阶段是否使用索引(IXSCAN),避免在聚合早期阶段处理大量数据。

总结

explain() 是 MongoDB 性能优化的"显微镜",其使用流程可概括为:

  1. 对目标查询执行 explain("executionStats") 获取执行计划。
  2. 检查 stage 是否为 IXSCAN,以及扫描文档数、耗时等指标。
  3. 根据分析结果调整索引设计(如新增、删除或修改索引)。
  4. 再次执行 explain() 验证优化效果,循环迭代直至性能达标。

相关推荐
哥哥还在IT中2 分钟前
脚本统计MongoDB集合结构信息
数据库·mongodb
哥哥还在IT中1 小时前
MVCC 实现之探析
数据库·mysql·tidb
程序员瓜叔1 小时前
window10本地运行datax与datax-web
数据库·datax
斯普信专业组2 小时前
Mongodb常用命令简介
数据库·mongodb
-风中叮铃-2 小时前
【MongoDB学习笔记1】MongoDB的常用命令介绍-数据库操作、集合操作、文档操作、文档分页查询、高级查询
数据库·学习·mongodb
老华带你飞2 小时前
生产管理ERP系统|物联及生产管理ERP系统|基于SprinBoot+vue的制造装备物联及生产管理ERP系统设计与实现(源码+数据库+文档)
java·数据库·vue.js·论文·制造·毕设·生产管理erp系统
野蛮人6号3 小时前
MySQL笔记
数据库·笔记·mysql
山茶花开时。4 小时前
[Oracle] FLOOR()函数
数据库·oracle
蓝倾9764 小时前
唯品会以图搜图(拍立淘)API接口调用指南详解
java·大数据·前端·数据库·开放api接口
Ai财富密码4 小时前
【Python爬虫】正则表达式入门及在数据提取中的高效应用
数据库·mysql·php