用索引让查询更快
🎯 引言
前面几篇我们一直在 user 集合上做增删改查,数据只有几条,怎么查都很快。但真实项目里一个集合可能有几十万、几百万条数据,同样一句 find,速度可能天差地别,差别往往就在索引上。
学完这篇文章,你能理解索引是什么、为什么它能让查询变快,学会创建、查看和删除索引,并能用 explain 验证索引是否真的生效。这是从「会用 MongoDB」走向「用好 MongoDB」的关键一步。
mongo:8)编写和验证,所有操作都在 mongosh 中完成。如果还没连上,先执行:docker exec -it mongo-demo mongosh -u root -p 123456 --authenticationDatabase admin
use blog_demo,继续沿用之前的 user 集合。🧱 为什么需要索引
我们先想清楚一个问题:执行 db.user.find({ username: '张三' }) 时,MongoDB 是怎么找到这条数据的?
没有索引时,它只能把集合里的文档一条一条拿出来看,挨个检查 username 是不是 '张三',直到全部看完为止。这种方式叫全集合扫描(COLLSCAN):集合里有多少条数据,就要检查多少条。
可以打个生活化的比喻:你想在一本书里找一句话。如果这本书没有目录,你只能从开头起逐页翻,书越厚翻得越慢;如果有目录,你先查目录定位到页码,直接翻过去就行了。索引就是 MongoDB 给集合建的「目录」。
_id 字段自动创建一个索引,所以按 _id 查询一直都很快。其他字段则不会自动建,需要我们手动创建。🛠 创建与查看索引
创建索引用 createIndex,参数是一个文档,指明给哪个字段建索引:
db.user.createIndex({ username: 1 });
这里的 1 表示按升序排列索引(-1 是降序)。对单个字段的查询来说,升序和降序都能用,初学阶段统一写 1 就好。
执行成功后会返回索引的名字 username_1:字段名加排序方向,是 MongoDB 自动起的名字。
查看当前集合有哪些索引,用 getIndexes():
db.user.getIndexes();
结果里能看到两个索引:一个是 _id 自带的 _id_,另一个就是我们刚建的 username_1。
如果某个索引不再需要,可以用 dropIndex 按名字删除:
db.user.dropIndex('username_1');
✨ 用 explain 验证索引是否生效
索引建了到底有没有用上?不能靠猜,MongoDB 提供了 explain 方法,可以把一次查询的执行计划展示出来。在查询后面接上 .explain('executionStats') 即可:
db.user.find({ username: '张三' }).explain('executionStats');
返回的结果很长,但初学者只看两个关键指标就够了,其他字段暂时不用管:
winningPlan里的stage:表示查询是怎么执行的。COLLSCAN是全集合扫描(没用上索引),IXSCAN是走了索引(用上了)。executionStats里的docsExamined:本次查询一共检查了多少条文档,数字越小说明扫描越少。
建议你自己动手做个对比实验:
- 先执行一次
explain,此时还没建索引,会看到stage是COLLSCAN,docsExamined等于集合里的文档总数。 - 执行
db.user.createIndex({ username: 1 })建好索引。 - 再执行一次同样的
explain,stage变成了IXSCAN,docsExamined明显变小。
数据量小的时候,两者的耗时差距可能感觉不出来,但 docsExamined 的变化是实打实的:走索引后 MongoDB 不再需要翻看整个集合,数据越多,这个优势越明显。
💡 唯一索引:让字段不允许重复
有些字段天然不允许重复,比如用户名、手机号。普通索引只管查询快,不管值重不重复;给 createIndex 加上 { unique: true },就能创建唯一索引,顺带把「不能重复」这个规则也交给数据库来保证:
db.user.createIndex({ username: 1 }, { unique: true });
建好之后再插入一个已经存在的 username,会直接报错:
db.user.insertOne({ username: '张三', age: 26, city: '北京' });
// 报错:E11000 duplicate key error
这个错误信息的意思是违反了唯一索引约束。它很适合用户名、手机号这类字段:与其在代码里先查一遍再插入,不如让数据库在底层直接拦住重复数据,既简单又可靠。
_id 上的索引本身就是唯一索引,所以每条文档的 _id 不会重复。🪤 索引不是越多越好
看到这里你可能会想:既然索引这么快,那把每个字段都建上索引不就行了?不行,因为索引是有代价的。
可以回到目录的比喻:书每增加一章内容,目录也要跟着更新。MongoDB 也一样,每次插入、修改、删除文档,所有相关的索引都要同步维护一遍。索引建得越多:
- 写入操作要维护的索引越多,写入速度会变慢。
- 索引本身要占用内存和磁盘空间,集合会变得更「重」。
正确的思路是:只为高频使用的查询条件建索引。比如用户登录要按 username 查,就给它建;某个字段一年也查不了几次,就没必要建。索引是「按需配置」的优化手段,不是「一键全开」的开关。
🧾 小节总结
- 没有索引时,MongoDB 会做全集合扫描(COLLSCAN),逐条检查所有文档;索引就像书的目录,能让查询直接定位。
- 每个集合的
_id字段自带索引,其他字段需要手动创建。 createIndex({ username: 1 })创建索引,getIndexes()查看索引,dropIndex('username_1')按名字删除索引。- 用
find(...).explain('executionStats')查看执行计划,重点看stage是否从 COLLSCAN 变成 IXSCAN,以及docsExamined是否明显变小。 createIndex({ username: 1 }, { unique: true })创建唯一索引,重复插入相同值会报错,适合用户名、手机号等字段。- 索引多了会拖慢写入、占用空间,只为高频查询条件建索引。
❓ 知识问答
Q1:createIndex({ username: 1 }) 里的 1 是什么意思?写 -1 会怎样?
1 表示升序,-1 表示降序,影响的是索引里值的排列方向。对单字段查询来说两者效果相当,写 1 即可;排序方向的影响主要体现在多字段组合排序的场景,初学阶段先不用深究。
Q2:建了索引之后,查询会自动走索引吗?
会。只要查询条件和索引字段匹配,MongoDB 会自动选择走索引,不需要在查询语句里做任何特殊声明。不确定时用 explain 看一下 stage 就知道了。
Q3:为什么我执行 explain 后 stage 还是 COLLSCAN?
常见原因有两个:索引还没建成功(用 getIndexes() 确认一下),或者查询条件里的字段和索引字段对不上(比如索引建在 username 上,查询条件用的却是 city)。
Q4:唯一索引和普通索引冲突吗?能同时建吗?
不冲突,但也不需要同时建。唯一索引本身就具备普通索引的查询加速能力,给同一个字段建一个唯一索引就够了。
Q5:删掉索引会把数据也删掉吗?
不会。索引只是数据的「目录」,删除索引只影响查询的查找方式,文档数据原封不动。真正删数据的是 deleteOne / deleteMany,别和 dropIndex 混淆。
🧪 小练习
练习一:给 user 集合的 city 字段创建一个升序索引,然后查看集合当前的所有索引,确认创建成功。
// 请在这里编写代码
练习二:用 explain('executionStats') 分别观察「按 username 查询」和「按 age 查询」的执行计划,说说两者的 stage 分别是什么、为什么不一样(假设只给 username 建了索引)。
// 请在这里编写代码
🎉 恭喜你已经掌握 MongoDB 索引技能啦!下一篇我们学习聚合管道,看看如何用 $match、$group 等阶段对数据做分组统计,让 MongoDB 直接帮你算出想要的结果。
