MongoDB中的单字段索引,本质上是在一个字段值上独立建立的有序映射结构。数据库写入集合时,会同时维护这份索引;查询时如果条件刚好命中这个字段,执行器就可以沿着索引树快速定位目标数据块,避免把整个集合扫描一遍。创建命令本身很简洁,但要让索引真正稳定发挥作用,还需要理解它背后的维护成本、执行计划判断以及实际使用中容易忽略的边界条件。

一、单字段索引的创建语法与底层存储
在MongoDB中创建一个单字段索引,使用createIndex方法,基本格式为db.collection.createIndex({字段名: 1})。数字1代表按该字段升序建立索引,-1代表降序建索引。对于单字段索引来说,方向主要影响排序扫描行为,等值匹配时升降序差别不大。下面是一个在用户集合的age字段上建立升序索引的示例:
db.users.createIndex({ age: 1 })
这条命令执行成功后,MongoDB会在后台为age字段生成独立的B树结构。每个索引节点保存的是索引键值和指向实际文档的存储位置,键值按照从小到大的顺序排列。因此当查询条件为age: 30时,执行器可以从根节点开始逐层比较,快速缩小范围;当条件为age > 25时,索引也能直接定位到第一个大于25的键,然后顺序扫描后续区间。除了基本的升序降序,createIndex还支持unique、sparse、expireAfterSeconds等参数。例如邮箱字段通常需要唯一约束,可以这样创建:
db.users.createIndex({ email: 1 }, { unique: true, name: "idx_email_unique" })
从存储角度看,索引文件与集合数据是分离的。WiredTiger引擎会压缩索引页,但索引依然会占用额外磁盘空间。字段越长、索引条目越多,空间占用越大。创建索引时如果不指定名称,MongoDB会自动生成类似age_1的名称。给索引起一个语义明确的名称,在后续查看和删除时会更直观。查看当前集合的所有索引可以使用getIndexes:
db.users.getIndexes()
返回结果中可以看到每个索引的名称、键定义以及属性。了解这些基础参数后,下一步关键是判断索引是否真正被查询使用。
二、执行计划分析与索引命中判断
创建索引并不等于查询一定会使用它。MongoDB的查询优化器会根据集合统计信息、索引选择性等因素选择执行计划。要确认单字段索引是否生效,可以使用explain方法。重点是观察winningPlan中的stage字段,如果显示IXSCAN说明走的是索引扫描,如果显示COLLSCAN则说明执行了全集合扫描。下面这个查询会返回带执行统计信息的计划:
db.users.find({ age: 30 }).explain("executionStats")
如果看到stage为IXSCAN,并且totalDocsExamined与nReturned非常接近,说明索引过滤效果好。反之,如果扫描了大量索引键却只返回少量文档,则说明这个字段的选择性不够理想。对于范围查询,执行计划同样可以通过索引完成数据定位。比如查找年龄大于25并按年龄升序返回,可以这样验证:
db.users.find({ age: { $gt: 25 } }).sort({ age: 1 }).explain("executionStats")
在这个语句中,索引不仅可以用来过滤age大于25的条件,还能直接满足按age升序排序的要求,避免额外排序操作。如果执行计划中的sort阶段没有出现,说明索引提供了有序结果。但要注意,单字段索引只能解决单个字段的排序问题。如果查询是sort({ age: 1, name: 1 }),那么仅靠age索引通常无法同时满足两个字段的排序,优化器可能会选择全集合扫描或额外内存排序。此时就需要评估复合索引是否更合适,而不是继续在单字段索引上纠结。
三、读写性能与字段选择性
索引是一把双刃剑。读操作受益的同时,写操作必须付出维护成本。每次插入文档、删除文档,或者更新被索引字段的值,MongoDB都要同步更新对应的B树节点。字段索引越多,写放大越明显。以一个简单的写入测试为例,如果对100万级文档的集合插入新数据,同时存在五个单字段索引,插入耗时可能比无索引状态增加百分之二十到五十,具体取决于索引键长度和存储引擎页分裂频率。因此并不是字段越多越要建单字段索引。
字段选择性是决定是否建单字段索引的重要依据。选择性可以理解为不同取值数量与总文档数的比例。像userId、orderNo、email这类高基数字段,索引能快速缩小范围;而性别、布尔状态等低基数字段,由于重复值过多,即使走了索引也可能需要扫描大量结果,优化器有时会倾向于直接全集合扫描。通过explain观察totalDocsExamined可以判断选择性是否达标。如果扫描的文档数接近集合总量,那么这个单字段索引基本没有价值,可以考虑删除。
某些场景下单字段索引还承担着业务约束的功能。例如给email添加unique后,插入重复邮箱会直接报错;给日志时间字段加上expireAfterSeconds,可以利用TTL索引自动清理过期文档。创建TTL索引的示例是:
db.logs.createIndex({ createTime: 1 }, { expireAfterSeconds: 3600 })
这条命令表示文档在createTime时间点之后3600秒自动过期删除。需要注意的是,TTL监控线程每隔60秒运行一次,所以过期时间不是毫秒级精确。对于可能缺失的字段,sparse索引可以只对包含该字段的文档建索引,既节省空间又能避免大量null值进入索引。
四、单字段索引的常见误区与最佳实践
第一个常见误区是数据类型不一致导致索引失效。比如集合中的age字段以字符串形式存储"25",而查询传入数字25,这时两者的BSON类型不同,MongoDB不会把它们看作同一个键值,索引自然无法命中。因此在录入数据时应尽量保持字段类型一致,查询时也使用相同类型。另一个容易忽略的问题是函数操作。如果查询写成db.users.find({ email: { $regex: /@gmail\.com$/i } }),由于正则不区分大小写且不是前缀匹配,通常无法利用索引;改成前缀正则/^zhang/则有机会走索引范围扫描。
第二个误区是盲目创建重复或冗余索引。例如已经存在{ status: 1, createTime: 1 }复合索引,再单独创建{ status: 1 },很多情况下属于冗余,因为复合索引的前缀已经覆盖了status单字段的等值查询和排序需求。不过也有例外,如果单字段索引需要唯一性约束,或者查询只涉及status且集合规模很大,需要实测判断。定期使用db.collection.getIndexes()检查索引清单,删除不再使用的索引,可以降低写入压力。删除命令为db.collection.dropIndex("索引名称")。
第三个误区是认为索引会永远被使用。优化器会根据统计信息动态选择计划,数据分布变化后,原本走索引的查询可能退化。生产环境排查慢查询时,除了看执行计划,还可以用hint强制指定索引来对比效果。例如db.users.find({ age: 30 }).hint("age_1"),如果强制走索引后性能明显提升,则需要检查优化器为何选错,比如统计信息过期或选择性判断不准确。总结来说,单字段索引适合高选择性、类型稳定、出现在查询条件或排序中的字段;创建后要用explain验证命中情况,并定期评估写入成本和空间占用。这样既能享受索引带来的查询加速,也能避免不必要的维护负担。