使用 MongoDB 索引

MongoDBBeginner
立即练习

介绍

在本实验中,你将学习使用 MongoDB 索引来优化查询性能的基础知识。索引是一种特殊的数据结构,它包含集合数据中一小部分易于搜索的数据,从而使 MongoDB 能够比扫描整个集合更快地查找文档。

你将首先观察没有索引的查询性能。然后,你将创建单字段索引和复合索引,并了解它们如何显著提高查询和排序速度。最后,你将学习如何通过列出和删除索引来管理你的索引。在本实验结束时,你将对如何创建和使用索引来提高 MongoDB 应用程序的效率有实际的理解。

这是一个实验(Guided Lab),提供逐步指导来帮助你学习和实践。请仔细按照说明完成每个步骤,获得实际操作经验。根据历史数据,这是一个 初级 级别的实验,完成率为 100%。获得了学习者 100% 的好评率。

无索引查询

在创建索引之前,了解 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");

输出提供了关于查询执行的详细统计信息。查找 winningPlanexecutionStats 部分。

示例输出(已截断):

{
  "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 索引来查找匹配的文档。
  • totalKeysExaminedtotalDocsExamined 现在是 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 构建快速可扩展应用程序的基础。