MongoDB: 索引基础与原理

最后更新:2026-08-26

索引是数据库性能的关键——掌握索引原理和优化能提升 1000 倍查询速度。

1. 你将学到


2. 索引原理(B-Tree)

概念说明:索引是一种辅助数据结构,类似于书籍的目录,它让数据库无需扫描整个集合就能快速定位文档。MongoDB 使用基于 B-Tree 的索引结构(WiredTiger 引擎使用 B+Tree 变体),将字段值与文档位置建立有序映射,把查询的时间复杂度从 O(N) 降至 O(log N)。

工作原理:MongoDB 的 B+Tree 索引将字段值存储在内部节点用于路由,将实际键值和文档指针存储在叶子节点,叶子节点之间通过双向链表连接,天然支持范围查询和排序。当查询命中索引时,引擎从根节点逐层向下比较键值,最终在叶子节点定位目标文档的物理位置,避免全集合扫描(COLLSCAN)。

使用场景

100%
graph TB
    A[Root Node<br/>50-100] --> B[Internal Node 1<br/>20-50]
    A --> C[Internal Node 2<br/>20-50]
    B --> D[Leaf Node 1<br/>指向文档]
    B --> E[Leaf Node 2]
    C --> F[Leaf Node 3]
    C --> G[Leaf Node 4]
    D <--> E <--> F <--> G

    style A fill:#cce5ff
    style D fill:#d4edda
    style E fill:#d4edda
    style F fill:#d4edda
    style G fill:#d4edda

B+Tree 查询流程:等值查询从根节点逐层比较 → 到达叶子节点 → 返回文档指针;范围查询找到起始叶子节点后,沿链表顺序扫描 → 收集所有符合条件的文档。

100%
sequenceDiagram
    participant App as 应用查询
    participant WT as WiredTiger引擎
    participant IX as B+Tree索引
    participant DOC as 集合文档

    App->>WT: find({sku: 'SKU-005'})
    WT->>IX: 根节点比较 50<005<100 → 左子树
    IX-->>WT: Internal Node: 20<005<50 → 左叶
    WT->>IX: 叶子节点查找 SKU-005
    IX-->>WT: 找到 → doc_pointer=0x7F3A
    WT->>DOC: 读取 0x7F3A 位置的文档
    DOC-->>App: 返回匹配文档

    Note over WT,IX: 时间复杂度 O(log N)<br/>无需扫描全部文档
操作 全表扫描 (COLLSCAN) B+Tree 索引 (IXSCAN) 性能差异
等值查询 O(N) O(log N) 10万行: 100K vs 17
范围查询 O(N) O(log N + K) 10万行: 100K vs 17+K
排序 O(N log N) O(log N + K) 索引天然有序,免排序
插入/更新 O(1) O(log N) 索引维护额外开销

(1) WiredTiger 索引存储细节

维度 说明
索引格式 B+Tree,键值对有序存储
叶子节点 包含索引键 + RecordID(文档位置指针)
内部节点 仅包含路由键 + 子节点指针
链表连接 叶子节点双向链表,支持顺序扫描
压缩 前缀压缩(Prefix Compression)减少存储

3. createIndex 语法

概念说明createIndex() 是 MongoDB 创建索引的核心命令,它告诉引擎为指定字段构建 B+Tree 索引结构。索引创建后,查询优化器会自动选择是否使用索引——开发者无需修改查询语句。

工作原理:创建索引时,MongoDB 扫描集合中所有文档,提取索引字段值并排序,构建 B+Tree 结构写入磁盘。创建期间会持有集合的写锁(background: true 可后台构建),大集合创建索引可能耗时数分钟到数小时。

语法规则

参数 类型 说明
keys object 索引字段及方向:1 升序,-1 降序
unique boolean 是否唯一索引,默认 false
background boolean 是否后台构建(不阻塞读写),默认 false
name string 自定义索引名称,默认 field_1
partialFilterExpression object 部分索引条件(仅索引满足条件的文档)
sparse boolean 稀疏索引,跳过 null 字段
expireAfterSeconds number TTL 索引,自动过期删除(秒)
v number 索引版本,默认 v=2
JAVASCRIPT
// === 创建单字段索引 ===
db.products.createIndex({ sku: 1 });          // 升序
db.products.createIndex({ createdAt: -1 });   // 降序

// === 创建复合索引 ===
db.products.createIndex({ category: 1, price: -1 });

// === 创建唯一索引 ===
db.products.createIndex({ sku: 1 }, { unique: true });

// === 后台创建(不阻塞)===
db.products.createIndex({ tags: 1 }, { background: true });

// === 自定义索引名称 ===
db.products.createIndex({ title: 1 }, { name: 'idx_title' });

要点解析

  1. 1 和 -1 影响索引排序方向,对单字段索引无实质影响,对复合索引排序优化至关重要
  2. background: true 在 MongoDB 4.2+ 已默认后台构建,参数保留但不再需要显式指定
  3. 每个集合默认有 _id 唯一索引(不可删除),无需额外创建

▶ 示例 1: createIndex 与索引构建监控

JAVASCRIPT
// ShopHub 电商:为商品集合创建关键索引,监控构建进度
db.products.createIndex({ category: 1, price: -1, rating: -1 }, { name: 'idx_category_price_rating', background: true });

// 查看索引构建进度
db.currentOp({
  $or: [
    { op: 'command', 'command.createIndexes': { $exists: true } },
    { op: 'none', ns: /shopdb\.products/ }
  ]
});

// 查看所有索引
db.products.getIndexes();
// [
//   { v: 2, key: { _id: 1 }, name: '_id_' },
//   { v: 2, key: { category: 1, price: -1, rating: -1 }, name: 'idx_category_price_rating' }
// ]

输出:

TEXT 📖 仅展示
// 执行成功

4. explain() 执行计划

概念说明explain() 是 MongoDB 的查询分析工具,它返回查询优化器选择的执行计划,包括是否使用索引、扫描了多少文档、耗时多少等关键指标。它是索引优化的"X光机"——先 explain(),再优化。

工作原理:MongoDB 查询优化器会为每个查询生成多个候选计划(Plan),实际执行并比较性能,选择最优计划缓存。explain() 输出三个层级:

100%
graph LR
    A[查询请求] --> B[查询优化器]
    B --> C[生成候选计划]
    C --> D[Plan A: IXSCAN]
    C --> E[Plan B: COLLSCAN]
    D --> F[执行比较]
    E --> F
    F --> G[选择最优计划]
    G --> H[缓存 + 执行]

    style G fill:#d4edda
    style E fill:#f8d7da

使用场景

JAVASCRIPT
// === 查看查询执行计划 ===
db.products.find({ category: 'Electronics' }).explain('executionStats');

// === 关键指标 ===
{
  queryPlanner: {
    winningPlan: {
      stage: 'IXSCAN',           // 索引扫描(✅)
      // stage: 'COLLSCAN',      // 全集合扫描(❌)
      inputStage: {
        stage: 'IXSCAN',
        indexName: 'category_1'
      }
    }
  },
  executionStats: {
    totalDocsExamined: 250,      // 扫描文档数
    totalKeysExamined: 250,      // 扫描索引键数
    nReturned: 250,              // 返回文档数
    executionTimeMillis: 5,      // 执行时间(毫秒)
    totalQueryPlanExecutionTime: 7
  }
}

(1) 关键指标解读

指标 理想值 问题 说明
stage IXSCAN COLLSCAN(全表扫描) COLLSCAN 必须加索引优化
totalDocsExamined ≈ nReturned 远大于 nReturned 大于 10 倍说明索引精度低
totalKeysExamined ≈ nReturned 远大于 nReturned 索引扫描范围过大
executionTimeMillis < 50ms > 100ms 考虑索引覆盖或复合索引
indexName 目标索引名 id 或无 确认是否命中预期索引

explain 三种模式对比

模式 输出内容 适用场景
'queryPlanner' 仅计划,不执行 快速查看是否走索引
'executionStats' 计划 + 执行统计 性能分析(最常用)
'allPlansExecution' 所有候选计划统计 优化器选择分析

▶ 示例 2: explain() 诊断慢查询

JAVASCRIPT
// TechCorp 系统发现订单查询变慢,用 explain 诊断
// 索引前:COLLSCAN
db.orders.find({ status: 'paid', total: { $gte: 100 } }).explain('executionStats');
// stage: 'COLLSCAN', totalDocsExamined: 100000, executionTimeMillis: 450

// 创建复合索引
db.orders.createIndex({ status: 1, total: -1 });

// 索引后:IXSCAN
db.orders.find({ status: 'paid', total: { $gte: 100 } }).explain('executionStats');
// stage: 'IXSCAN', indexName: 'status_1_total_-1'
// totalDocsExamined: 5000, nReturned: 5000, executionTimeMillis: 8

// 效率评估:examined/returned = 1.0(理想值),性能提升 56 倍

输出:

TEXT 📖 仅展示
// mongoose 操作成功执行
// 数据库查询/更新结果

5. 复合索引与最左前缀

概念说明:复合索引是对多个字段建立的单一索引(如 { category: 1, price: -1, rating: 1 }),它比多个单字段索引更高效,因为一次索引查找就能同时满足多条件查询。最左前缀原则是复合索引的核心规则——索引从最左字段开始连续使用才有效。

工作原理:复合索引按字段顺序构建 B+Tree,先按第一字段排序,第一字段相同时按第二字段排序,以此类推。查询时必须从最左字段开始匹配,跳过任何前缀字段会导致后续字段索引失效——就像查字典时必须先确定首字母。

100%
graph TB
    subgraph "复合索引 {category, price, rating}"
        A[Electronics<br/>$100<br/>★5] --> B[Electronics<br/>$200<br/>★4]
        B --> C[Electronics<br/>$300<br/>★3]
        C --> D[Books<br/>$10<br/>★5]
        D --> E[Books<br/>$20<br/>★4]
    end

    subgraph "查询命中分析"
        F["✅ {category}"] --> G["✅ {category, price}"]
        G --> H["✅ {category, price, rating}"]
        I["❌ {price}"] --> J["跳过 category"]
        K["❌ {rating}"] --> L["跳过 category, price"]
        M["❌ {category, rating}"] --> N["跳过 price"]
    end

    style F fill:#d4edda
    style G fill:#d4edda
    style H fill:#d4edda
    style I fill:#f8d7da
    style K fill:#f8d7da
    style M fill:#f8d7da

使用场景

查询模式 是否命中 {category, price, rating} 原因
{category: 'A'} ✅ 全部命中 使用 category 前缀
{category: 'A', price: {$gte: 100}} ✅ 全部命中 使用 category + price 前缀
{category: 'A', price: {$gte: 100}, rating: 5} ✅ 全部命中 完整匹配三字段
{price: {$gte: 100}} ❌ 不命中 跳过最左 category
{rating: 5} ❌ 不命中 跳过 category, price
{category: 'A', rating: 5} ⚠️ 仅 category price 断裂,rating 无法使用
JAVASCRIPT
// === 复合索引:{ category: 1, price: -1, rating: 1 } ===
db.products.createIndex({ category: 1, price: -1, rating: 1 });

// ✅ 使用索引的查询:
db.products.find({ category: 'Electronics' });                                    // 用 category
db.products.find({ category: 'Electronics', price: { $gte: 100 } });            // 用 category + price
db.products.find({ category: 'Electronics', price: { $gte: 100 }, rating: 5 }); // 用全部 3 字段

// ⚠️ 不使用索引的查询:
db.products.find({ price: { $gte: 100 } });                                      // 跳过 category
db.products.find({ rating: 5 });                                                 // 跳过 category, price
db.products.find({ category: 'Electronics', rating: 5 });                       // 跳过 price

最左前缀原则:复合索引从最左字段开始连续使用才有效。跳过中间字段会断裂索引链——后续字段无法使用索引。

ESR 规则(Equality → Sort → Range):复合索引字段顺序的金科玉律——等值过滤字段在前,排序字段居中,范围查询字段在后。后续课程(第 19 课)将深入讲解。


6. 索引覆盖

概念说明:索引覆盖(Covered Query)是指查询所需的所有字段都包含在索引中,引擎直接从索引返回结果,无需回表(fetch)读取文档原文。这是索引优化的终极形态——查询完全不需要访问文档数据。

工作原理:普通查询流程为"索引扫描 → 获取文档指针 → 回表读取文档 → 提取字段 → 返回";索引覆盖流程为"索引扫描 → 直接从索引提取字段 → 返回"。省去回表环节,IO 量减半,性能提升 50% 以上。

100%
graph LR
    subgraph "普通查询"
        A1[索引扫描] --> A2[获取 doc pointer]
        A2 --> A3[回表读文档]
        A3 --> A4[提取字段]
        A4 --> A5[返回结果]
    end

    subgraph "索引覆盖查询"
        B1[索引扫描] --> B2[直接提取索引字段]
        B2 --> B3[返回结果]
    end

    style A3 fill:#f8d7da
    style B2 fill:#d4edda

使用场景

维度 普通查询 索引覆盖
执行流程 索引扫描 → 回表查文档 索引扫描 → 直接返回
IO 次数 2 次(索引 + 文档) 1 次(仅索引)
性能 基准 快 50%+
explain 标识 FETCH stage PROJECTION_COVERED
限制 投影必须排除 _id
JAVASCRIPT
// === 普通查询:需回表查文档 ===
db.products.find(
  { category: 'Electronics' },
  { sku: 1, title: 1, price: 1 }
);

// === 索引覆盖查询:直接从索引返回 ===
db.products.createIndex({ category: 1, sku: 1, title: 1, price: 1 });

db.products.find(
  { category: 'Electronics' },
  { sku: 1, title: 1, price: 1, _id: 0 }  // _id: 0 必须排除
);
// explain 中显示 stage: 'PROJECTION_COVERED' ✅

要点解析

  1. _id 默认总是返回且不在普通索引中,必须用 _id: 0 排除才能实现覆盖
  2. 索引字段越多,覆盖查询越多,但索引体积也越大——需要权衡
  3. 索引覆盖对大文档效果最显著(节省的 IO 量与文档大小成正比)

7. 索引管理

概念说明:索引管理包括索引的创建、查看、删除和重建。生产环境中索引不是"建完就忘"的,需要持续监控使用情况,删除无用索引,重建碎片化索引。

索引生命周期

100%
graph LR
    A[分析查询模式] --> B[设计索引]
    B --> C[创建索引<br/>background:true]
    C --> D[验证命中<br/>explain()]
    D --> E[监控使用率<br/>$indexStats]
    E --> F{使用率低?}
    F -->|是| G[删除索引]
    F -->|否| H[继续监控]
    G --> A
    H --> E

    style G fill:#f8d7da
    style D fill:#d4edda
管理操作 命令 说明
查看索引 db.col.getIndexes() 列出所有索引及键定义
删除索引 db.col.dropIndex(name) 按名称或键定义删除
删除全部 db.col.dropIndexes() 删除所有索引(保留 _id
重建索引 db.col.reIndex() 重建所有索引(修复碎片)
索引大小 db.col.totalIndexSize() 查看索引占用空间(字节)
索引使用率 db.col.aggregate([{$indexStats:{}}]) 查看各索引使用次数
JAVASCRIPT
// === 查看集合所有索引 ===
db.products.getIndexes();

// === 删除索引 ===
db.products.dropIndex('sku_1');
db.products.dropIndex({ sku: 1 });

// === 删除所有索引(保留 _id)===
db.products.dropIndexes();

// === 重建索引 ===
db.products.reIndex();

// === 查看索引大小 ===
db.products.totalIndexSize();

要点解析

  1. reIndex() 会锁集合,生产环境建议在维护窗口执行
  2. dropIndex() 前建议先用 $indexStats 确认索引确实未使用
  3. 每个集合索引数建议不超过 10 个,过多影响写入性能

8. 索引代价

概念说明:索引不是免费的——每个索引都会占用磁盘空间、增加写入开销、消耗内存。理解索引代价是做出正确权衡的基础。"索引越多越好"是最常见的误区。

工作原理:每次插入、更新、删除文档时,MongoDB 必须同步更新所有相关索引的 B+Tree 结构。集合有 N 个索引,写操作就需要维护 N 棵 B+Tree。索引越多,写入越慢,内存占用也越大(WiredTiger 缓存需加载索引页)。

100%
graph LR
    A[建索引] --> B[读快]
    A --> C[写慢]
    A --> D[占空间]

    B --> B1[查询 +1000x]
    C --> C1[插入/更新 +50% 开销]
    D --> D1[每索引 1-10 MB]

    subgraph "权衡决策"
        E[查询频率高?] -->|是| F[✅ 建索引]
        E -->|否| G[❌ 不建]
        H[写入频率高?] -->|是| I[⚠️ 谨慎]
        H -->|否| F
    end

索引代价量化

代价维度 影响 量化
磁盘空间 每个索引约占数据量的 5-20% 10GB 数据 × 8 索引 ≈ 4-16GB 额外空间
写入延迟 每个索引增加 ~5-10% 写入耗时 8 索引 → 写入慢 40-80%
内存占用 WiredTiger 缓存需加载索引页 索引未缓存时查询退化
维护成本 reIndex、监控、重建等运维操作 索引越多运维越复杂

索引权衡

索引选择策略

场景 建议 原因
电商商品查询 category + price 复合索引 高频筛选 + 排序
用户登录 email 唯一索引 等值查询 + 唯一性约束
日志查询 createdAt TTL 索引 时间范围 + 自动过期
状态字段(低选择性) 不建议单独建 isActive 仅 2 个值,索引效率极低
文本搜索 text 索引 全文检索专用

9. 综合实战

(1) 索引设计原则

ESR 规则详解:复合索引字段顺序应遵循 Equality → Sort → Range。等值过滤字段放最前(快速缩小范围),排序字段居中(利用索引有序性避免内存排序),范围查询字段放最后(范围扫描会中断后续字段索引使用)。

100%
graph LR
    E["Equality<br/>等值过滤<br/>category='A'"] --> S["Sort<br/>排序<br/>createdAt: -1"]
    S --> R["Range<br/>范围<br/>price >= 100"]

    style E fill:#d4edda
    style S fill:#cce5ff
    style R fill:#fff3cd
索引顺序 查询效率 原因
{E, S, R} ⭐⭐⭐ 最优 等值精确定位 → 索引排序 → 范围扫描
{E, R, S} ⭐⭐ 良好 等值定位 → 范围扫描 → 内存排序
{R, S, E} ⭐ 较差 范围扫描面太广,等值优势丧失
JAVASCRIPT
// === 电商商品索引设计 ===
db.products.createIndex({ sku: 1 }, { unique: true });                    // 唯一索引
db.products.createIndex({ category: 1, price: -1 });                       // 分类页
db.products.createIndex({ category: 1, rating: -1 });                      // 分类+评分
db.products.createIndex({ isActive: 1, createdAt: -1 });                   // 上架时间
db.products.createIndex({ title: 'text', description: 'text' });          // 全文搜索
db.products.createIndex({ tags: 1 });                                      // 标签筛选

(2) 索引使用分析

JAVASCRIPT
// === 分析慢查询 ===
db.products.find({
  category: 'Electronics',
  price: { $gte: 100, $lte: 1000 },
  isActive: true
}).sort({ createdAt: -1 }).limit(20);

// 查看是否使用索引
const explain = db.products.find({...}).explain('executionStats');
print('Stage:', explain.queryPlanner.winningPlan.stage);
print('Docs Examined:', explain.executionStats.totalDocsExamined);
print('Time:', explain.executionStats.executionTimeMillis, 'ms');

▶ 示例:索引设计与性能分析实战

JAVASCRIPT
// 1. 创建测试集合(10 万条商品数据)
for (let i = 0; i < 100000; i++) {
  db.products.insertOne({
    sku: 'SKU-' + i.toString().padStart(6, '0'),
    title: 'Product ' + i,
    category: ['Electronics', 'Books', 'Clothing', 'Home'][i % 4],
    price: Math.random() * 1000,
    stock: Math.floor(Math.random() * 100),
    createdAt: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000),
    isActive: true
  });
}

// 2. 创建索引
db.products.createIndex({ sku: 1 }, { unique: true });
db.products.createIndex({ category: 1, price: -1 });  // 复合索引
db.products.createIndex({ createdAt: -1 });

// 3. 对比:有索引 vs 无索引
console.time('无索引查询');
db.products.find({ category: 'Electronics', price: { $gte: 100, $lte: 500 } }).toArray();
console.timeEnd('无索引查询');  // ~500ms

// 创建索引后
db.products.createIndex({ category: 1, price: 1 });

console.time('有索引查询');
db.products.find({ category: 'Electronics', price: { $gte: 100, $lte: 500 } }).toArray();
console.timeEnd('有索引查询');  // ~5ms(性能 ↑100x)

// 4. explain() 分析执行计划
const explain = db.products.find({
  category: 'Electronics',
  price: { $gte: 100, $lte: 500 }
}).sort({ createdAt: -1 }).limit(20).explain('executionStats');

print('Stage:', explain.queryPlanner.winningPlan.stage);  // IXSCAN
print('Index:', explain.queryPlanner.winningPlan.inputStage?.indexName);  // category_1_price_1
print('Docs Examined:', explain.executionStats.totalDocsExamined);
print('Keys Examined:', explain.executionStats.totalKeysExamined);
print('Returned:', explain.executionStats.nReturned);
print('Time:', explain.executionStats.executionTimeMillis, 'ms');

// 5. 索引覆盖查询(无需回表)
db.products.createIndex({ category: 1, sku: 1, price: 1 });

db.products.find(
  { category: 'Electronics' },
  { sku: 1, price: 1, _id: 0 }  // 仅返回索引中已有的字段
).explain();

// Stage: PROJECTION_COVERED(无需查文档)

输出:索引让查询性能提升 100 倍,explain() 显示 IXSCAN(索引扫描)+ PROJECTION_COVERED(索引覆盖)。

❓ 常见问题

Q 索引越多越好吗?
A 不是。索引加速读但拖慢写,占用空间。一般 5-10 个/集合合适。
Q 复合索引字段顺序重要吗?
A 非常重要。高选择性(唯一值多)的字段放前面,遵循 ESR 原则(Equality → Sort → Range)。
Q 索引为什么没生效?
A 常见原因:(1) 字段顺序不匹配最左前缀;(2) 用了 $ne / $nin / $exists;(3) 数据类型不匹配;(4) 集合数据太少(< 100 条不优化)。
Q explain() 中 COLLSCAN 怎么优化?
A 分析查询条件,为过滤字段建索引。考虑复合索引覆盖所有过滤条件。

📖 小节


📝 作业

  1. 基础题(⭐):为 products 集合创建 sku、category、createdAt 三个索引。
  2. 基础题(⭐):用 explain() 分析一个查询,确认使用了索引(IXSCAN)。
  3. 进阶题(⭐⭐):创建复合索引 { category: 1, price: -1 },测试最左前缀原则(哪些查询用索引)。
  4. 进阶题(⭐⭐):实现索引覆盖查询(所有字段都在索引中)。
  5. 挑战题(⭐⭐⭐):电商商品索引完整设计(8 个索引),分析每个索引的查询场景。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏