Skip to content

Latest commit

 

History

History
811 lines (490 loc) · 44.5 KB

02.md

File metadata and controls

811 lines (490 loc) · 44.5 KB

二、命令行操作和索引

在本章中,我们将介绍以下主题:

  • 创建测试数据
  • 从 Mongo shell 执行简单的查询、投影和分页
  • 更新和删除 shell 中的数据
  • 创建索引并查看查询计划
  • 在 shell 中创建背景和前景索引
  • 创建和理解稀疏索引
  • 使用 TTL 索引在固定时间间隔后过期文档
  • 使用 TTL 索引在给定时间过期文档

导言

在本章中,我们将使用 mongo shell 执行简单的查询。在本章后面,我们将详细介绍常用的 MongoDB 索引。

创建测试数据

本配方旨在为本章中的一些配方以及本书后面的章节创建测试数据。我们将演示如何使用 mongo 导入实用程序在 mongo 数据库中加载 CSV 文件。这是一个基本的配方,如果读者知道数据导入实用程序;他们可以从 Packt 网站(pincodes.csv上下载 CSV 文件,自己将其加载到收藏中,然后跳过剩下的食谱。我们将使用默认数据库test,集合将命名为postalCodes

准备好了吗

这里使用的数据是印度的邮政编码。从 Packt 网站下载pincodes.csv文件。该文件为 CSV 文件,包含 39732 条记录;成功导入时,它应创建 39732 个文档。我们需要让 Mongo 服务器启动并运行。有关如何启动服务器的说明,请参阅第 1 章安装和启动服务器中的安装单节点 MongoDB配方。服务器应开始侦听默认端口27017上的连接。

怎么做…

  1. 将要导入的文件放在当前目录下,从 shell 执行以下命令:

    $ mongoimport --type csv -d test -c postalCodes --headerline --drop pincodes.csv
  2. 通过在命令提示符下键入mongo来启动 mongo shell。

  3. 在 shell 中,执行以下命令:

    > db.postalCodes.count()

它是如何工作的…

假设服务器已启动并正在运行,CSV 文件已下载并保存在本地目录中,我们在该目录中使用当前目录中的文件执行导入实用程序。让我们看看mongoimport实用程序中给出的选项及其含义:

|

命令行选项

|

描述

| | --- | --- | | --type | 此指定输入文件的类型为 CSV。默认为 JSON;另一个可能的值是 TSV。 | | -d | 这是将加载数据的目标数据库。 | | -c | 这是前面提到的数据库中的集合,数据将在其中加载。 | | --headerline | 此仅在 TSV 或 CSV 文件的情况下相关。它表示文件的第一行是头。相同的名称将用作文档中字段的名称。 | | --drop | 在导入数据之前删除集合。 |

给出所有选项后,命令提示符上的最终值是文件名pincodes.csv

如果导入成功,您将在控制台上看到类似于以下内容的内容:

2015-05-19T06:51:54.131+0000	connected to: localhost
2015-05-19T06:51:54.132+0000	dropping: test.postalCodes
2015-05-19T06:51:54.810+0000	imported 39732 documents

最后,我们启动 mongo shell 并查找集合中文档的数量;正如前面的导入日志所示,它实际上应该是 39732。

邮政编码数据取自https://github.com/kishorek/India-Codes/ 。这些数据不是从官方来源获取的,可能不准确,因为这些数据是为免费公共使用而手动编译的。

另见

从 Mongo shell 配方执行简单查询、投影和分页的是关于对导入的数据执行一些基本查询。

从 Mongo shell 执行简单的查询、投影和分页

在这个配方中,我们将通过一点查询来从我们在上一个配方中设置的测试数据中选择文档,创建测试数据。这个配方中没有什么奢侈的东西,精通查询语言基础知识的人可以跳过这个配方。其他对基本查询不太满意的人或希望得到一点复习的人可以继续阅读食谱的下一部分。此外,此配方旨在从先前配方中获得测试数据设置的感觉。

准备好了吗

要执行简单的查询,我们需要一个服务器启动并运行。我们需要一个简单的单节点。有关如何启动服务器的说明,请参阅第一章安装和启动服务器中的安装单节点 MongoDB配方。我们将要操作的数据需要导入数据库。导入数据的步骤在前面的配方创建测试数据中给出。您还需要启动 mongo shell 并连接到本地主机上运行的服务器。一旦这些先决条件完成,我们就可以开始了。

怎么做…

  1. 让我们首先查找集合中的文档计数:

    > db.postalCodes.count()
  2. 让我们从postalCodes集合中找到一个文档,如下所示:

    > db.postalCodes.findOne()
  3. 现在,我们在集合中发现多个文档如下:

    > db.postalCodes.find().pretty()
  4. 前面的查询检索前 20 个文档的所有键,并将它们显示在 shell 上。在结果的末尾,您会注意到一行,上面写着Type "it" for more。通过键入"it",mongo shell将迭代生成的光标。现在让我们做几件事;我们将只显示citystatepincode字段。此外,我们希望显示集合中编号为 91 到 100 的文档。让我们看看我们是如何做到这一点的:

    > db.postalCodes.find({}, {_id:0, city:1, state:1, pincode:1}).skip(90).limit(10)
  5. 让我们向前迈进一步,编写一个稍微复杂的查询,在这里我们找到古吉拉特邦按城市名称排序的前 10 个城市,与上一个查询类似,我们只需选择citystatepincode字段:

    > db.postalCodes.find({state:'Gujarat'},{_id:0, city:1, state:1, pincode:1}).sort({city:1}).limit(10)

它是如何工作的…

这个配方非常简单,可以让我们感受到在上一个配方中设置的测试数据。然而,和其他食谱一样,我确实应该为我们在这里所做的事情向你们所有人解释一下。

我们首先使用db.postalCodes.count()找到了收藏中的文件数量,它应该给我们 39732 份文件。这应该与我们在导入邮政编码集合中的数据时看到的日志同步。接下来,我们使用findOne从集合中查询了一个文档。此方法返回查询结果集中的第一个文档。在没有查询或排序顺序的情况下(如本例中),它将是集合中按自然顺序排序的第一个文档。

接下来,我们执行find而不是findOne。两者的区别在于,find操作返回结果集的迭代器,我们可以使用它遍历 find 操作的结果,而findOne返回一个文档。向find操作添加一个 pretty 方法调用将以 pretty 或格式化的方式打印结果。

提示

请注意,pretty方法是有意义的,它只适用于find而不适用于findOne。这是因为findOne的返回值是一张单据,返回的单据没有pretty操作。

现在,我们将在 mongo shell 上执行以下查询:

> db.postalCodes.find({}, {_id:0, city:1, state:1, pincode:1}).skip(90).limit(10) 

这里我们将两个参数传递给的find方法:

  • 第一个是{},这是选择文档的查询,在中,我们要求 mongo 选择所有文档。
  • 第二个参数是我们希望在结果文档中使用的字段集,也称为投影。请记住,_id字段默认存在,除非我们明确表示_id:0。对于所有其他字段,我们需要说<field_name>:1<field_name>:true。带有投影的 find 部分与在关系世界中表示select field1``, field2 from table相同,在关系世界中不指定要在 find 中选择的字段表示select * from table

接下来,我们只需要看看skiplimit做了什么:

  • skip函数从结果集一直跳过给定数量的文档,直到结束文档
  • 然后,limit函数将结果限制为给定数量的文档

让我们用一个例子来看看这一切意味着什么。通过这样做。skip(90).limit(10),我们说要从结果集中跳过第一个90文档,从第 91 个文档开始返回。但是,限制规定我们只返回第 91 个文档中的10文档。

现在,我们需要知道一些边界条件。如果 skip 的值大于集合中的文档总数,该怎么办?那么在这种情况下呢,就不会退回任何文件了。此外,如果提供给 limit 函数的数量大于集合中剩余的实际文档数量,则返回的文档数量将与集合中剩余的文档数量相同,并且在这两种情况下都不会引发异常。

更新和删除外壳中的数据

这同样是一个简单的配方,将在测试集合上执行删除和更新。我们将不会处理我们作为导入的相同测试数据。我们不想更新/删除任何测试数据,但相反,我们将只处理为此配方创建的测试集合。

准备好了吗

对于这个配方,我们将创建一个名为updAndDelTest的集合。我们将要求服务器启动并运行。有关如何启动服务器的说明,请参阅第 1 章安装和启动服务器中的安装单节点 MongoDB配方。在加载UpdAndDelTest.js脚本的情况下启动 shell。此脚本将在 Packt 网站上下载。要知道如何使用预加载的脚本启动 shell,请参阅第一章安装和启动服务器中的使用 JavaScript连接 Mongo shell 中的单个节点。

怎么做…

  1. 启动 MongoDB shell 并预加载脚本:

    $ mongo --shell updAndDelTest.js
  2. 启动 shell 并加载脚本后,在 shell 中执行以下操作:

    > prepareTestData()
  3. 如果一切顺利,您应该会看到控制台上打印的Inserted 20 documents in updAndDelTest

  4. 为了了解这个集合,让我们查询如下:

    > db.updAndDelTest.find({}, {_id:0})
  5. 我们应该看到,对于x的每个值12,对于x的每个值,y从 1 增加到 10。

  6. 我们将首先更新一些文件并观察结果。执行以下更新:

    > db.updAndDelTest.update({x:1}, {$set:{y:0}})
  7. 执行以下find命令,观察结果;我们应该得到 10 份文件。对于每一个,请注意y的值。

    > db.updAndDelTest.find({x:1}, {_id:0})
  8. 我们现在将执行以下更新:

    > db.updAndDelTest.update({x:1}, {$set:{y:0}}, {multi:true})
  9. 再次执行步骤 6 中给出的查询以查看更新的文档。它将显示我们之前看到的相同文档。再次记下y的值,并将它们与我们上次在执行步骤 7 中给出的更新之前执行此查询时看到的结果进行比较。

  10. 现在我们将了解删除是如何工作的。我们将再次选择x1的文档进行删除测试。让我们从集合

```js
> db.updAndDelTest.remove({x:1})

```

中删除`x`为`1`的所有文档:
  1. Execute the following find command and observe the results. We will not get any results. It seems that the remove operation has removed all the documents with x as 1.
```js
> db.updAndDelTest.find({x:1}, {_id:0})

```

### 注

当您在 mongoshell 中并且希望查看函数的源代码时,只需输入函数名,不带括号。例如,在此配方中,我们可以通过键入函数名`prepareTestData`(不带括号)查看自定义函数的代码,然后按*Enter*。

它是如何工作的…

首先,我们设置用于更新和删除test的数据。我们已经看到了数据,知道它是什么。值得注意的一件有趣的事情是,当我们执行一个更新(如db.updAndDelTest.update({x:1}, {$set:{y:0}}))时,它只更新与作为第一个参数提供的查询匹配的第一个文档。这是我们在更新后查询集合时将观察到的情况。更新功能的格式如下db.<collection name>.update(query, update object, {upsert: <boolean>, multi:<boolean>})

我们将在后面的食谱中看到 upsert 是什么。多参数默认设置为false。这意味着update方法不会更新多个文档;只会更新第一个匹配的文档。但是,当我们将 multi 设置为true时,集合中与给定查询匹配的所有文档都会更新。这是查询集合后可以验证的内容。

另一方面,删除的行为不同。默认情况下,remove操作会删除与提供的查询匹配的所有文档。但是,如果我们只想删除一个文档,则显式地将第二个参数传递为true

更新和删除的默认行为不同。update调用默认只更新第一个匹配文档,而remove删除所有匹配查询的文档。

创建查询索引和查看计划

在这个配方中,我们将查看数据查询,通过解释查询计划来分析其性能,然后通过创建索引来优化它。

准备好了吗

为了创建索引,我们需要启动并运行一台服务器。我们需要一个简单的单节点。有关如何启动服务器的说明,请参阅第一章安装和启动服务器中的安装单节点 MongoDB配方。我们将要处理的数据需要导入数据库。导入数据的步骤在前面的配方创建测试数据中给出。一旦这个先决条件完成,我们就可以开始了。

怎么做…

我们正在尝试编写一个查询,以查找给定状态下的所有邮政编码。

  1. Execute the following query to view the plan of this query:

    > db.postalCodes.find({state:'Maharashtra'}).explain('executionStats')

    注意解释计划操作结果中的stagenReturnedtotalDocsExamineddocsExaminedexecutionTimeMillis字段。

  2. 让我们再次执行相同的查询,但这次我们将结果限制为仅 100 个结果:

    > db.postalCodes.find({state:'Maharashtra'}).limit(100).explain()
  3. 请注意结果中的以下字段:nReturnedtotalDocsExamineddocsExaminedexecutionTimeMillis

  4. 我们现在在statepincode字段上创建一个索引,如下所示:

    > db.postalCodes.createIndex({state:1, pincode:1})
  5. Execute the following query:

    > db.postalCodes.find({state:'Maharashtra'}).explain()

    请注意结果中的以下字段:stagenReturnedtotalDocsExamineddocsExaminedexecutionTimeMillis

  6. As we want the pincodes only, we modify the query as follows and view its plan:

    > db.postalCodes.find({state:'Maharashtra'}, {pincode:1, _id:0}).explain()

    请注意结果中的以下字段:stagenReturnedtotalDocsExamineddocsExaminedexecutionTimeMillis

它是如何工作的…

这里有很多要解释的。我们将首先讨论我们刚刚做了什么,以及如何分析统计数据。接下来,我们将讨论创建索引时需要记住的一些要点和一些注意事项。

分析计划

好的,让我们看第一步,分析我们执行的输出:

db.postalCodes.find({state:'Maharashtra'}).explain()

我的机器上的输出如下:(我现在跳过不相关的字段。)

{
        "stage" : "COLLSCAN",
...
        "nReturned" : 6446,
        "totalDocsExamined " : 39732, 
          
    "docsExamined" : 39732, 
          

        "executionTimeMillis" : 12,}

结果中的stage字段的值为COLLSCAN,这意味着为了在整个集合中搜索匹配的文档,已经进行了完整集合扫描(一个接一个地扫描所有文档)。nReturned值为6446,是匹配查询的结果数。totalDocsExamineddocsExamined字段的值为39,732,这是为检索结果而扫描的集合中的文档数。这也是集合中存在的文档总数,所有文档都已扫描结果。最后,executionTimeMillis是检索结果所用的毫秒数。

提高查询执行时间

到目前为止,查询的性能看起来不太好,还有很大的改进空间。为了演示应用于查询的限制如何影响查询计划,我们可以在不使用索引但使用 limit 子句的情况下再次找到查询计划,如下所示:

> db.postalCodes.find({state:'Maharashtra'}).limit(100).explain()

{
 "stage" : "COLLSCAN",
 "nReturned" : 100,
 "totalDocsExamined" : 19951,

 
 "docsExamined" : 19951,
 
 "executionTimeMillis" : 8,}

这次的查询计划很有趣。尽管我们还没有创建索引,但我们确实看到查询执行所需的时间和检索结果所扫描的对象数量有所改进。这是因为一旦达到limit函数中指定的文档数量,mongo 就会忽略对剩余文档的扫描。因此,我们可以得出结论,建议您使用limit函数来限制您的结果数量,因为预先知道访问的最大文档数量。这可能会提供更好的查询性能。单词may很重要,因为在没有索引的情况下,如果不满足匹配数,则可能仍然会对集合进行完全扫描。

利用指标进行改进

接着,我们在 state 和 pincode 字段上创建一个复合索引。在这种情况下,索引的顺序是递增的(因为值是 1),除非我们计划执行多键排序,否则索引的顺序并不重要。这是一个决定因素,决定结果是否可以仅使用索引进行排序,还是 mongo 需要在稍后返回结果之前在内存中对其进行排序。就查询计划而言,我们可以看到有一个显著的改进:

{
"executionStages" : {
 "stage" : "FETCH",

"inputStage" : {
 "stage" : "IXSCAN",

 "nReturned" : 6446,
 "totalDocsExamined" : 6446,
 "docsExamined" : 6446,
 
 "executionTimeMillis" : 4,}

inputStage字段现在有IXSCAN值,这表明现在确实使用了索引。结果的数量与预期一样,保持在6446相同。索引中扫描的对象数和集合中扫描的文档数现在已减少到与结果中相同的文档数。这是因为我们现在使用了一个索引,该索引为我们提供了要扫描的起始文档,只有在该索引中,才会扫描所需数量的文档。这类似于使用书的索引查找单词或扫描整本书来搜索单词。正如所料,executionTimeMillis中的时间也减少了。

使用覆盖索引进行改进

这就给了我们一个字段executionStages,即FETCH,我们将看到这意味着什么。要知道这个值是什么,我们需要简单地看一下索引是如何运行的。

索引存储集合中原始文档的字段子集。索引中的字段与在其上创建索引的字段相同。但是,字段在索引中按索引创建期间指定的顺序进行排序。除了字段之外,索引中还存储了一个附加值,用作指向集合中原始文档的指针。因此,每当用户执行查询时,如果查询中包含索引所在的字段,则会参考索引以获得一组匹配项。指针存储在与查询匹配的索引项中,然后用于执行另一个 IO 操作,从集合中获取完整的文档,然后返回给用户。

executionStages的值为FETCH,表示用户在查询中请求的数据并不完全存在于索引中,但需要额外的 IO 操作,从索引指针后面的集合中检索整个文档。如果索引本身中存在值,则无需执行额外的操作从集合中检索文档,并返回索引中的数据。这称为覆盖指数,executionStages的值在本例中为IXSCAN

在我们的例子中,我们只需要夹点。那么,为什么不在查询中使用投影来检索我们所需要的内容呢?这也会使索引被覆盖,因为索引条目只有状态名称和 pincode,并且可以完全提供所需的数据,而无需从集合中检索原始文档。本例中的查询计划也很有趣。

执行以下命令:

db.postalCodes.find({state:'Maharashtra'}, {pincode:1, _id:0}).explain()

这给了我们以下计划:

{
"executionStages" : {
 "stage" : "PROJECTION",

"inputStage" : {
 "stage" : "IXSCAN",

 "nReturned" : 6446,
 "totalDocsExamined" : 0,
 "totalKeysExamined": 6446
 "executionTimeMillis" : 4,}

totalDocsExaminedexecutionStage: PROJECTION字段的值值得观察。正如预期的那样,我们在预测中要求的数据可以仅从索引中获得。在本例中,我们扫描了索引中的 6446 个条目,因此,totalKeysExamined值为6446

由于整个结果是从索引中获取的,因此我们的查询没有从集合中获取任何文档。因此,totalDocsExamined的值为0

由于这个集合很小,我们看不到查询执行时间的显著差异。这将在更大的收藏中更加明显。利用指标是很好的,给了我们一个很好的性能。利用覆盖指数给我们带来了更好的表现。

MongoDB 的 explain results 功能在 3.0 版中进行了重大改进。我建议花几分钟的时间在浏览它的文档 http://docs.mongodb.org/manual/reference/explain-results/

另一件需要记住的事情是,如果您的文档有很多字段,请尝试使用投影来检索我们需要的字段数。_id字段默认每次都取。除非我们计划使用它,否则如果它不是索引的一部分,请将_id:0设置为不检索它。执行覆盖查询是查询集合的最有效方法。

指数创建的一些注意事项

现在我们将看到索引创建中的一些陷阱,以及在索引中使用数组字段时的一些事实。

一些没有有效使用索引的操作符是$where$nin$exists操作符。无论何时在查询中使用这些运算符,都应该记住,当数据大小增加时,可能会出现性能瓶颈。

类似地,$in运算符必须优先于$or运算符,因为两者都可以实现或多或少相同的结果。作为练习,试着在postalCodes收藏中找到马哈拉施特拉邦和古吉拉特邦的枕形符号。编写两个查询:一个使用$or和一个使用$in运算符。解释这两个查询的计划。

在索引中使用数组字段时会发生什么情况?

Mongo 为文档数组字段中的每个元素创建一个索引项。因此,如果文档中的数组中有 10 个元素,那么将有 10 个索引项,数组中的每个元素对应一个索引项。但是,在创建包含数组字段的索引时存在一个约束。使用多个字段创建索引时,一个数组类型的字段不能超过一个,这样做是为了防止在索引中使用的数组中添加一个元素时,索引数量可能会激增。如果我们仔细考虑,就会为数组中的每个元素创建一个索引项。如果允许数组类型的多个字段作为索引的一部分,那么索引中将有大量的条目,这将是这些数组字段长度的乘积。例如,如果允许使用两个数组字段创建一个索引,则添加了两个数组字段(每个字段长度为 10)的文档将向索引中添加 100 个条目。

这应该是足够好的,现在,刮一个普通的,香草指数的表面。我们将在以下几个食谱中看到更多选项和类型。

在 shell 中创建背景和前景索引

在前面的方法中,我们研究了如何分析查询,如何决定需要创建什么索引,以及如何创建索引。这本身就很简单,看起来也相当简单。然而,对于大型集合,随着索引创建时间的增加,情况开始变得更糟。这个方法的目的是在创建索引时,特别是在大型集合上,对这些概念有一些了解,并避免这些陷阱。

准备好了吗

为了创建索引,我们需要启动并运行一台服务器。我们需要一个简单的单节点。有关如何启动服务器的说明,请参阅第一章安装和启动服务器中的安装单节点 MongoDB配方。

只需在操作系统外壳中键入mongo,即可开始将两个外壳连接到服务器。默认情况下,它们都将连接到test数据库。

我们的邮政编码测试数据太小,无法证明在大型集合上创建索引时所面临的问题。我们需要更多的数据,因此,我们将首先创建一些数据来模拟索引创建过程中的问题。这些数据没有实际意义,但足以检验这些概念。在一个已启动的 shell 中复制以下片段并执行:(这是一个很容易键入的代码段。)

for(i = 0; i < 5000000 ; i++) {
  doc = {}
  doc._id = i
  doc.value = 'Some text with no meaning and number ' + i + ' in between'
  db.indexTest.insert(doc)
}

此集合中的文档外观如下所示:

{ _id:0, value:"Some text with no meaning and number 0 in between" }

执行死刑需要相当长的时间,所以我们需要耐心。一旦执行结束,我们都准备好行动了。

如果您想知道集合中加载的文档的当前数量,请定期从第二个 shell 中评估以下内容:

db.indexTest.count()

怎么做…

  1. 在文档的value字段上创建索引,如下所示:

    > db.indexTest.createIndex({value:1})
  2. 在索引创建过程中(这需要相当长的时间),切换到第二个控制台并执行以下操作:

    > db.indexTest.findOne()
  3. 索引创建 shell 和我们执行findOne的 shell 都会被阻塞,直到索引创建完成,才会显示提示。

  4. 现在,默认情况下,这是前台索引创建。我们希望看到后台索引创建中的行为。删除创建的索引,如下所示:

    > db.indexTest.dropIndex({value:1})
  5. 再次创建索引,但这次是在后台,如下所示:

    > db.indexTest.createIndex({value:1}, {background:true})
  6. 在第二个 mongo shell 中执行findOne如下:

    > db.indexTest.findOne()
  7. 这将返回一个文档,这与第一个实例不同,在第一个实例中,操作被阻止,直到在前台完成索引创建。

  8. 在第二个 shell 中,在每次解释计划调用之间以 4 到 5 秒的间隔重复执行以下解释操作,直到索引创建过程完成:

    > db.indexTest.find({value:"Some text with no meaning and number 0 in between"}).explain()

它是如何工作的…

现在让我们分析一下我们刚才做了什么。我们创建了大约 500 万个没有实际意义的文档,但我们只是希望获得一些数据,这些数据将花费大量时间来构建索引。

索引可以用两种方式建立,前台和后台。在这两种情况下,shell 在createIndex操作完成之前不会显示提示,并将阻止所有操作,直到创建索引为止。为了说明前台和后台索引创建之间的区别,我们执行了第二个 mongo shell。

我们首先在前台创建索引,这是默认行为。在构建索引之前,这个索引构建不允许我们(从第二个 shell)查询集合。在返回结果之前,findOne操作被阻止,直到构建完整个索引(从第一个外壳开始)。另一方面,在后台构建的索引并没有阻止findOne操作。如果您想在建立索引时尝试将新文档插入到集合中,这应该可以很好地工作。您可以随意删除索引并在后台重新创建它,同时将文档插入到indexTest集合中,您会注意到它工作得很顺利。

那么,这两种方法之间的区别是什么?为什么不总是在后台构建索引?除了一个额外的参数,{background:true}(也可以是{background:1})作为第二个参数传递给createIndex调用之外,没有什么区别。在后台创建索引的过程将略慢于在前台创建的索引。此外,尽管与最终用户无关,但在前台创建的索引将比在后台创建的索引更紧凑。

除此之外,不会有显著差异。事实上,如果系统正在运行,并且在为最终用户服务时需要创建索引(不推荐,但有时可能会出现需要在实时系统中创建索引的情况),那么在后台创建索引是唯一的方法。还有其他执行此类管理活动的策略,我们将在管理部分的一些食谱中看到。

更糟糕的是,mongo 在索引创建期间获取的锁不在集合级别,而是在数据库级别。为了解释这意味着什么,我们必须删除indexTest集合上的索引,并执行以下小练习:

  1. 首先,通过执行以下命令从 shell 在前台创建索引:

    > db.indexTest.createIndex({value:1})
  2. 现在,在 person 集合中插入一个文档,该文档在测试数据库中可能存在,也可能不存在,如下所示:

    > db.person.insert({name:'Amol'})

我们将看到,在indexTest集合上的索引创建过程中,person 集合中的此插入操作将被阻止。但是,如果在建立索引期间对不同数据库中的集合执行此插入操作,则它将正常执行而不会阻塞。(您也可以尝试一下。)这清楚地表明锁是在数据库级别获取的,而不是在集合或全局级别获取的。

在 mongo 的版本 2.2 之前,锁处于全局级别,即 mongod 进程级别,而不是我们之前看到的数据库级别。在处理早于 2.2 版的 mongo 发行版时,您需要记住这一事实。

创建和理解稀疏索引

无模式设计是 Mongo 的基本特征之一。这允许集合中的文档具有完全不同的字段,某些文档中存在某些字段,而其他文档中不存在这些字段。换句话说,这些字段可能是稀疏的,这可能已经为您提供了关于什么是稀疏索引的线索。在这个配方中,我们将创建一些随机测试数据,并查看稀疏索引相对于正常索引的行为。我们将看到使用稀疏索引的优点和一个主要缺陷。

准备好了吗

对于这个配方,我们需要创建一个名为sparseTest的集合。我们将需要一个服务器来启动和运行。有关如何启动服务器的说明,请参阅第 1 章安装和启动服务器中的安装单节点 MongoDB配方。在加载SparseIndexData.js脚本的情况下启动 shell。此脚本可在 Packt 网站上下载。要知道如何使用预加载的脚本启动 shell,请参阅第一章安装和启动服务器中的使用 JavaScript连接 Mongo shell 中的单个节点。

怎么做…

  1. 通过调用以下命令加载集合中的数据。这将导入sparseTest集合中的 100 个文档。

    > createSparseIndexData()
  2. 现在,通过执行以下查询查看数据,注意前几个结果中的y字段:

    > db.sparseTest.find({}, {_id:0})
  3. 我们可以看到,y字段不存在,或者如果存在,则它是唯一的。然后执行以下查询:

    > db.sparseTest.find({y:{$ne:2}}, {_id:0}).limit(15)
  4. 记下结果;它既包含符合条件的文档,也包含不包含给定字段y的字段。

  5. As the value of y seems unique, let's create a new unique index on the y field as follows:

    > db.sparseTest.createIndex({y:1}, {unique:1})

    这会引发一个错误,抱怨该值不是唯一的,并且有问题的值是 null 值。

  6. 我们将通过如下方式使此索引稀疏来解决此问题:

    > db.sparseTest.createIndex({y:1}, {unique:1, sparse:1})
  7. This should fix our problem. To confirm that the index got created, execute the following on the shell:

    > db.sparseTest.getIndexes()

    这应该显示两个索引,_id上的默认索引和我们在前面步骤中刚刚创建的索引。

  8. 现在,再次执行我们前面在步骤 3 中执行的查询,并查看结果。

  9. 查看结果,并将其与创建索引之前看到的结果进行比较。重新执行查询,但有以下提示强制执行完整集合扫描:

    >db.sparseTest.find({y:{$ne:2}},{_id:0}).limit(15).hint({$natural:1})
  10. 观察结果。

它是如何工作的…

我们刚才执行了很多步骤。现在,我们将深入挖掘并解释查询使用稀疏索引的集合时所看到的奇怪行为的内部结构和原因。

我们使用 JavaScript 方法创建的测试数据只是创建了一个带有键x的文档,该键的值是一个从 1 开始的数字,一直到 100。只有当x是三的倍数时才设置y的值,其值也是一个运行数,从一开始,当x99时,应最多增加到 33。

然后,我们执行一个查询,并看到以下结果:

> db.sparseTest.find({y:{$ne:2}}, {_id:0}).limit(15)
{ "x" : 1 }
{ "x" : 2 }
{ "x" : 3, "y" : 1 }
{ "x" : 4 }
{ "x" : 5 }
{ "x" : 7 }
{ "x" : 8 }
{ "x" : 9, "y" : 3 }
{ "x" : 10 }
{ "x" : 11 }
{ "x" : 12, "y" : 4 }
{ "x" : 13 }
{ "x" : 14 }
{ "x" : 15, "y" : 5 }
{ "x" : 16 }

y2的值在结果中丢失,这是我们想要的。请注意,y不存在的文档仍然可以在结果中看到。我们现在计划在y字段上创建一个索引。由于字段不存在或具有唯一的值,因此唯一索引应该正常工作。

在内部,索引默认在索引中添加一个条目,即使该字段在集合中的原始文档中不存在。但是,索引中的值将为 null。这意味着索引中的条目数将与集合中的文档数相同。对于唯一索引,值(包括 null 值)在整个集合中应该是唯一的,这解释了为什么在创建索引时字段稀疏(并非所有文档中都存在)会出现异常。

这个问题的一个解决方案是使索引稀疏,我们所做的只是将sparse:1unique:1一起添加到选项中。如果文档中不存在字段,则不会在索引中放置条目。因此,索引现在将包含更少的条目。它将只包含文档中存在字段的条目。这不仅使索引更小,更容易放入内存,而且还解决了添加唯一约束的问题。我们最不希望的是一个包含数百万文档的集合的索引有数百万个条目,其中只有几百个条目定义了一些值。

虽然我们可以看到,创建稀疏索引确实提高了索引的效率,但它引入了一个新问题,即某些查询结果不一致。我们前面执行的同一个查询会产生不同的结果。请参见以下输出:

> db.sparseTest.find({y:{$ne:2}}, {_id:0}).hint({y:1}).limit(15)
{ "x" : 3, "y" : 1 }
{ "x" : 9, "y" : 3 }
{ "x" : 12, "y" : 4 }
{ "x" : 15, "y" : 5 }
{ "x" : 18, "y" : 6 }
{ "x" : 21, "y" : 7 }
{ "x" : 24, "y" : 8 }
{ "x" : 27, "y" : 9 }
{ "x" : 30, "y" : 10 }
{ "x" : 33, "y" : 11 }
{ "x" : 36, "y" : 12 }
{ "x" : 39, "y" : 13 }
{ "x" : 42, "y" : 14 }
{ "x" : 45, "y" : 15 }
{ "x" : 48, "y" : 16 }

为什么会这样?答案在于此查询的查询计划。执行以下操作以查看此查询的计划:

>db.sparseTest.find({y:{$ne:2}}, {_id:0}). hint({y:1}).limit(15).explain()

此计划显示它使用索引获取匹配结果。由于这是一个稀疏索引,所有没有y字段的文档都不在其中,它们也不会出现在结果中,尽管它们应该有。当使用稀疏索引查询集合并且查询恰好使用该索引时,我们需要小心这一陷阱。它将产生意想不到的结果。一种解决方案是强制进行完全收集扫描,我们使用hint函数向查询分析器提供提示。提示用于强制查询分析器使用用户指定的索引。虽然通常不建议这样做,因为您确实需要知道自己在做什么,但这是真正需要这样做的场景之一。那么,我们如何强制进行全表扫描呢?我们所做的就是在hint功能中提供{$natural:1}。集合的自然顺序是特定集合在磁盘上的存储顺序。这个hint强制进行全表扫描,现在我们得到的结果与以前一样。但是,对于大型集合,查询性能会降低,因为它现在使用的是完整表扫描。

如果该字段存在于大量文档中(对于批次没有正式的截止值;对于某些文档,它可以是 50%,对于其他文档,它可以是 75%),并且不是真正稀疏的,那么将索引稀疏化除了我们想使其唯一外没有多大意义。

如果两个文档的同一字段的值为空,则唯一索引创建将失败,并且将其创建为稀疏索引也不会有帮助。

使用 TTL 索引在固定时间间隔后过期的文档

Mongo 中一个有趣的特性是在预定的时间后自动使集合中的数据过期。当我们想要清除一些比特定时间段早的数据时,这是一个非常有用的工具。对于关系数据库,人们不常设置每晚运行的批处理作业来执行此操作。

有了 Mongo 的 TTL特性,您不必担心这一点,因为数据库会立即处理它。让我们看看如何才能做到这一点。

准备好了吗

让我们使用 TTL 索引在 Mongo 中创建要使用的数据。为此,我们将创建一个名为ttlTest的集合。我们将需要一个服务器来启动和运行。有关如何启动服务器的说明,请参阅第 1 章安装和启动服务器中的安装单节点 MongoDB配方。在加载TTLData.js脚本的情况下启动 shell。此脚本可在 Packt 网站上下载。要知道如何使用预加载的脚本启动 shell,请参阅第一章安装和启动服务器中的使用 JavaScript连接 Mongo shell 中的单个节点。

怎么做…

  1. 假设服务器已经启动,并且提供的脚本加载到 shell 上,从 mongo shell 调用以下方法:

    > addTTLTestData()
  2. createDate字段上创建一个 TTL 索引,如下所示:

    > db.ttlTest.createIndex({createDate:1}, {expireAfterSeconds:300})
  3. 现在,按如下方式查询集合:

    > db.ttlTest.find()
  4. 这应该给我们三个文件。重复此过程,并在大约 30-40 秒内重复执行find查询,以查看三个文档被删除,直到整个集合中没有剩余文档。

它是如何工作的…

让我们先打开TTLData.js文件,看看里面发生了什么。代码非常简单,它只是使用新的Date()获取当前日期。然后,它创建了三个带有createDate的文档,这三个文档比当前时间晚了四、三和两分钟。因此,在执行本脚本中的addTTLTestData()方法时,ttlTest集合中有三个文档,每个文档的创建时间相差一分钟。

下一步是TTL 特性的核心:创建TTL 索引。它类似于使用createIndex方法创建任何其他索引,只是它还接受第二个参数,即 JSON 对象。这两个参数如下:

  • 第一个参数为{createDate:1};这将告诉 mongo 在createDate字段上创建索引,索引的顺序是递增的,因为值是1-1应该是递减的)。
  • 第二个参数{expireAfterSeconds:300}使该索引成为 TTL 索引,它告诉 Mongo 在 300 秒(五分钟)后自动使文档过期。

好吧,但是从什么时候开始五分钟了?是插入到集合中的时间还是其他时间戳?在本例中,它将createTime字段作为基础,因为这是我们创建索引的字段。

这就引出了一个问题:如果一个字段被用作计算时间的基础,那么它的类型就必须有一些限制。在char字段上创建一个 TTL 索引(如我们之前创建的)是没有意义的,比如说,保存一个人的名字。

对正如我们猜测的,字段的类型可以是 BSON 类型的日期或日期数组。如果一个数组有多个日期,会发生什么情况?在这种情况下会考虑什么?

事实证明,Mongo 在数组中使用了最少的可用日期。作为练习,尝试此场景。

在文档中,根据字段名updateField放置两个彼此间隔约 5 分钟的日期,然后在此字段上创建一个 TTL 索引,使文档在 10 分钟(600 秒)后过期。查询集合并查看文档何时从集合中删除。在updateField数组中出现的最小时间值过后大约 10 分钟后,它应该被删除。

除了字段类型的约束外,还有一些约束:

  • 如果字段上已有索引,则无法创建 TTL 索引。由于集合的_id字段在默认情况下已经有索引,这实际上意味着您无法在_id字段上创建 TTL 索引。

  • TTL 索引不能是涉及多个字段的复合索引。

  • 如果字段不存在,它将永远不会过期。(我想这很符合逻辑。)

  • It cannot be created on capped collections. In case you are not aware of capped collections, they are special collections in Mongo with a size limit on them with a FIFO insertion order and delete old documents to make place for new documents, if needed.

    TTL 索引仅在 Mongo 2.2 及更高版本上受支持。请注意,文档不会在字段中给定的时间被删除。周期的粒度为一分钟,这将删除自上次运行周期以来符合删除条件的所有文档。

另见

用例可能不要求在固定间隔过后删除所有文档。如果我们要自定义点,直到文档保留在集合中,该怎么办?这也是可以实现的,这将在下一个配方中演示,使用 TTL 索引在给定时间过期文档。

使用 TTL 索引在给定时间到期的文档

在前面的方法中,使用 TTL 索引在固定时间间隔后过期文档,我们已经看到了文档如何在固定时间后过期。但是,在某些情况下,我们可能希望文档在不同的时间过期。这不是我们在前面的食谱中看到的。在这个配方中,我们将看到如何指定文档的过期时间,不同文档的过期时间可能不同。

准备好了吗

对于这个配方,我们将创建一个名为ttlTest2的集合。我们将需要一个服务器来启动和运行。有关如何启动服务器的说明,请参阅第 1 章安装和启动服务器中的安装单节点 MongoDB配方。在加载TTLData.js脚本的情况下启动 shell。此脚本可在 Packt 网站上下载。要知道如何使用预加载的脚本启动 shell,请参阅第一章安装和启动服务器中的使用 JavaScript连接 Mongo shell 中的单个节点。

怎么做…

  1. 使用addTTLTestData2方法在采集中加载所需数据。在 mongo shell 上执行以下操作:

    > addTTLTestData2()
  2. 现在,在ttlTest2集合上创建 TTL 索引,如下所示:

    > db.ttlTest2.createIndex({expiryDate :1}, {expireAfterSeconds:0})
  3. 执行以下find查询查看集合中的三张单据:

    > db.ttlTest2.find()
  4. 现在,在大约四、五和七分钟后,分别删除 ID 为 2、1 和 3 的文档。

它是如何工作的…

让我们从打开TTLData.js文件开始,看看里面发生了什么。我们对该配方感兴趣的方法是addTTLTestData2。此方法只需在tllTest2集合中创建三个文档,分别为123,其exipryDate字段设置为当前时间后的547分钟。请注意,此字段有一个未来日期,与前一配方中给出的日期不同,前一配方中给出的日期是创建日期。

接下来,我们创建一个索引:db.ttlTest2.createIndex({expiryDate :1}, {expireAfterSeconds:0})。这与我们为前一个配方创建索引的方式不同,在前一个配方中,对象的expireAfterSeconds字段被设置为非零值。这就是expireAfterSeconds属性值的解释方式。如果该值非零,则这是 Mongo 从集合中删除文档的基准时间之后经过的时间(以秒为单位)。此基准时间是在创建索引的字段中保留的值(createTime,如前一配方中所示)。如果该值为零,则创建索引的日期值(expiryDate,在本例中)将是文档过期的时间。

总之,如果您想在文档到期时删除文档,TTL 索引可以很好地工作。在很多情况下,我们可能希望将文档移动到归档集合,在归档集合中,可以根据(例如)年份和月份创建归档集合。在任何这样的场景中,TTL 索引都没有帮助,我们可能需要自己编写一个外部作业来完成这项工作。这样的作业还可以读取一系列文档的集合,将它们添加到目标集合,然后从源集合中删除它们。MongoDB 的人已经计划发布一个功能来解决这个问题。

另见

在本教程和上一个教程中,我们研究了 TTL 索引以及如何使用它们。但是,如果在创建 TTL 索引之后,我们想要修改 TTL 值,该怎么办?这可以通过使用collMod选项来实现。有关此选项的更多信息,请参见管理部分中的。