MongoDB: 索引类型与高级特性

最后更新:2026-08-26

高级索引特性——掌握 TTL、partial、ESR 规则,构建生产级索引体系。

1. 你将学到


100%
graph TB
    A[索引类型] --> B[unique<br/>唯一索引]
    A --> C[sparse<br/>稀疏索引]
    A --> D[TTL<br/>自动过期]
    A --> E[partial<br/>部分索引]
    A --> F[compound<br/>复合索引]

    B --> B1[保证唯一性<br/>重复抛错]
    C --> C1[跳过 null<br/>节省空间]
    D --> D1[自动删除<br/>定时清理]
    E --> E1[仅索引子集<br/>性能 + 节省]

    style D fill:#d4edda
    style E fill:#d4edda

2. 唯一索引(unique)

概念说明:唯一索引保证索引字段值在整个集合中不重复,是数据完整性的数据库级保障。与应用层校验不同,唯一索引是数据库引擎强制的——无论通过哪个客户端写入,重复值都会被拒绝。

工作原理:唯一索引在 B+Tree 中额外维护唯一性约束。每次插入或更新时,引擎先检查索引中是否已存在相同键值,若存在则抛出 E11000 duplicate key error。复合唯一索引要求字段组合唯一(单个字段可重复)。

使用场景

维度 普通索引 唯一索引
值约束 允许重复 不允许重复
插入检查 仅更新 B+Tree 更新 B+Tree + 唯一性校验
写入性能 基准 略慢(+5% 校验开销)
错误提示 E11000 duplicate key error
部分唯一 不支持 配合 partialFilterExpression 实现
JAVASCRIPT
// === 创建唯一索引 ===
db.users.createIndex({ email: 1 }, { unique: true });
// email 字段值必须唯一

// === 复合唯一索引 ===
db.products.createIndex({ sku: 1, variant: 1 }, { unique: true });
// (sku, variant) 组合必须唯一

// === 唯一索引 + 部分字段 ===
db.users.createIndex(
  { email: 1 },
  { unique: true, partialFilterExpression: { email: { $exists: true } } }
);
// 只对存在 email 字段的文档应用唯一约束

要点解析

  1. 唯一索引允许 null 值存在,但整个集合只能有一个 null(null 被视为相同值)
  2. 使用 partialFilterExpression 可让唯一约束仅对部分文档生效,解决 null 唯一性问题
  3. 唯一索引创建前,集合中不能已有重复值,否则创建失败

3. 稀疏索引(sparse)

概念说明:稀疏索引只对存在索引字段且非 null 的文档建索引,跳过字段缺失或为 null 的文档。普通索引会对所有文档建索引(缺失字段视为 null),稀疏索引通过排除无效条目节省空间。

工作原理:创建稀疏索引时,引擎在插入文档后检查索引字段是否存在且非 null,只有满足条件才会向 B+Tree 插入索引条目。查询时,如果使用稀疏索引,结果中不会包含字段缺失的文档(因为它们不在索引中)。

使用场景

100%
graph LR
    subgraph "集合文档"
        D1["{sku:'A', discount:10}"]
        D2["{sku:'B'}"]
        D3["{sku:'C', discount:null}"]
        D4["{sku:'D', discount:20}"]
    end

    subgraph "普通索引"
        I1["A→D1, B→D2, C→D3, D→D4"]
    end

    subgraph "稀疏索引"
        I2["10→D1, 20→D4<br/>(仅2条)"]
    end

    style I2 fill:#d4edda
场景 推荐 原因
字段经常缺失(如可选字段) 稀疏索引 节省空间,仅索引有值的文档
字段必有值 普通索引 无需跳过,稀疏无优势
唯一 + 允许多个 null 稀疏唯一索引 null 不参与唯一性检查
查询需返回 null 文档 普通索引 稀疏索引会遗漏 null 文档
JAVASCRIPT
// === 稀疏索引:跳过 null 字段 ===
db.products.createIndex({ discount: 1 }, { sparse: true });
// 仅索引有 discount 字段的文档

// === 稀疏索引 vs 普通索引 ===
// 普通索引:所有文档都建索引(包括 null)
// 稀疏索引:仅非 null 文档建索引

要点解析

  1. 稀疏索引不覆盖字段缺失的文档,find({discount: null}) 不会返回缺失该字段的文档
  2. 稀疏 + 唯一组合:允许多个文档缺失该字段,同时保证已有值唯一
  3. partial 索引是稀疏索引的超集(更灵活),推荐优先使用 partial 索引

4. TTL 索引(自动过期)

概念说明:TTL(Time-To-Live)索引是 MongoDB 的自动过期删除机制,它基于日期字段自动删除超过指定时间的文档。无需手动清理,无需定时任务,数据库引擎后台自动完成——是会话管理、日志清理的利器。

工作原理:TTL 索引在 B+Tree 之上增加了一个后台清理线程。该线程每 60 秒运行一次,扫描索引中的日期字段,计算 当前时间 - 字段值 > expireAfterSeconds,删除所有过期文档。删除操作本身也有写开销,大量过期文档可能造成突发写入压力。

100%
sequenceDiagram
    participant App as 应用
    participant TTL as TTL后台线程
    participant IX as TTL索引B+Tree
    participant DOC as 集合文档

    App->>DOC: 插入 {token:'abc', createdAt: T1}
    DOC->>IX: 索引条目 createdAt=T1

    Note over TTL: 每60秒运行一次

    loop 每60秒
        TTL->>IX: 扫描 createdAt 值
        IX-->>TTL: 返回所有条目
        TTL->>TTL: 计算当前时间 - createdAt
        alt 过期 ( > expireAfterSeconds )
            TTL->>DOC: 删除过期文档
            DOC->>IX: 删除对应索引条目
        else 未过期
            Note over TTL: 跳过
        end
    end

使用场景

场景 expireAfterSeconds 典型值
用户会话 30 分钟 1800
验证码 10 分钟 600
日志数据 保留 90 天 7776000
临时令牌 1 小时 3600
限流记录 1 天 86400

⚠️ TTL 限制

限制 说明 解决方案
不能是复合索引 TTL 只能基于单个日期字段 需要复合条件用 partial 索引 + 应用层清理
字段必须是 Date 类型 Number/String 不支持 使用 new Date() 存储时间
删除延迟 后台线程每 60 秒扫描一次 最多延迟 60 秒,非精确时间
不保证精确删除 大量过期文档可能分批删除 业务层做好容错
JAVASCRIPT
// === 创建 TTL 索引(30 天后自动删除)===
db.sessions.createIndex(
  { createdAt: 1 },
  { expireAfterSeconds: 30 * 24 * 60 * 60 }
);

// === 修改 TTL ===
db.runCommand({
  collMod: 'sessions',
  index: { keyPattern: { createdAt: 1 }, expireAfterSeconds: 7 * 24 * 60 * 60 }
});

// === 取消 TTL(设为 false)===
db.runCommand({
  collMod: 'sessions',
  index: { keyPattern: { createdAt: 1 }, expireAfterSeconds: -1 }
});

典型场景


5. Partial 部分索引

概念说明:Partial 索引只对满足过滤条件的文档建索引,是稀疏索引的升级版。它通过 partialFilterExpression 指定哪些文档参与索引,既能节省空间,又能提高索引精度,是生产环境最推荐的索引优化手段之一。

工作原理:创建 partial 索引时,引擎仅将满足 partialFilterExpression 的文档插入 B+Tree。查询时,如果查询条件"覆盖"了 partialFilterExpression(即查询条件是过滤条件的子集或等价),优化器才会选择该索引;否则不会使用。

使用场景

100%
graph TB
    subgraph "集合(10000文档)"
        A[全部文档<br/>10000条]
    end

    subgraph "普通索引"
        B[索引条目<br/>10000条<br/>~10MB]
    end

    subgraph "Partial索引<br/>isActive=true, stock>0"
        C[索引条目<br/>3000条<br/>~3MB]
    end

    A --> B
    A --> C

    style C fill:#d4edda
对比维度 普通索引 Partial 索引 Sparse 索引
过滤条件 支持 $eq/$gt/$gte/$lt/$lte/$exists/$type/$and 仅字段是否存在
空间节省 0% 30-70% 取决于缺失比例
灵活性 基准 ⭐⭐⭐ 最灵活 ⭐ 较局限
查询限制 查询条件需覆盖 filter 条件 查询需包含字段条件
推荐 默认 ✅ 生产首选 仅简单场景
表达式 支持
$eq / $gt / $gte / $lt / $lte
$exists: true
$type
$and
$or / $in / $nin
JAVASCRIPT
// === 部分索引:仅索引满足条件的文档 ===
db.products.createIndex(
  { category: 1, price: 1 },
  {
    partialFilterExpression: {
      isActive: true,
      stock: { $gt: 0 }
    }
  }
);
// 仅索引在售商品(isActive=true, stock>0)

// === 部分索引节省空间 ===
// 普通索引:1 万文档全部建索引
// 部分索引:仅 3000 个在售商品建索引(节省 70% 空间)

▶ 示例 1: Partial 索引实战

JAVASCRIPT
// ShopHub:仅索引在售且有库存的商品,节省 70% 索引空间
db.products.insertMany([
  { sku: 'A001', category: 'Electronics', price: 599, isActive: true, stock: 50 },
  { sku: 'A002', category: 'Electronics', price: 299, isActive: false, stock: 0 },
  { sku: 'A003', category: 'Books', price: 29, isActive: true, stock: 100 },
  { sku: 'A004', category: 'Books', price: 49, isActive: true, stock: 0 }
]);

// Partial 索引仅索引 isActive=true AND stock>0 的文档(A001, A003)
db.products.createIndex(
  { category: 1, price: 1 },
  { partialFilterExpression: { isActive: true, stock: { $gt: 0 } } }
);

// 查询必须包含 filter 条件才能命中
db.products.find({
  category: 'Electronics',
  isActive: true,
  stock: { $gt: 0 },
  price: { $gte: 100 }
}).explain();
// winningPlan.stage: IXSCAN ✅

// 缺少 filter 条件 → 不命中
db.products.find({ category: 'Electronics', price: { $gte: 100 } }).explain();
// winningPlan.stage: COLLSCAN(不使用 partial 索引)

输出:

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

6. ESR 规则

Equality → Sort → Range:复合索引字段顺序的金科玉律。

概念说明:ESR 规则是 MongoDB 索引设计最重要的规则,它规定了复合索引中字段的最优排列顺序:等值过滤(Equality)排在最前,排序字段(Sort)居中,范围查询(Range)放最后。违反 ESR 规则会导致索引效率大幅下降甚至失效。

工作原理

100%
graph TB
    subgraph "ESR 索引 {status, createdAt, price}"
        A["Equality: status='paid'<br/>→ 定位到所有 paid 文档"] --> B["Sort: createdAt: -1<br/>→ 索引已排序,无需内存排序"]
        B --> C["Range: price >= 100<br/>→ 范围扫描,中断后续字段"]
    end

    subgraph "反例:RSE 索引 {price, status, createdAt}"
        D["Range: price >= 100<br/>→ 扫描大量索引条目"] --> E["Sort: status<br/>→ 需内存排序"]
        E --> F["Equality: createdAt<br/>→ 已无法利用索引"]
    end

    style A fill:#d4edda
    style B fill:#cce5ff
    style C fill:#fff3cd
    style D fill:#f8d7da
    style E fill:#f8d7da
    style F fill:#f8d7da

ESR 规则对比表

顺序 类型 示例 作用
1st Equality category: 'Electronics' 快速缩小范围
2nd Equality isActive: true 进一步缩小范围
3rd Sort sort: { createdAt: -1 } 利用索引有序性,免排序
4th Range price: { $gte: 100 } 范围扫描,放最后

违反 ESR 的后果

错误排列 后果 explain 表现
Range 在 Equality 前 范围扫描面太广 totalKeysExamined 远大于 nReturned
Sort 在 Range 后 无法利用索引排序 出现 SORT stage(内存排序)
跳过 Equality 直接 Sort 排序无精确起点 全索引扫描 + 排序
JAVASCRIPT
// === 查询:等值过滤 + 排序 + 范围 ===
db.products.find({
  category: 'Electronics',          // Equality
  isActive: true                    // Equality
}).sort({ createdAt: -1 })          // Sort
  .limit(20);

// 价格范围
db.products.find({
  category: 'Electronics',
  createdAt: { $gte: new Date('2026-01-01') }  // Range
}).sort({ rating: -1 });

// === 最佳索引 ===
db.products.createIndex({
  category: 1,         // E - 等值
  isActive: 1,         // E - 等值
  createdAt: -1,       // S - 排序(索引方向匹配)
  rating: -1           // R - 范围(放最后)
});
顺序 类型 示例
1st Equality category: 'Electronics'
2nd Equality isActive: true
3rd Sort sort: { createdAt: -1 }
4th Range createdAt: { $gte: ... }

▶ 示例 2: ESR 规则对比实战

JAVASCRIPT
// TechCorp 订单系统:对比 ESR 正确 vs 错误排列的执行计划
db.orders.insertMany([
  { userId: 'user_001', status: 'paid', createdAt: new Date('2026-07-01'), total: 100 },
  { userId: 'user_002', status: 'pending', createdAt: new Date('2026-07-02'), total: 200 },
  { userId: 'user_001', status: 'paid', createdAt: new Date('2026-07-03'), total: 50 }
]);

// ❌ 错误索引:Range 在 Sort 前
db.orders.createIndex({ userId: 1, total: 1, status: 1, createdAt: -1 }, { name: 'idx_wrong' });
db.orders.find({ userId: 'user_001', status: 'paid' })
  .sort({ createdAt: -1 }).explain('executionStats');
// 出现 SORT stage(内存排序),totalKeysExamined 远大于 nReturned

// ✅ 正确索引:ESR 顺序
db.orders.dropIndex('idx_wrong');
db.orders.createIndex({ userId: 1, status: 1, createdAt: -1, total: 1 }, { name: 'idx_esr' });
db.orders.find({ userId: 'user_001', status: 'paid' })
  .sort({ createdAt: -1 }).explain('executionStats');
// 无 SORT stage,totalKeysExamined ≈ nReturned

输出:

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

7. 索引设计最佳实践

概念说明:索引设计不是孤立的技术决策,而是基于查询模式、数据特征和业务需求的综合权衡。好的索引设计能让查询飞起来,坏的设计则浪费空间、拖慢写入。

设计原则

  1. 基于查询模式设计——先分析 explain() 和慢查询日志,再建索引
  2. ESR 优先——复合索引字段顺序遵循 Equality → Sort → Range
  3. 高选择性优先——唯一值多的字段(如 userId)比低选择性字段(如 isActive)更适合索引
  4. 索引覆盖——高频查询考虑索引覆盖,避免回表
  5. 定期清理——用 $indexStats 发现并删除未使用索引
100%
graph TB
    A[分析查询模式<br/>慢查询日志] --> B[识别高频查询]
    B --> C{查询类型?}
    C -->|等值查询| D[单字段/唯一索引]
    C -->|多条件组合| E[复合索引 ESR]
    C -->|排序+筛选| F[ESR复合索引]
    C -->|仅显示少量字段| G[索引覆盖]
    B --> H[评估选择性]
    H -->|高选择性| I[✅ 建索引]
    H -->|低选择性| J[❌ 不建/用partial]
    I --> K[创建+验证<br/>explain]
    K --> L[监控使用率<br/>$indexStats]
    L --> M{使用率低?}
    M -->|是| N[删除索引]
    M -->|否| O[保留]

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

(1) 推荐做法

JAVASCRIPT
// ✅ 推荐 1:单字段索引覆盖高频查询
db.products.createIndex({ sku: 1 });

// ✅ 推荐 2:复合索引遵循 ESR 原则
db.orders.createIndex({ userId: 1, status: 1, createdAt: -1 });

// ✅ 推荐 3:TTL 自动清理
db.logs.createIndex({ createdAt: 1 }, { expireAfterSeconds: 30 * 86400 });

// ✅ 推荐 4:部分索引节省空间
db.products.createIndex(
  { category: 1 },
  { partialFilterExpression: { isActive: true } }
);

// ✅ 推荐 5:唯一索引保证数据完整性
db.users.createIndex({ email: 1 }, { unique: true });

(2) 反模式

常见反模式及后果

反模式 后果 正确做法
低选择性字段建索引 索引效率差,优化器可能放弃 用 partial 索引或复合索引
过多索引(>10/集合) 写性能严重下降 仅保留高频查询索引
复合索引字段顺序错误 索引部分失效 遵循 ESR 规则
不必要的索引 浪费空间 + 写入开销 用 $indexStats 定期清理
索引未验证命中 可能根本没走索引 创建后必须 explain 验证
JAVASCRIPT
// ❌ 反模式 1:低选择性字段建索引
db.users.createIndex({ isActive: 1 });
// isActive 只有 true/false 两个值,索引效率低

// ❌ 反模式 2:过多索引
// 一个集合超过 20 个索引,写性能严重下降

// ❌ 反模式 3:复合索引字段顺序错误
db.products.createIndex({ price: 1, category: 1 });
// 错:price 应放 category 之后

// ❌ 反模式 4:不必要的索引
db.users.createIndex({ lastLoginAt: 1 });
// 如果很少按 lastLoginAt 查询,索引浪费

8. 索引性能监控

概念说明:索引监控是生产环境必不可少的运维环节。通过慢查询日志和索引使用统计,可以发现未使用的索引(浪费空间)、低效的索引(需要优化)、缺失的索引(需要新建)。

工作原理:MongoDB 内置 Profiler 记录慢查询,$indexStats 聚合管道统计每个索引的使用次数和时间。两者结合,可以全面了解索引的健康状况。

监控指标

指标 命令 健康值 警告值
索引使用率 $indexStats ops > 1000/天 ops = 0(未使用)
慢查询数 system.profile 0 > 10/小时
索引大小 totalIndexSize() < 数据量 50% > 数据量 100%
索引碎片化 collStats 平均填充率 > 80% < 60%
JAVASCRIPT
// === 慢查询分析 ===
db.setProfilingLevel(2, { slowms: 100 });
// 记录超过 100ms 的查询

// === 查看慢查询 ===
db.system.profile.find({ millis: { $gt: 100 } })
  .sort({ ts: -1 })
  .limit(10);

// === 查看索引使用统计 ===
db.products.aggregate([
  { $indexStats: {} }
]);
// 找出 unused index

// === 删除未使用的索引 ===
db.products.dropIndex('unused_index_name');

要点解析

  1. Profiling Level 0=关闭,1=仅慢查询,2=全部记录(Level 2 影响性能,仅调试用)
  2. $indexStatsaccesses.ops 为 0 表示自 MongoDB 启动以来从未使用
  3. 生产环境建议 Level 1 + slowms: 100,平衡监控粒度与性能

▶ 示例:TTL 索引 + 部分索引 + ESR 实战

JAVASCRIPT
// 场景 1:TTL 索引自动清理会话(30 分钟过期)
db.sessions.insertMany([
  { userId: 'user_001', token: 'abc', createdAt: new Date() },
  { userId: 'user_002', token: 'xyz', createdAt: new Date(Date.now() - 31 * 60 * 1000) }  // 已过期
]);

db.sessions.createIndex(
  { createdAt: 1 },
  { expireAfterSeconds: 30 * 60 }  // 30 分钟
);

// 等待 60 秒后,过期文档被自动删除
// db.sessions.find() → 只剩 user_001 的会话

// 场景 2:部分索引 - 仅索引在售商品(节省 70% 空间)
db.products.createIndex(
  { category: 1, price: 1 },
  {
    partialFilterExpression: {
      isActive: true,
      stock: { $gt: 0 }
    }
  }
);

// 部分索引只在查询包含 filter 条件时使用
db.products.find({
  category: 'Electronics',
  isActive: true,    // 必须匹配 partialFilterExpression
  stock: { $gt: 0 }, // 必须匹配 partialFilterExpression
  price: { $gte: 100, $lte: 500 }
}).explain();
// winningPlan.inputStage.stage: IXSCAN(使用了部分索引)

// 场景 3:ESR 规则实战 - 订单查询
db.orders.insertMany([
  { userId: 'user_001', status: 'paid', createdAt: new Date('2026-07-01'), total: 100 },
  { userId: 'user_001', status: 'paid', createdAt: new Date('2026-07-02'), total: 200 },
  { userId: 'user_002', status: 'pending', createdAt: new Date('2026-07-03'), total: 50 }
]);

// ESR 最佳索引:Equality → Sort → Range
db.orders.createIndex({
  userId: 1,        // E - 等值过滤
  status: 1,        // E - 等值过滤
  createdAt: -1,    // S - 排序(方向匹配)
  total: 1          // R - 范围查询(放最后)
});

// 高效查询:用户已支付订单,按时间倒序,价格范围
db.orders.find({
  userId: 'user_001',                              // E
  status: 'paid',                                  // E
  total: { $gte: 50, $lte: 300 }                   // R
}).sort({ createdAt: -1 }).limit(20)               // S
.explain('executionStats');

// 输出:完全使用复合索引,无需额外排序
// totalKeysExamined: 2, totalDocsExamined: 2, nReturned: 2

输出:TTL 自动清理过期会话;部分索引节省空间;ESR 复合索引让查询达到最优性能。

❓ 常见问题

Q TTL 索引删除文档的延迟?
A 后台线程每 60 秒扫描一次。最长延迟 60 秒 + 文档生命周期。
Q partial 索引 vs sparse 索引?
A partial 索引支持更复杂的条件($gt/$gte/$and),sparse 仅基于字段是否存在。推荐 partial(更灵活)。
Q ESR 规则中 sort 字段方向?
A sort 方向要与索引方向匹配。sort({ createdAt: -1 })createdAt: -1 索引。
Q 索引什么时候被删除?
A (1) 手动 dropIndex;(2) 集合删除;(3) TTL 索引自动过期(仅日期字段)。

📖 小节


📝 作业

  1. 基础题(⭐):为 users 集合创建 email 唯一索引。
  2. 基础题(⭐):为 sessions 集合创建 TTL 索引(30 分钟过期)。
  3. 进阶题(⭐⭐):为 products 集合创建 partial 索引(仅在售商品)。
  4. 进阶题(⭐⭐):应用 ESR 规则为 orders 集合设计最优索引。
  5. 挑战题(⭐⭐⭐):完整电商索引设计(10+ 索引),用 $indexStats 分析使用率,删除未使用索引。
Web-Tutorial.com

Web-Tutorial 技术团队

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

100%

🙏 帮我们做得更好

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

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