01. 索引基础——单字段与复合索引
MongoDB 的索引原理跟 SQL 差不多——B 树结构,加速查询。创建索引用 createIndex,第一个参数是索引字段和方向(1 升序、-1 降序),第二个参数是选项。
_id 字段自动有一个唯一索引。其他字段需要手动建——一般在查询频繁的字段上建索引。
单字段索引:createIndex({ field: 1 })。复合索引:createIndex({ a: 1, b: -1 }),支持最左前缀匹配(跟 SQL 的联合索引一样)。
索引方向在单字段索引里不重要(MongoDB 可以反向遍历),在复合索引里才有关键影响——排序时如果索引方向匹配就不用额外排序。
javascript
// 单字段索引
db.users.createIndex({ email: 1 });
// 复合索引
db.users.createIndex({ city: 1, age: -1 });
// 唯一索引
db.users.createIndex({ email: 1 }, { unique: true });
// 查看集合的所有索引
db.users.getIndexes();02. 索引类型——TTL、文本、地理空间
MongoDB 支持多种索引类型,远超 B 树索引:
TTL 索引——给日期字段建索引,指定过期时间。到期后 MongoDB 自动删除文档。适合存临时数据、会话信息、验证码。后台每 60 秒运行一次清理任务。
文本索引——对字符串内容做全文搜索。一个集合只能有一个文本索引但可以包含多个字段。支持多语言分词(中文需要指定语言或装中文分词插件)。搜索时用美元 text 操作符。
地理空间索引——2dsphere 索引支持地理空间查询,能查出附近的点、在某个区域内的点。外卖、打车类应用的基础。
哈希索引——对字段的哈希值建索引,只支持等值查询。用在分片集群里让数据均匀分布。
javascript
// TTL 索引(60 秒后自动删除)
db.sessions.createIndex(
{ created_at: 1 },
{ expireAfterSeconds: 60 }
);
// 文本索引
db.articles.createIndex({ title: "text", content: "text" });
db.articles.find({ $text: { $search: "mongodb tutorial" } });
// 地理空间索引
db.restaurants.createIndex({ location: "2dsphere" });
db.restaurants.find({
location: {
$near: {
$geometry: { type: "Point", coordinates: [116.4, 39.9] },
$maxDistance: 5000
}
}
});TTL 索引的清理程序每 60 秒跑一次,不是精确到秒的。如果需要严格定时删除,不要在业务逻辑里依赖它。
03. 索引覆盖查询
跟 SQL 的覆盖索引一个道理——如果查询需要的所有字段都包含在索引里了,MongoDB 就不用去查实际的文档数据,直接从索引返回结果,速度快很多。
判断是否覆盖:explain 的结果里 totalDocsExamined 为 0 且没有 FETCH 阶段,说明数据全从索引拿了。
要实现覆盖查询,投影里只选索引中包含的字段,而且不能返回下划线 id(除非下划线 id 也在索引里)。因为下划线 id 默认返回,如果不 index 它就会触发回表。
但别为了覆盖查询建太宽的索引——索引越大磁盘越多、写入越慢。只对高频查询做覆盖优化。
javascript
// 建覆盖索引
db.orders.createIndex({ user_id: 1, amount: 1 });
// 覆盖查询(只查索引里的字段,且 _id 不返回)
db.orders.find(
{ user_id: ObjectId("...") },
{ user_id: 1, amount: 1, _id: 0 }
);
// 验证是否覆盖
db.orders.find(
{ user_id: ObjectId("...") },
{ user_id: 1, amount: 1, _id: 0 }
).explain("executionStats");覆盖查询要求投影的字段全部在索引里,而且 _id 要么包含在索引里要么显式排除。
04. 索引策略与 explain
explain 是索引优化的神器。三种模式:queryPlanner(仅执行计划,默认)、executionStats(执行计划加实际统计)、allPlansExecution(所有候选计划的对比)。生产环境用 executionStats 最实用。
解读 explain 的关键字段:winningPlan.stage——IXSCAN 表示走了索引,COLLSCAN 表示全表扫描。totalDocsExamined——实际扫了多少文档。nReturned——返回了多少。executionTimeMillis——执行毫秒数。
索引设计原则:
覆盖查询频率高的字段
等值查询字段放复合索引前面,范围查询字段放后面
排序字段最好在索引里(避免内存排序)
经常一起查询的字段建复合索引
用 explain 验证索引是否真的被用了
javascript
// 详细执行计划
db.orders.find({ user_id: ObjectId("...") }).explain("executionStats");
// 关键指标
// winningPlan.inputStage.stage: "IXSCAN" 或 "COLLSCAN"
// executionStats.totalDocsExamined
// executionStats.nReturned
// executionStats.executionTimeMillistotalDocsExamined / nReturned 比值越大说明索引效率越低。理想情况这个比值接近 1。
05. 索引维护与管理
索引不是建了就一劳永逸的——需要定期维护和监控。
查看索引使用情况:用美元 indexStats 聚合管道看每个索引的访问次数,找出不再使用的索引(它们是写操作的累赘)。
重建索引:随着文档增删改,索引可能出现碎片。用 reIndex() 重建索引能回收空间、提升性能。但重建期间会锁集合,生产环境要谨慎计划。
隐藏索引:用 hideIndex() 隐藏某个索引,查询不会用它但索引还在。适合做大胆的测试——先隐藏看查询有没有变慢,确认没事再删。
删除索引:dropIndex() 或 dropIndexes()。删除不需要的索引能提升写性能。
javascript
// 查看索引使用统计
db.orders.aggregate([{ $indexStats: {} }]);
// 隐藏索引(测试用)
db.users.hideIndex("email_1");
// 取消隐藏
db.users.unhideIndex("email_1");
// 删索引
db.users.dropIndex("email_1");
// 删所有非 _id 索引
db.users.dropIndexes();dropIndexes() 删除所有索引(除了 _id),不是 dropIndex(一个)。看清楚了再执行。
知识测验
第 1/5 题正确 0
TTL 索引的作用是?
下一节
下一节 副本集