4. 单字段索引设计与创建
单字段索引是 MongoDB 索引体系中最基础、也最容易被低估的一类索引。它只针对一个字段建立 B-Tree 结构,但字段的方向、命名、唯一性约束、稀疏与部分过滤这些设计决策,会直接影响查询能否命中索引、写入是否被拒绝、以及索引体积是否被无意义文档撑大。本节围绕 db.collection.createIndex() 展开,把“建什么、怎么建、建完怎么验证”讲清楚。
createIndex() 的基本形态与方向语义
创建单字段索引的标准写法是:
db.users.createIndex(
{ age: 1 }, // 索引键:字段名 + 方向
{ name: "idx_age_1" } // 可选:索引名
)
第一个参数是索引键文档,1 表示升序,-1 表示降序。对单字段索引而言,方向在查询匹配阶段几乎不影响能否使用索引:因为 B-Tree 可以正向或反向遍历,{ age: 1 } 与 { age: -1 } 都能支持 age 的等值、范围与排序查询。方向真正产生差异的场景是复合索引中的排序方向组合,以及单字段索引参与 sort() 时与查询方向的一致性判断。因此,单字段索引的方向选择原则是:与最常用排序方向保持一致,便于阅读和后续扩展为复合索引;若没有明确排序需求,统一用 1 即可。
需要特别注意的是,MongoDB 默认会为 _id 字段创建唯一索引,这是集合创建时自动完成的,不需要也不应重复创建。
索引命名:可读性与运维价值
如果不指定 name,MongoDB 会按“字段名_方向”自动生成,例如 age_1、createdAt_-1。自动命名在单字段场景下通常够用,但在以下情况建议显式命名:
- 字段名较长或包含嵌套路径,自动名可读性差,例如
profile.address.city_1; - 同一字段需要保留多个不同选项的索引(如普通索引与部分索引并存),需要靠名字区分;
- 团队规范要求索引名带业务前缀,便于在
db.collection.getIndexes()输出中快速定位。
命名建议采用“idx_字段_方向”或“idx_业务_字段”形式,长度控制在 128 字符以内。索引名一旦创建,后续删除索引必须使用该名字,因此命名应稳定、可预期。
唯一索引:把业务约束下沉到存储层
唯一索引通过 unique: true 声明:
db.users.createIndex(
{ email: 1 },
{ unique: true, name: "idx_email_unique" }
)
它保证索引键值在集合内不重复。若插入或更新产生重复键,写入会失败并返回 E11000 duplicate key error。这是把“邮箱不可重复”这类业务规则交给数据库兜底的有效手段,比应用层先查后写更可靠,能避免并发下的竞态。
唯一索引有一个容易踩的细节:缺失字段会被当作 null 参与唯一性判断。也就是说,如果集合中已有一条文档没有 email 字段,再插入一条同样缺失 email 的文档,会触发重复键错误。若业务允许“未填写邮箱”的多条记录共存,应改用稀疏唯一索引或部分唯一索引。
稀疏索引与部分索引:控制索引覆盖范围
稀疏索引通过 sparse: true 声明,它不索引缺失该字段的文档。例如:
db.users.createIndex(
{ phone: 1 },
{ unique: true, sparse: true, name: "idx_phone_sparse_unique" }
)
这样,没有 phone 字段的文档不会进入索引,既避免了 null 重复冲突,也减小了索引体积。但稀疏索引的代价是:对“字段不存在”的查询无法使用该索引,例如 { phone: { $exists: false } } 会退化为集合扫描。
部分索引(Partial Index)用 partialFilterExpression 声明,只索引满足过滤条件的文档,比稀疏索引更灵活:
db.orders.createIndex(
{ orderNo: 1 },
{
unique: true,
name: "idx_orderNo_active",
partialFilterExpression: { status: { $in: ["paid", "shipped"] } }
}
)
它只对 status 为 paid 或 shipped 的订单保证 orderNo 唯一,草稿或已取消订单不参与约束。部分索引要生效,查询必须包含或蕴含该过滤条件,否则优化器不会选择它。稀疏索引可视为部分索引在“字段存在”这一条件上的特例,新项目更推荐用 partialFilterExpression 表达意图。
创建后的验证:getIndexes 与 explain
索引创建后,第一步是确认它真实存在且选项正确:
db.users.getIndexes()
输出中应能看到 key、name、unique、sparse、partialFilterExpression 等字段。第二步是验证查询是否命中。对目标查询执行:
db.users.find({ email: "a@x.com" }).explain("executionStats")
重点观察 winningPlan 中是否出现 IXSCAN 而非 COLLSCAN,以及 executionStats.totalKeysExamined 与 totalDocsExamined 是否接近返回文档数。若仍是 COLLSCAN,常见原因是:查询条件不蕴含部分索引的过滤条件、字段类型不匹配、或存在更优的复合索引被优化器选中。
设计检查清单
| 检查项 | 判断依据 | 常见问题 |
|---|---|---|
| 字段是否高频出现在查询条件 | 慢查询日志、explain 中 COLLSCAN | 为低频字段建索引,浪费写入 |
| 方向是否与排序一致 | sort 字段与索引方向 | 单字段方向随意,扩展复合时需重建 |
| 是否需要唯一约束 | 业务是否要求字段不重复 | 应用层查重,并发下仍可能重复 |
| 缺失字段是否允许重复 | 是否存在多条无该字段的文档 | 普通唯一索引触发 E11000 |
| 是否可用部分索引缩小范围 | 查询是否总带某个状态条件 | 过滤条件未被查询蕴含,索引失效 |
| 索引名是否可读 | getIndexes 输出是否易辨认 | 自动名过长,删除时难以引用 |
把以上决策落到一次实际创建中,可以按“先确认查询模式 → 选择选项 → 创建 → getIndexes 核对 → explain 验证”的顺序推进。单字段索引虽然简单,但它是复合索引、覆盖索引和索引选择策略的起点;把方向、命名、唯一性、稀疏与部分过滤这几件事做对,后续的性能优化才有稳固基础。