在MongoDB中,索引(Index)是提升查询性能的关键机制,类似于关系型数据库中的索引。它通过创建数据的有序结构,帮助数据库快速定位和访问数据,避免全集合扫描(full collection scan),从而显著提高查询效率。
一、索引的基本概念
- 作用:加速查询操作,减少数据扫描范围。
- 代价:索引会占用额外存储空间,且在插入、更新、删除操作时需要维护索引,可能略微降低写性能。
- 默认索引 :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
集合的title
和content
创建文本索引
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.stage
为IXSCAN
,表示使用了索引。 - 若为
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" } // 自定义索引名称(便于管理)
)
索引注释:
-
字段顺序 :
userId
在前,createTime
在后- 遵循"最左前缀原则":查询条件中必须包含
userId
才能触发索引,而createTime
作为范围查询条件放在后面。 - 若颠倒顺序(
createTime
在前),则无法通过userId
快速过滤数据,索引失效。
- 遵循"最左前缀原则":查询条件中必须包含
-
排序方向 :
createTime: -1
(降序)- 与查询中的
sort({ createTime: -1 })
一致,避免数据库额外排序(索引本身已按时间倒序排列,可直接返回结果)。
- 与查询中的
-
业务价值:
- 原本需要扫描全表匹配
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" }
)
索引注释:
-
字段顺序逻辑:
- 第一字段
publisherId
:过滤"关注人"的动态,是查询的核心条件,基数(不同值的数量)适中,适合作为前缀。 - 第二字段
type
:进一步过滤"图文"类型,基数较小(通常只有3-5种类型),放在中间可减少索引扫描范围。 - 第三字段
publishTime: -1
:匹配排序需求,避免额外排序开销。
- 第一字段
-
为何不将
type
放在最后?- 若索引为
{ publisherId: 1, publishTime: -1, type: 1 }
,查询时虽能通过publisherId
过滤,但type
条件需要在时间范围内再次筛选,效率低于先过滤type
。
- 若索引为
-
业务效果:
- 支持每秒数万次的动态流查询,响应时间控制在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" }
)
索引注释:
-
字段优先级:
waybillNo
(物流单号):基数极高(几乎唯一),作为首字段可瞬间定位到某单的所有轨迹,过滤效率最高。- 后续字段
city
→status
:按查询条件的过滤粒度依次排列,逐步缩小范围。 updateTime: 1
:匹配正序排序需求,索引直接有序,无需额外处理。
-
索引覆盖性:
- 若查询仅需返回
updateTime
和location
,可扩展为"覆盖索引":
db.tracks.createIndex({ waybillNo: 1, city: 1, status: 1, updateTime: 1 }, { name: "...", include: { location: 1 } })
- 覆盖索引可直接从索引返回结果,无需访问文档,性能再提升30%+。
- 若查询仅需返回
-
业务价值:
- 支持百万级物流单号的实时轨迹查询,解决了原全表扫描时"查询超时"问题。
复合索引设计的核心原则总结
- 最左前缀匹配:查询条件必须包含索引的前N个字段,否则索引失效。
- 字段顺序依据:基数高(值唯一或多样)的字段放前面,过滤性强;排序字段放最后,且排序方向需与索引一致。
- 避免过度设计:字段数量不宜过多(建议≤5个),否则索引维护成本高,写性能下降。
- 结合查询频率:只为高频查询创建复合索引,低频查询可接受稍慢的响应。
- 每个集合最多支持
64个索引
。- 索引键的总长度不能超过1024字节。
- 某些操作(如
$where
、$expr
)可能无法使用索引。
为什么 w h e r e 和 where和 where和expr难以使用索引
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:创建合适的索引
根据查询条件(userId
、status
、createTime
)和排序(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 性能优化的"显微镜",其使用流程可概括为:
- 对目标查询执行
explain("executionStats")
获取执行计划。 - 检查
stage
是否为IXSCAN
,以及扫描文档数、耗时等指标。 - 根据分析结果调整索引设计(如新增、删除或修改索引)。
- 再次执行
explain()
验证优化效果,循环迭代直至性能达标。