介绍
在本实验中,你将学习使用 MongoDB 索引来优化查询性能的基础知识。索引是一种特殊的数据结构,它包含集合数据中一小部分易于搜索的数据,从而使 MongoDB 能够比扫描整个集合更快地查找文档。
你将首先观察没有索引的查询性能。然后,你将创建单字段索引和复合索引,并了解它们如何显著提高查询和排序速度。最后,你将学习如何通过列出和删除索引来管理你的索引。在本实验结束时,你将对如何创建和使用索引来提高 MongoDB 应用程序的效率有实际的理解。
在本实验中,你将学习使用 MongoDB 索引来优化查询性能的基础知识。索引是一种特殊的数据结构,它包含集合数据中一小部分易于搜索的数据,从而使 MongoDB 能够比扫描整个集合更快地查找文档。
你将首先观察没有索引的查询性能。然后,你将创建单字段索引和复合索引,并了解它们如何显著提高查询和排序速度。最后,你将学习如何通过列出和删除索引来管理你的索引。在本实验结束时,你将对如何创建和使用索引来提高 MongoDB 应用程序的效率有实际的理解。
在创建索引之前,了解 MongoDB 在没有索引的情况下如何执行查询非常重要。在此步骤中,你将设置一个示例集合,运行一个查询,并分析其执行计划,以了解全集合扫描(full collection scan)的性能影响。
首先,打开 MongoDB Shell (mongosh) 与你的数据库进行交互。这个命令行界面允许你直接对你的 MongoDB 实例执行命令。
mongosh
进入 shell 后,你将看到 > 提示符。让我们切换到一个名为 indexlab 的新数据库,并向 users 集合中插入一些示例文档。如果数据库或集合不存在,MongoDB 会自动创建它们。
use indexlab
db.users.insertMany([
{ name: "Alice", age: 28, city: "New York" },
{ name: "Bob", age: 35, city: "San Francisco" },
{ name: "Charlie", age: 42, city: "Chicago" },
{ name: "David", age: 25, city: "New York" },
{ name: "Eve", age: 31, city: "San Francisco" }
]);
现在,让我们查找所有年龄大于 30 的用户。我们将使用 .explain("executionStats") 方法来查看 MongoDB 如何执行此查询。此方法提供了关于查询执行计划的详细统计信息。
db.users.find({ age: { $gt: 30 } }).explain("executionStats");
输出提供了关于查询执行的详细统计信息。查找 winningPlan 和 executionStats 部分。
示例输出(已截断):
{
"queryPlanner": {
"winningPlan": {
"stage": "COLLSCAN",
"filter": { "age": { "$gt": 30 } },
"direction": "forward"
}
},
"executionStats": {
"executionSuccess": true,
"nReturned": 3,
"executionTimeMillis": 0,
"totalKeysExamined": 0,
"totalDocsExamined": 5
}
}
这里的关键信息是 stage: "COLLSCAN" 和 totalDocsExamined: 5。
COLLSCAN 代表“集合扫描”(Collection Scan)。这意味着 MongoDB 必须检查集合中的每一个文档才能找到匹配查询的文档。totalDocsExamined: 5 确认了集合中的所有 5 个文档都被扫描了。虽然对于小型集合来说这很快,但对于包含数百万文档的集合进行集合扫描会非常慢。在下一步中,你将通过添加索引来解决这个问题。
既然你已经看到了集合扫描的低效性,让我们通过创建索引来提高性能。在 age 字段上的索引将允许 MongoDB 快速查找相关文档,而无需扫描整个集合。
你应该仍然处于上一步的 mongosh shell 中。
在 age 字段上创建一个升序索引。1 指定升序索引,而 -1 则指定降序索引。
db.users.createIndex({ age: 1 });
MongoDB 将确认索引已成功创建。此索引的默认名称将是 age_1。
现在,运行与上一步完全相同的查询并检查其执行计划。
db.users.find({ age: { $gt: 30 } }).explain("executionStats");
示例输出(已截断):
{
"queryPlanner": {
"winningPlan": {
"stage": "FETCH",
"inputStage": {
"stage": "IXSCAN",
"keyPattern": { "age": 1 },
"indexName": "age_1"
}
}
},
"executionStats": {
"executionSuccess": true,
"nReturned": 3,
"executionTimeMillis": 0,
"totalKeysExamined": 3,
"totalDocsExamined": 3
}
}
请注意执行计划中的显著变化:
stage 现在是 IXSCAN,代表“索引扫描”(Index Scan)。这表明 MongoDB 使用了 age_1 索引来查找匹配的文档。totalKeysExamined 和 totalDocsExamined 现在是 3,而不是 5。MongoDB 只需通过索引查看匹配查询的 3 个文档,而忽略了另外 2 个。这就是性能提升的来源。索引不仅用于加速查询,对于高效排序也至关重要。当你对未索引的字段进行排序时,MongoDB 必须在内存中执行排序,这可能会很慢并消耗大量 RAM。复合索引(包含多个字段)可以优化那些在这些字段上进行过滤和排序的查询。
让我们在 city(升序)和 age(降序)字段上创建一个复合索引。索引中字段的顺序对于其使用方式很重要。
db.users.createIndex({ city: 1, age: -1 });
现在,让我们运行一个按城市然后按年龄对用户进行排序的查询。我们将再次使用 .explain() 来确认索引用于排序。
db.users.find().sort({ city: 1, age: -1 }).explain("executionStats");
示例输出(已截断):
{
"queryPlanner": {
"winningPlan": {
"stage": "FETCH",
"inputStage": {
"stage": "IXSCAN",
"keyPattern": { "city": 1, "age": -1 },
"indexName": "city_1_age_-1"
}
}
}
}
IXSCAN 阶段表明 MongoDB 使用了我们新的 city_1_age_-1 索引。由于数据已根据我们的排序条件在索引中排序,MongoDB 无需执行单独的、成本高昂的内存排序步骤。
要查看实际的排序结果,请在不使用 .explain() 的情况下运行查询。
db.users.find().sort({ city: 1, age: -1 });
输出:
[
{ _id: ObjectId("..."), name: 'Charlie', age: 42, city: 'Chicago' },
{ _id: ObjectId("..."), name: 'Alice', age: 28, city: 'New York' },
{ _id: ObjectId("..."), name: 'David', age: 25, city: 'New York' },
{ _id: ObjectId("..."), name: 'Bob', age: 35, city: 'San Francisco' },
{ _id: ObjectId("..."), name: 'Eve', age: 31, city: 'San Francisco' }
]
文档首先按 city 按字母顺序排序,然后在每个城市内按 age 从大到小排序,这与复合索引的定义相匹配。
虽然索引可以提高读取性能,但它们并非没有成本。它们会占用存储空间,并为写入操作(插入、更新和删除)增加少量的开销。因此,定期审查和删除不再使用的索引是一个好习惯。
首先,你可以使用 getIndexes() 方法列出集合上的所有索引。
db.users.getIndexes();
输出:
[
{ "v": 2, "key": { "_id": 1 }, "name": "_id_" },
{ "v": 2, "key": { "age": 1 }, "name": "age_1" },
{ "v": 2, "key": { "city": 1, "age": -1 }, "name": "city_1_age_-1" }
]
这显示了 _id 字段上的默认索引(为每个集合自动创建),以及我们创建的两个索引。
假设我们已确定不再需要复合索引 city_1_age_-1。你可以使用 dropIndex() 方法将其删除,并将索引名称作为参数传递。
db.users.dropIndex("city_1_age_-1");
MongoDB 将返回一个对象,指示删除操作之前存在多少个索引。
{ "nIndexesWas": 3, "ok": 1 }
现在,通过再次列出索引来验证索引是否已被删除。
db.users.getIndexes();
输出:
[
{ "v": 2, "key": { "_id": 1 }, "name": "_id_" },
{ "v": 2, "key": { "age": 1 }, "name": "age_1" }
]
如你所见,city_1_age_-1 索引已不存在。适当的索引管理是维护健康且高性能数据库的关键部分。
要退出 MongoDB shell,你可以输入 exit 或按 Ctrl+D。
exit;
在本实验中,你学习了使用 MongoDB 索引的基本技术。你首先观察了没有索引的查询中的 COLLSCAN,并理解了其性能限制。然后,你创建了一个单字段索引,这使得查询计划变为更高效的 IXSCAN。
此外,你还探索了复合索引,并了解了如何使用它们来优化排序操作,避免了昂贵的内存排序。最后,你学会了如何通过 getIndexes() 列出索引以及使用 dropIndex() 删除未使用的索引来管理你的索引。这些技能是使用 MongoDB 构建快速可扩展应用程序的基础。