背景:索引的核心价值与基础原理
索引本质:对指定字段预排序的数据结构(B-tree),通过空间换时间优化查询性能
搜索时间复杂度优化至 O(log n)
关键优势:
- 查询性能提升 10~1000 倍(取决于数据规模)
- 排序操作可直接利用索引顺序,避免全表扫描
以用户银行账户集合为例,文档结构包含字段:
name(客户姓名)currency(账户货币种类)balance(账户余额)
性能瓶颈与解决方案
原始数据存储结构(无序):
Bob → Sam → Alice → Bob
- 问题场景:查询
name: "Alice"需遍历所有文档(时间复杂度O(n)) - 索引作用:构建排序映射(
时间复杂度 O(log n))
索引创建流程:
- 提取目标字段值(如
name) - 对值排序(字母序:Alice → Billy → Bob)
- 每个值关联指向原始文档存储地址的指针

索引的性能价值对比
| 场景 | 时间复杂度 | 百万文档查询耗时 | 性能影响 |
|---|---|---|---|
无索引(COLLSCAN) | O(n) | 秒级~分钟级 | 线性劣化,资源消耗高 |
有索引(IXSCAN) | O(log n) | 毫秒级 | 查询加速 10~1000 倍 |
趋势结论:数据量每增加一个数量级,无索引查询耗时成倍增长,索引优化效果更显著。
索引类型与核心特性
索引分类与创建方法
| 类型 | 适用场景 | 创建代码(Mongo Shell) | 说明 |
|---|---|---|---|
| 单键索引 | 单字段查询/排序 | db.accounts.createIndex({ name: 1 }) | 索引名称自动生成:name_1 |
| 复合键索引 | 多字段组合查询/排序 | db.accounts.createIndex({ name: 1, balance: -1 }) | 索引名称:name_1_balance_-1 |
| 多键索引 | 数组字段元素查询 | db.accounts.createIndex({ currencies: 1 }) | – |
复合键索引的核心规则:前缀子查询
- 定义:索引
{ a:1, b:1, c:1 }仅支持以下查询:{ a: value }{ a: value, b: value }{ a: value, b: value, c: value }
- 不支持的场景:
- 非前缀字段查询(如
{ b: value }或{ c: value })。 - 跳前缀查询(如
{ b: value, c: value })。
- 非前缀字段查询(如
- 示例:
- 索引
{name:1, currency:1, balance:1}可加速{name:"Alice"}或{name:"Alice", currency:"CNY"}。 - 无法加速
{balance:100}或{currency:"USD", balance:100}(非前缀组合)。
- 索引
复合键索引的前缀子查询规则(简注:索引生效的前提条件),前缀子查询规则:
-
复合索引仅支持从左到右连续字段的查询:
索引字段 有效查询字段 无效查询字段 A-B-C A / A-B / A-B-C B / C / B-C -
示例
// 索引定义:{ name:1, currency:1, balance:1 } // ✅ 有效查询(前缀匹配) db.accounts.find({ name: "Alice" }) // 使用第一个字段 db.accounts.find({ name: "Alice", currency: "CNY" }) // 使用前两个字段 // ❌ 无效查询(非前缀) db.accounts.find({ balance: 100 }) // 缺失前缀字段 db.accounts.find({ currency: "USD", balance: 100 }) // 跳过前缀字段 -
多键索引(数组字段)
// 为数组字段 currency 创建索引 db.accounts.createIndex({ currency: 1 }) -
存储机制:数组每个元素单独作为索引键
-
数据结构示例:
GBP → 指向 Alice 文档地址 USD → 指向 Alice 文档地址 EUR → 指向 Bob 文档地址
索引管理操作
// 查看索引
db.accounts.getIndexes() // 包含默认 _id 索引
// 删除索引
db.accounts.dropIndex("name_1") // 按名称删除
db.accounts.dropIndex({ name:1, balance:-1 }) // 按定义删除
// 重建索引(需先删后建)
db.accounts.reIndex()
// 或
db.accounts.dropIndex("old_index");
db.accounts.createIndex({ newField: 1 }, { name: "new_index" });
要点
- 复合键索引字段顺序 = 查询频率优先级
- 唯一性+稀疏性组合解决非空字段约束问题
- TTL索引仅支持单键索引,后台清理存在延迟
索引设计最佳实践
- 前缀匹配优先:复合键字段顺序按查询频率从高到低排列。
- 避免过度索引:索引增加写入开销(需维护数据结构)
- 覆盖查询优化:优先返回索引字段,消除
FETCH阶段
四大高级特性
1 ) 唯一性(Unique)
db.accounts.createIndex({ balance: 1 }, { unique: true })
// 规则:字段值必须唯一,缺失字段视为 null(仅允许一篇文档缺失)
- 默认唯一索引:
_id字段自动创建 - 冲突规则:
- 字段值重复的文档会导致创建失败
- 缺失字段处理:多篇文档缺失该字段时触发唯一性冲突(值视为
null)
2 ) 稀疏性(Sparse)
db.accounts.createIndex({ balance: 1 }, { sparse: true })
// 规则:仅索引包含该字段的文档(节省存储空间)
- 节省空间:缺失字段的文档不加入索引
- 复合索引规则:仅当文档缺失所有索引字段时被排除
3 )唯一性+稀疏性组合
- 允许缺省字段且不触发唯一性约束(常用场景:可选唯一字段)
db.accounts.createIndex( { balance: 1 }, { unique: true, sparse: true } // 关键组合 ); // 可插入多篇无 balance 文档 db.accounts.insert({ name: "Doc3" }); // 成功 db.accounts.insert({ name: "Doc4" }); // 成功
4 ) TTL生存时间索引
db.accounts.createIndex({ lastAccess: 1 }, { expireAfterSeconds: 20 })
// 规则:自动删除过期文档(适用于会话/日志)
// 插入文档(自动过期删除)
db.accounts.insert({
name: "Temp",
lastAccess: new Date() // 插入时间戳
});
// 20 秒后文档自动删除
- 字段要求:日期类型或日期数组
- 数组处理:取最小日期值判断过期
- 删除机制:后台线程执行,存在延迟
特性组合最佳实践:
// 唯一性+稀疏性 → 处理可选唯一字段
db.accounts.createIndex({ balance: 1 }, { unique: true, sparse: true })
索引效果分析与性能验证
使用 explain() 方法分析查询执行计划:
// 分析未使用索引的查询
db.accounts.find({ balance: 100 }).explain("executionStats")
关键执行阶段解析:
| 执行阶段 (stage) | 含义 | 性能影响 |
|---|---|---|
COLLSCAN | 全集合扫描 | 严重性能瓶颈 |
IXSCAN | 索引扫描 | 高效 |
FETCH | 根据指针获取完整文档 | 可优化项 |
索引优化示例:
// 案例1:索引覆盖查询(避免 FETCH 阶段)
db.accounts.find({ name: "Alice" }, { _id: 0, name: 1 })
// 案例2:索引加速排序
db.accounts.find().sort({ name: 1, balance: -1 })
// 若匹配复合索引 {name:1, balance:-1} 则无 SORT 阶段
性能对比:
- 全表扫描 (
COLLSCAN):强制内存排序,资源消耗高 - 索引扫描 (
IXSCAN):直接按索引顺序返回结果
性能分析:explain() 命令
- 场景对比:
查询类型 winningPlan.stage性能 无索引字段查询 COLLSCAN(全表扫描)低效(O(n)) 索引字段查询 IXSCAN(索引扫描)高效(O(log n)) 索引覆盖查询(仅返回索引字段) IXSCAN(无FETCH阶段)最优 - 示例:
// 低效查询(无索引) db.accountsWithIndex.find({ balance: 100 }).explain(); // 输出: "winningPlan.stage": "COLLSCAN" // 高效查询(使用索引) db.accountsWithIndex.find({ name: "Alice" }).explain(); // 输出: "winningPlan.stage": "IXSCAN"
案例:性能验证与优化实战
案例1:Explain分析索引效果
// 无索引查询 → COLLSCAN(全表扫描)
db.accounts.find({ balance: 100 }).explain("executionStats")
/* 输出关键指标:
"executionStats": {
"executionTimeMillis": 120, // 耗时高
"totalDocsExamined": 1000000, // 扫描所有文档
"stage": "COLLSCAN" // 性能瓶颈
}
*/
// 有索引查询 → IXSCAN(索引扫描)
db.accounts.find({ name: "Alice" }).explain("executionStats")
/* 输出:
"executionStats": {
"executionTimeMillis": 2, // 毫秒级响应
"totalDocsExamined": 1, // 仅扫描匹配文档
"stage": "IXSCAN",
"indexName": "name_1"
}
*/
2 ) 案例2:索引覆盖查询(简注:避免FETCH阶段)
// 仅返回索引字段 → 直接读取索引数据
db.accounts.find({ name: "Alice" }, { _id: 0, name: 1 })
// 执行计划显示无 FETCH 阶段(性能最优)
3 ) 案例3:复合索引加速排序
// 索引匹配排序条件
db.accounts.find().sort({ name: 1, balance: -1 })
// 若存在索引 {name:1, balance:-1} → 直接复用索引顺序
// 索引不匹配 → 触发内存排序
db.accounts.find().sort({ name: 1, balance: 1 }) // 出现 SORT 阶段
4 ) 案例4:NestJS中实现索引(TypeORM)
typeorm 示例
import { Entity, Column, Index } from 'typeorm';
@Entity()
export class Account {
@Column()
@Index() // 单字段索引
name: string;
@Column('simple-array')
@Index({ sparse: true }) // 数组稀疏索引
currencies: string[];
@Column()
@Index('IDX_BALANCE', { unique: true }) // 唯一索引
balance: number;
@Column({ type: 'timestamp' })
@Index('IDX_LAST_ACCESS', { expireAfterSeconds: 3600 }) // TTL索引
lastAccess: Date;
}
mogoose 示例
// 使用 Mongoose 在 NestJS 中创建索引
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
@Schema()
export class Account {
@Prop({ required: true })
name: string;
@Prop({ type: [String] })
currency: string[];
@Prop()
balance: number;
@Prop({ type: Date })
lastAccess: Date;
}
export const AccountSchema = SchemaFactory.createForClass(Account);
// 添加索引
AccountSchema.index({ name: 1 }); // 单键索引
AccountSchema.index({ name: 1, balance: -1 }); // 复合键索引
AccountSchema.index({ lastAccess: 1 }, { expireAfterSeconds: 20 }); // TTL索引
要点
explain()的stage字段是性能关键指标(优先IXSCAN)- 索引覆盖查询可减少 50% 以上 I/O 开销
- 复合索引字段顺序必须与排序方向一致
5 ) 原生 SQL 索引操作对比
-- 创建单字段索引 (MySQL)
CREATE INDEX idx_name ON accounts(name);
-- 创建复合索引 (PostgreSQL)
CREATE INDEX idx_name_currency ON accounts(name, currency);
-- 创建唯一约束 (SQL Server)
CREATE UNIQUE INDEX uq_balance ON accounts(balance);
索引最佳实践与生产建议
核心特性总结
| 特性 | 适用场景 | 生产环境建议 |
|---|---|---|
| 复合键索引 | 多字段组合查询 | 字段顺序按查询频率排列 |
| 唯一性+稀疏性 | 可选唯一字段(如手机号) | 优先使用组合特性 |
| TTL索引 | 临时数据(会话/日志) | 设置合理过期时间 |
- 索引本质: 排序后的字段-地址映射表(B-Tree),将查询复杂度从 O(n) 降至 O(log n)
- 复合键索引: 必须遵循前缀子查询规则,非前缀查询无法优化
- 索引代价: 占用存储空间,降低写入速度(需更新索引),需权衡读写比例
- 特性组合:
唯一性+稀疏性是处理可选唯一字段的最佳实践,TTL 索引适用于临时数据场景
索引选择原则
- 优先为高频查询字段创建索引
- 复合索引字段顺序 = 查询频率排序
- 避免全集合扫描 (
COLLSCAN)
索引代价平衡矩阵
| 优势 | 代价 | 缓解策略 |
|---|---|---|
| 查询性能提升 10~1000 倍 | 占用额外磁盘/内存空间 | 定期清理无用索引 |
避免全表扫描(COLLSCAN) | 降低写入速度(维护索引成本) | 读写分离架构 |
| 加速排序操作 | 增加代码复杂度 | 使用 explain() 持续监控 |
生产环境最佳实践
- 唯一性+稀疏性组合:
{ unique: true, sparse: true } - TTL 索引用于日志/会话等临时数据
- 设计阶段
- 为高频查询字段创建索引
- 复合索引字段 ≤ 3 个(避免过度索引)
- 运维阶段
- 每月执行
db.collection.stats()分析索引大小 - 使用 Performance Advisor(Atlas工具)自动推荐索引
- 每月执行
- 监控告警
- 设置慢查询阈值(
db.setProfilingLevel(1, { slowms: 100 })) - 当
COLLSCAN比例 > 5% 时触发告警
- 设置慢查询阈值(
终极原则:
- 索引不是越多越好 – 每新增一个索引,写入速度下降 10%~15%
- 通过
executionStats持续验证索引收益,动态调整策略
要点
- 索引提升读性能,代价是写延迟
- TTL索引删除存在延迟(异步线程)
- 唯一性约束对缺失字段的处理需特别注意
728

被折叠的 条评论
为什么被折叠?



