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 表达意图。

单字段索引选项决策与覆盖范围 users 集合文档 email:"a@x.com" phone:"138…" email:"b@x.com" phone:"139…" email:"c@x.com" 无 phone 字段 email:"d@x.com" 无 phone 字段 email:"e@x.com" phone:"137…" 建索引 普通唯一索引 { email: 1 }, unique: true 索引全部 5 条文档 缺失字段按 null 参与唯一性 → 两条无 phone 会冲突 稀疏唯一索引 { phone: 1 }, unique, sparse 仅索引含 phone 的 3 条 无 phone 文档不入索引 $exists:false 无法命中 部分唯一索引 partialFilterExpression 按业务条件筛选文档 查询须蕴含过滤条件 比稀疏更灵活、意图更清晰 选择要点 方向 1/-1 单字段等价 方向与常用排序一致 显式命名便于运维 唯一约束下沉到存储层 缺失字段=null 会冲突 稀疏省空间但限制查询 部分索引按条件裁剪 过滤条件须被查询蕴含 建后必查 getIndexes() 用 explain 验证命中

创建后的验证: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,常见原因是:查询条件不蕴含部分索引的过滤条件、字段类型不匹配、或存在更优的复合索引被优化器选中。

单字段索引的“方向”在单字段场景下不影响命中,但会影响可读性与后续扩展;真正决定索引是否被使用、是否约束写入的,是 unique、sparse、partialFilterExpression 这三个选项。

设计检查清单

检查项判断依据常见问题
字段是否高频出现在查询条件慢查询日志、explain 中 COLLSCAN为低频字段建索引,浪费写入
方向是否与排序一致sort 字段与索引方向单字段方向随意,扩展复合时需重建
是否需要唯一约束业务是否要求字段不重复应用层查重,并发下仍可能重复
缺失字段是否允许重复是否存在多条无该字段的文档普通唯一索引触发 E11000
是否可用部分索引缩小范围查询是否总带某个状态条件过滤条件未被查询蕴含,索引失效
索引名是否可读getIndexes 输出是否易辨认自动名过长,删除时难以引用

把以上决策落到一次实际创建中,可以按“先确认查询模式 → 选择选项 → 创建 → getIndexes 核对 → explain 验证”的顺序推进。单字段索引虽然简单,但它是复合索引、覆盖索引和索引选择策略的起点;把方向、命名、唯一性、稀疏与部分过滤这几件事做对,后续的性能优化才有稳固基础。