MongoDB CRUD的概念

一、原子性和事务

1.原子性

在MongoDB中,写操作是单个文档级别上的原子操作,即使该操作修改了单个文档中的多个嵌入文档。

2.多文档事务

当单个写操作(例如db.collection.updateMany())修改多个文档时,对每个文档的修改是原子性的,但整个操作不是原子性的。

在执行多文档写操作时,无论是通过单个写操作还是多个写操作,其他操作可能会交错进行。

对于需要对多个文档进行原子性读写的情况(在单个或多个集合中),MongoDB支持多文档事务:

  • 在4.0版本中,MongoDB支持副本集上的多文档事务。
  • 在版本4.2中,MongoDB引入了分布式事务,它在sharded集群上添加了对多文档事务的支持,并合并了对副本集上多文档事务的现有支持。

有关MongoDB事务的详细信息,请参阅事务页面。

重要:
在大多数情况下,与单个文档写入相比,多文档事务会带来更大的性能成本,而且多文档事务的可用性不应该代替有效的模式设计。对于许多场景,非规范化数据模型(嵌入文档和数组)对于您的数据和用例仍然是最优的。也就是说,对于许多场景,适当地对数据建模将最小化对多文档事务的需求。

有关其他事务使用注意事项(如运行时限制和oplog大小限制),请参见生产注意事项。

3. 并发控制

并发控制允许多个应用程序并发运行,而不会导致数据不一致或冲突。

一种方法是在只能具有唯一值的字段上创建惟一的索引。这可以防止插入或更新创建重复的数据。在多个字段上创建惟一索引,以强制字段值的惟一性。有关用例的示例,请参见update()和Unique Index以及findAndModify()和Unique Index。

另一种方法是为写操作在查询谓词中指定字段的预期当前值。

二、Read Isolation, Consistency, and Recency

1. 隔离保证

1.1 未提交读

根据读的关系,客户端可以看到写的结果之前,写是持久的:

  • 不管写操作的写关注点是什么,其他使用“local”或“available”读关注点的客户机可以在向发出客户机确认写操作之前看到写操作的结果。
  • 使用“本地”或“可用”读关注点的客户端可以读取数据,这些数据随后可能在复制集故障转移期间回滚。

对于多文档事务中的操作,当事务提交时,在事务中所做的所有数据更改都会被保存并在事务外部可见。也就是说,事务在回滚其他更改时不会提交某些更改。

在事务提交之前,事务中所做的数据更改在事务外部是不可见的。

然而,当一个事务写入多个碎片时,并不是所有外部读操作都需要等待提交的事务的结果在碎片中可见。例如,如果提交了一个事务,并且在分片a上可以看到写1,但是在分片B上还不能看到写2,那么外部的read at read关注点“local”可以在看不到写2的情况下读取写1的结果。

Read uncommitted是默认的隔离级别,适用于mongod的独立实例、副本集和分片集群。

1.2 读取未提交的单文档原子性

写操作是相对于单个文档的原子性操作;例如,如果一个写操作正在更新文档中的多个字段,那么一个读操作将永远不会看到文档中只有一些字段被更新。然而,尽管客户端可能看不到部分更新的文档,但read uncommitted意味着并发读操作仍然可能在更改变为持久之前看到更新的文档。

对于一个独立的mongod实例,对单个文档的一组读写操作是可序列化的。对于副本集,只有在没有回滚的情况下,对单个文档的一组读写操作才是可序列化的。

1.3 读取未提交和多个文档写入

当单个写操作(例如db.collection.updateMany())修改多个文档时,对每个文档的修改是原子性的,但整个操作不是原子性的。

在执行多文档写操作时,无论是通过单个写操作还是多个写操作,其他操作可能会交错进行。

对于需要对多个文档进行原子性读写的情况(在单个或多个集合中),MongoDB支持多文档事务:

  • 在4.0版本中,MongoDB支持副本集上的多文档事务。
  • 在版本4.2中,MongoDB引入了分布式事务,它在sharded集群上添加了对多文档事务的支持,并合并了对副本集上多文档事务的现有支持。

有关MongoDB事务的详细信息,请参阅事务页面。

重要的
在大多数情况下,与单个文档写入相比,多文档事务会带来更大的性能成本,而且多文档事务的可用性不应该代替有效的模式设计。对于许多场景,非规范化数据模型(嵌入文档和数组)对于您的数据和用例仍然是最优的。也就是说,对于许多场景,适当地对数据建模将最小化对多文档事务的需求。

在不隔离多文档写操作的情况下,MongoDB表现出以下行为:

  1. Non-point-in-time读取操作。假设读取操作从时间t1开始,并开始读取文档。写操作然后在稍后的t2时间向其中一个文档提交更新。读者可能会看到文档的更新版本,因此不会看到数据的时间点快照。
  2. Non-serializable操作。假设一个读操作在t1时刻读取文档d1,而一个写操作在t3时刻更新d1。这引入了读写依赖关系,如果要序列化操作,则读操作必须位于写操作之前。但也假设写操作在t2时更新文档d2,而读操作随后在t4时读取d2。这引入了一个写-读依赖项,它要求在可串行化调度的写操作之后执行读操作。有一个依赖循环使得序列化成为不可能。
  3. 读取操作可能会丢失在读取操作过程中更新的匹配文档。

1.4 游标快照

在某些情况下,MongoDB游标可以多次返回相同的文档。当游标返回文档时,其他操作可能与查询交织在一起。如果其中一些操作更改了查询使用的索引上的索引字段;然后光标将不止一次地返回相同的文档。

如果您的集合有一个或多个从未修改过的字段,那么您可以在该字段或这些字段上使用惟一的索引,这样查询将最多一次返回每个文档。使用hint()来显式地强制查询使用该索引。

2. 单调写

默认情况下,MongoDB为独立的mongod实例和副本集提供单调的写保证。
对于单调的写和切分群集,请参见因果一致性。

3. Real Time Order

新版本3.4。
对于主文档上的读写操作,使用“可线性化”的读关注点发出读操作,使用“多数”的写关注点发出写操作,使得多个线程能够对单个文档执行读写操作,就好像一个线程实时执行这些操作一样;也就是说,这些读写的相应调度被认为是可线性化的。

4. 因果一致性

新版本3.6。
如果一个操作在逻辑上依赖于前一个操作,那么这些操作之间就存在因果关系。例如,基于指定条件删除所有文档的写操作和随后验证删除操作的读操作之间存在因果关系。

在因果一致的会话中,MongoDB按照尊重因果关系的顺序执行因果操作,客户端观察到的结果与因果关系一致。

4.1 客户会话和因果一致性保证

为了提供因果一致性,MongoDB 3.6支持客户端会话的因果一致性。一个因果一致的会话表示与“多数”读操作和与“多数”写操作相关的读操作序列具有因果关系,并通过它们的顺序反映出来。应用程序必须确保一次只有一个线程在客户端会话中执行这些操作。

对于因果相关的操作:

a. 客户端启动客户端会话。

重要的
客户会议只保证因果一致性:

  • 读取“多数”操作;也就是说,返回的数据已经被大多数的复制集成员确认并且是持久的。
  • 具有“多数”写关心的写操作;即写操作,请求确认该操作已应用于复制集的大多数投票成员。

b.当客户端发出一个带有“多数”读关注和写操作(带有“多数”写关注)的读序列时,客户端在每个操作中包含会话信息。

c.对于每个具有“多数”读关注的读操作和与会话相关的具有“多数”写关注的写操作,MongoDB返回操作时间和集群时间,即使操作错误。客户机会话跟踪操作时间和集群时间。

请注意
MongoDB不返回未确认(w: 0)写操作的操作时间和集群时间。未被承认的写并不意味着任何因果关系。
虽然MongoDB在客户端会话中返回读操作和已确认的写操作的操作时间和集群时间,但只有“多数”读操作和“多数”写操作才能保证因果一致性。有关详细信息,请参阅因果一致性和读写关注点。

d.关联的客户端会话跟踪这两个时间字段。

请注意
操作可以在不同的会话之间保持因果一致性。MongoDB驱动程序和mongo shell提供了提高客户机会话的操作时间和集群时间的方法。因此,客户机可以将一个客户机会话的集群时间和操作时间提前,以与另一个客户机会话的操作保持一致。

4.2 因果一致性保证

下表列出了因果一致性会话为“多数”读操作和“多数”写操作提供的因果一致性保证。

GuaranteesDescription
Read your writes读操作反映了它们之前的写操作的结果。
Monotonic reads

读操作不会返回与前一个读操作之前的数据状态相对应的结果。

例如,如果在一个会话中:

  • write1 先于write2,
  • read1 先于read2, and
  • read1返回反映write2的结果

then read2不能返回write1的结果。

Monotonic writes

必须在其他写之前执行的写操作将在其他写之前执行。

例如,如果一个会话中的write1必须在write2之前,那么write2时数据的状态必须反映write1后数据的状态。其他写操作可以在write1和write2之间交替进行,但是write2不能在write1之前发生。

Writes follow reads必须在读操作之后执行的写操作在那些读操作之后执行。也就是说,写操作时的数据状态必须包含前面的读操作的数据状态。

5 复制集读选项

这些保证适用于MongoDB部署的所有成员。例如,如果在一个偶然一致的会话中,您发出一个带有“多数”写关注点的写,然后发出一个从带有“多数”读关注点的次读(即读首选项次读)的读,那么读操作将反映写操作之后数据库的状态。

5.1 隔离

在因果一致的会话内的操作与会话外的操作是不分离的。如果并发写操作在会话的写操作和读操作之间交错,则会话的读操作可能返回反映在会话的写操作之后发生的写操作的结果。

6. 特性兼容性的版本

特性兼容性版本(fCV)必须设置为“3.6”或更高。要检查fCV,请运行以下命令:

db.adminCommand( { getParameter: 1, featureCompatibilityVersion: 1 } )

7. MongoDB Drivers

提示
应用程序必须确保一次只有一个线程在客户端会话中执行这些操作。

客户端需要MongoDB驱动更新为MongoDB 3.6或更高:

Java 3.6+

Python 3.6+

C 1.9+

C# 2.5+

Node 3.0+

Ruby 2.5+

Perl 2.0+

PHPC 1.4+

Scala 2.2+

例子

重要的
因果一致性会话只能保证“多数”读关心的读和“多数”写关心的写之间的因果一致性。

考虑一个集合项,它维护各种项的当前和历史数据。只有历史数据具有非空结束日期。如果某个项目的sku值发生变化,则需要使用结束日期更新具有旧sku值的文档,然后用当前sku值插入新文档。客户端可以使用一致的会话来确保更新发生在插入之前。

// Example 1: Use a causally consistent session to ensure that the update occurs before the insert.
ClientSession session1 = client.startSession(ClientSessionOptions.builder().causallyConsistent(true).build());
Date currentDate = new Date();
MongoCollection<Document> items = client.getDatabase("test")
        .withReadConcern(ReadConcern.MAJORITY)
        .withWriteConcern(WriteConcern.MAJORITY.withWTimeout(1000, TimeUnit.MILLISECONDS))
        .getCollection("test");

items.updateOne(session1, eq("sku", "111"), set("end", currentDate));

Document document = new Document("sku", "nuts-111")
        .append("name", "Pecans")
        .append("start", currentDate);
items.insertOne(session1, document);

如果另一个客户端需要读取所有当前的sku值,您可以将集群时间和操作时间提前到另一个会话的时间,以确保该客户端与另一个会话的时间是一致的,并在两个写入之后进行读取:

// Example 2: Advance the cluster time and the operation time to that of the other session to ensure that
// this client is causally consistent with the other session and read after the two writes.
ClientSession session2 = client.startSession(ClientSessionOptions.builder().causallyConsistent(true).build());
session2.advanceClusterTime(session1.getClusterTime());
session2.advanceOperationTime(session1.getOperationTime());

items = client.getDatabase("test")
        .withReadPreference(ReadPreference.secondary())
        .withReadConcern(ReadConcern.MAJORITY)
        .withWriteConcern(WriteConcern.MAJORITY.withWTimeout(1000, TimeUnit.MILLISECONDS))
        .getCollection("items");

for (Document item: items.find(session2, eq("end", BsonNull.VALUE))) {
    System.out.println(item);
}

8. 局限性

以下构建内存结构的操作不是因果一致的:

OperationNotes
collStats 
$collStats with latencyStats option. 
$currentOp如果操作与原因一致的客户端会话相关联,则返回一个错误。
createIndexes 
dbHashStarting in MongoDB 4.2
dbStats 
getMore如果操作与原因一致的客户端会话相关联,则返回一个错误。
$indexStats 
mapReduceStarting in MongoDB 4.2
ping如果操作与原因一致的客户端会话相关联,则返回一个错误。
serverStatus如果操作与原因一致的客户端会话相关联,则返回一个错误。
validateStarting in MongoDB 4.2

三、因果一致性和读写关系

使用MongoDB的因果一致性客户机会话,不同的读写关注点组合提供了不同的因果一致性保证。当因果一致性被定义为意味着持久性时,下表列出了各种组合提供的具体保证:

Read ConcernWrite ConcernRead own writesMonotonic readsMonotonic writesWrites follow reads
"majority""majority"
"majority"{ w: 1 }  
"local"{ w: 1 }    
"local""majority"   

如果因果一致性意味着持久性,那么,从表中可以看出,只有具有“多数”读关注的读操作和具有“多数”写关注的写操作才能保证所有四个因果一致性保证。也就是说,因果一致性的客户端会话只能保证因果一致性:

  • 具有“多数”读关心的读操作;也就是说,读取操作返回的数据已经被大多数复制集成员确认并且是持久的。
  • 具有“多数”写关心的写操作;即写操作,请求确认该操作已应用于复制集的大多数投票成员。

如果因果一致性并不意味着持久性(即写操作可以回滚),那么使用{w: 1}写关注点的写操作也可以提供因果一致性。

请注意
读写关注点的其他组合也可能在某些情况下满足所有四个因果一致性保证,但不一定在所有情况下都满足。

读关心“多数”,写关心“多数”,确保即使在副本集中的两个成员暂时认为他们是主要成员的情况下(例如网络分区),这四个因果一致性保证也成立。虽然两个初选都可以使用{w: 1}的写关注点来完成写操作,但是只有一个初选可以使用“多数”的写关注点来完成写操作。

例如,考虑这样一种情况:一个网络分区将一个5个成员的副本集划分为:

使用上述分区:

  • 带有“多数”写关心的写可以在Pnew上完成,但不能在Pold上完成。
  • 写关心可以在Pold或Pnew上完成。但是,一旦这些成员恢复与复制集的其余部分的通信,对Pold的写操作(以及复制到S1的写操作)将回滚。
  • 成功地在Pnew上使用“majority”写关注点之后,与“majority”读关注点一致的读可以观察在Pnew、S2和S3上的写。一旦读操作可以与复制集的其他成员进行通信并进行同步,就可以观察Pold和S1上的写操作。在分区期间对Pold和/或复制到S1的任何写操作都将回滚。

1. 场景

为了说明读写关注点需求,下面的场景中,客户端向副本集发出一系列操作,这些操作具有不同的读写关注点组合:

1.1 Read Concern "majority" and Write concern "majority"

在因果一致性会话中使用read concern“majority”和write concern“majority”提供了以下因果一致性保证:

✅ Read own writes ✅ Monotonic reads ✅ Monotonic writes ✅ Writes follow reads

场景1(读关注“多数”,写关注“多数”)

在两个初级阶段的过渡期内,因为只有Pnew可以用{w: "majority"}的write关注点来完成写操作,所以一个客户端会话可以成功地发出以下操作序列:

SequenceExample

1. Write1 with write concern "majority" to Pnew

2. Read1 with read concern "majority" to S2

3. Write2 with write concern "majority" to Pnew

4. Read2 with read concern "majority" to S3

For item A, update qty to 50.

Read item A.

For items with qty less than 50, update reorder to true.

Read item A.

Read own writes

Read1 reads data from S2 that reflects a state after Write1.

Read2 reads data from S1 that reflects a state after Write11 followed by Write2.

Monotonic readsRead2 reads data from S3 that reflects a state after Read1.
Monotonic writesWrite2 updates data on Pnew that reflects a state after Write1.
Writes follow readsWrite2 updates data on Pnew that reflects a state of the data after Read1 (i.e. an earlier state reflects the data read by Read1).

场景2(读关注“多数”,写关注“多数”)

考虑另一个序列,其中Read1与read关系“多数”路由到S1:

SequenceExample

1. Write1 with write concern "majority" to Pnew

2. Read1 with read concern "majority" to S1

3. Write2 with write concern "majority" to Pnew

4. Read2 with with read concern "majority" to S3

For item A, update qty to 50.

Read item A.

For items with qty less than 50, update reorder to true.

Read item A.

在这个顺序中,Read1直到在Pold上的大多数提交点已经前进了才可以返回。在Pold和S1能够与复制集的其余部分通信之前,这种情况不会发生;此时,Pold已经退出(如果还没有退出),两个成员同步(包括Write1)来自复制集的其他成员。

Read own writesRead1反映了Write11之后的数据状态,尽管此时网络分区已经恢复,且该成员已与副本集中的其他成员同步。
Read2从S3读取反映Write11和Write2之后状态的数据。
Monotonic readsRead2从反映Read1之后状态的S3中读取数据(即Read1读取的数据中反映了较早的状态)。
Monotonic writesWrite2更新Pnew上反映Write1之后状态的数据。
Writes follow readsWrite2更新Pnew上反映Read1之后数据状态的数据(即较早的状态反映Read1读取的数据)。

读取关注“多数”并写入关注{w: 1}

在因果一致性会话中,使用read concern“majority”和write concern {w: 1}提供了以下因果一致性保证,如果因果一致性意味着持久性:

❌ Read own writes ✅ Monotonic reads ❌ Monotonic writes ✅ Writes follow reads

如果因果一致性并不意味着持久性:

✅ Read own writes ✅ Monotonic reads ✅ Monotonic writes ✅ Writes follow reads

场景3(读关注“多数”,写关注{w: 1})

在有两个初级阶段的过渡期间,因为Pold和Pnew都可以使用{w: 1}的write关注点来完成写操作,所以一个客户端会话可以成功地发出以下操作序列,但是如果因果一致性意味着持久性,那么它就不是因果一致的:

SequenceExample

1. Write1 with write concern { w: 1 } to Pold

2. Read1 with read concern "majority" to S2

3. Write2 with write concern { w: 1 } to Pnew

4. Read2 with with read concern "majority" to S3

For item A, update qty to 50.

Read item A.

For items with qty less than 50, update reorder to true.

Read item A.

在这个序列,

  • 只有当Pnew上的大多数提交点超过了Write1时,Read1才能返回。
  • Read2不能返回,直到在Pnew上的大多数提交点已经超过了Write2的时间。
  • 当网络分区恢复时,Write1将回滚。

如果因果一致性意味着持久性

Read own writesRead1从S2读取的数据在写1之后不反映状态。
Monotonic readsRead2从反映Read1之后状态的S3中读取数据(即Read1读取的数据中反映了较早的状态)。
Monotonic writesWrite2更新Pnew上不反映Write1后状态的数据。
Writes follow readsWrite2更新Pnew上反映Read1之后状态的数据(即较早的状态反映Read1读取的数据)。

如果因果一致性并不意味着持久性

Read own writesRead1从S2读取数据,返回的数据反映了与Write1等价的状态,然后是Write1的回滚。
Monotonic readsRead2从反映Read1之后状态的S3中读取数据(即Read1读取的数据中反映了较早的状态)。
Monotonic writesWrite2更新Pnew上的数据,相当于在Write1之后回滚Write1。
Writes follow readsWrite2更新Pnew上的数据,该数据反映了Read1之后的状态(即其早期状态反映了Read1读取的数据)。

场景4(读取关注“多数”,并写入关注{w: 1})

考虑另一个序列,其中Read1与read关系“多数”路由到S1:

SequenceExample

Write1 with write concern { w: 1 } to Pold

Read1 with read concern "majority" to S1

Write2 with write concern { w: 1 } to Pnew

Read2 with with read concern "majority" to S3

For item A, update qty to 50.

Read item A.

For items with qty less than 50, update reorder to true.

Read item A.

在这个序列:

  • 在S1上的大多数提交点之前,Read1不能返回。直到Pold和S1可以与复制集的其他成员通信时,这才会发生。此时,Pold已经退出(如果还没有),Write1将从Pold和S1回滚,并且这两个成员将与复制集的其他成员同步。

如果因果一致性意味着持久性

Read own writesRead1读取的数据不反映已回滚的Write1的结果。
Monotonic readsRead2从反映Read1之后状态的S3中读取数据(即其早期状态反映Read1读取的数据)。
Monotonic writesWrite2更新Pnew上的数据,该数据不反映Write1之后的状态,Write1在Write2之前,但已回滚。
Writes follow readsWrite2更新Pnew上的数据,该数据反映了Read1之后的状态(即其早期状态反映了Read1读取的数据)。

如果因果一致性并不意味着持久性

Read own writesRead1返回反映Write1最终结果的数据,因为Write1最终回滚。
Monotonic readsRead2从反映Read1之后状态的S3中读取数据(即较早的状态反映Read1读取的数据)。
Monotonic writesWrite2更新Pnew上的数据,相当于在Write1之后回滚Write1。
Writes follow readsWrite2更新Pnew上反映Read1之后状态的数据(即较早的状态反映Read1读取的数据)。

读取关注点“local”并写入关注点{w: 1}

在因果一致的会话中使用read concern“local”和write concern {w: 1}不能保证因果一致。

❌ Read own writes ❌ Monotonic reads ❌ Monotonic writes ❌ Writes follow reads

这种组合可能在某些情况下满足所有四种因果一致性保证,但不一定在所有情况下都满足。

场景5(读取关注“本地”并写入关注{w: 1})

在这个过渡期间,因为Pold和Pnew都可以使用{w: 1}的write关注点来完成写操作,所以客户端会话可以成功地发出以下操作序列,但是不具有因果一致性:

SequenceExample

1. Write1 with write concern { w: 1 } to Pold

2. Read1 with read concern "local" to S1

3. Write2 with write concern { w: 1 } to Pnew

4. Read2 with read concern "local" to S3

For item A, update qty to 50.

Read item A.

For items with qty less than 50, update reorder to true.

Read item A.

❌ Read own writesRead2 reads data from S3 that only reflects a state after Write2 and not Write1 followed by Write2.
❌ Monotonic readsRead2 reads data from S3 that does not reflect a state after Read1 (i.e. an earlier state does not reflect the data read by Read1).
❌ Monotonic writesWrite2 updates data on Pnew that does not reflect a state after Write1.
❌ Write follow readWrite2 updates data on Pnew that does not reflect a state after Read1 (i.e. an earlier state does not reflect the data read by Read1).

读关心“本地”,写关心“多数”

在一个因果一致的会话中,使用“局部”和“多数”的读关注提供了以下因果一致的保证:

❌ Read own writes ❌ Monotonic reads ✅ Monotonic writes ❌ Writes follow reads

这种组合可能在某些情况下满足所有四种因果一致性保证,但不一定在所有情况下都满足。

场景6(读关注“本地”,写关注“多数”)

在这个过渡期间,因为只有Pnew可以用{w: "majority"}的write关注点来完成写操作,所以一个客户端会话可以成功地发出以下操作序列,但不是因果一致的:

SequenceExample

1. Write1 with write concern "majority" to Pnew

2. Read1 with read concern "local" to S1

3. Write2 with write concern "majority" to Pnew

4. Read2 with read concern "local" to S3

For item A, update qty to 50.

Read item A.

For items with qty less than 50, update reorder to true.

Read item A.

❌ Read own writes.Read1 reads data from S1 that does not reflect a state after Write11.
❌ Monotonic reads.Read2 reads data from S3 that does not reflect a state after Read1 (i.e. an earlier state does not reflect the data read by Read1).
✅ Monotonic writesWrite2 updates data on Pnew that reflects a state after Write1.
❌ Write follow read.Write2 updates data on Pnew that does not reflect a state after Read1 (i.e. an earlier state does not reflect the data read by Read1).

三、分布式查询

1.将操作读入复制集

默认情况下,客户端从复制集的主节点读取数据;但是,客户端可以指定一个read首选项,将读操作直接发送给其他成员。例如,客户端可以配置读首选项,从二级或从最近的成员读到:

  • 减少多数据中心部署中的延迟,
  • 通过分配高读卷(相对于写卷)来提高读吞吐量,
  • 执行备份操作,
  • 并/或在选择新主服务器之前允许读。

从复制集的次要成员执行的读取操作可能不反映主要成员的当前状态。直接对不同服务器执行读操作的读首选项可能导致非单调的读操作。

版本3.6的变化:在MongoDB 3.6中开始,客户端可以使用因果一致的会话,它提供了各种保证,包括单调的读取。

您可以在每个连接或每个操作的基础上配置读首选项。有关读首选项或读首选项模式的更多信息,请参见读首选项和读首选项模式。

2. 在复制集上写操作

在复制集中,所有的写操作都将转到集的主节点。主应用写操作并记录主操作日志或oplog上的操作。oplog是对数据集的可复制操作序列。集合的次要成员不断地复制oplog,并在异步过程中将操作应用到它们自己。

3. 将操作读入分片集群

切分集群允许您以对应用程序几乎透明的方式在mongod实例集群中划分数据集。有关切分集群的概述,请参阅本手册的切分部分。
对于分片集群,应用程序将操作发送给与集群关联的mongos实例之一。

当被指向特定碎片时,分片集群上的读操作是最有效的。对切分集合的查询应该包括集合的切分键。当查询包含一个碎片键时,mongos可以使用配置数据库中的集群元数据将查询路由到碎片。

如果一个查询不包含碎片键,mongos必须将查询指向集群中的所有碎片。这些分散的收集查询可能是低效的。在较大的集群中,散点收集查询对于常规操作是不可行的。

对于复制集碎片,从复制集的次要成员读取操作可能不反映主服务器的当前状态。直接对不同服务器执行读操作的读首选项可能导致非单调的读操作。

请注意
从MongoDB 3.6开始,

  • 客户端可以使用因果一致的会话,这提供了各种保证,包括单调的读取。
  • 碎片复制集的所有成员(不仅仅是主副本集)维护关于块元数据的元数据。如果不使用“available”读关注,这将防止从二级读取返回孤立的数据。在较早的版本中,从中学进行的读取(不管读取的问题)可以返回孤立的文档。

4. 在分片集群上写操作

对于分片集群中的分片集合,mongos将写操作从应用程序定向到分片,分片负责数据集的特定部分。mongos使用配置数据库中的集群元数据将写操作路由到适当的分片。

MongoDB根据切分键的值将切分集合中的数据划分为范围。然后,MongoDB将这些块分发到碎片。分片键确定块到分片的分布。这可能会影响集群中的写操作的性能。

重要的:
影响单个文档的更新操作必须包括碎片键或_id字段。影响多个文档的更新在某些情况下更有效,如果它们具有碎片密钥,但是可以广播到所有碎片。

如果分片键的值随每次插入而增加或减少,则所有插入操作都针对单个分片。因此,单个碎片的容量成为切分集群插入容量的限制。

三、通过findAndModify实现线性化读取

1.概述

当阅读从一套副本,可以读取数据,过期(即可能并不能反映所有写道,发生前读操作)或不耐用(即数据可能反映了写的状态还没有得到多数或副本集成员因此可以回滚),这取决于所使用的阅读问题。

从3.4版开始,MongoDB引入了“linearizable”read关注点,该关注点返回未过期的持久数据。可线性化的read关注点保证只在read操作指定唯一标识单个文档的查询筛选器时应用。

本教程概述了另一种方法,即使用db.collection.findAndModify()来读取未过期且不能回滚的数据,用于使用MongoDB 3.2进行部署。对于MongoDB 3.4,虽然可以应用上述过程,但请参阅“linearizable”read concern。

2.通过findAndModify实现线性化读取

此过程使用db.collection.findAndModify()来读取未过期且不能回滚的数据。为此,该过程使用带有写关注点的findAndModify()方法来修改文档中的虚拟字段。具体而言,该程序要求:

  • findandmodify()使用精确匹配的查询,并且必须存在唯一的索引来满足查询。
  • findAndModify()必须实际修改文档;即导致对文档的更改。
  • findAndModify()必须使用写关注点{w: "majority"}。

重要:
“quorum read”过程与简单地使用“多数人”的读关注点相比有很大的成本,因为它会导致写延迟而不是读延迟。只有在绝对无法忍受老化的情况下,才应该使用这种技术。

2.1 前提条件

本教程从一个名为products的集合中读取。使用以下操作初始化集合。

db.products.insert( [
   {
     _id: 1,
     sku: "xyz123",
     description: "hats",
     available: [ { quantity: 25, size: "S" }, { quantity: 50, size: "M" } ],
     _dummy_field: 0
   },
   {
     _id: 2,
     sku: "abc123",
     description: "socks",
     available: [ { quantity: 10, size: "L" } ],
     _dummy_field: 0
   },
   {
     _id: 3,
     sku: "ijk123",
     description: "t-shirts",
     available: [ { quantity: 30, size: "M" }, { quantity: 5, size: "L" } ],
     _dummy_field: 0
   }
] )

这个集合中的文档包含一个名为_dummy_field的虚拟字段,该字段将由本教程中的db.collection.findAndModify()递增。如果该字段不存在,db.collection.findAndModify()操作将把该字段添加到文档中。该字段的目的是确保db.collection.findAndModify()导致对文档的修改。

2.2 过程

a.创建唯一的索引。

创建唯一的索引。
在字段上创建一个惟一的索引,用于在db.collection.findAndModify()操作中指定精确的匹配。
本教程将在sku字段上使用精确匹配。因此,在sku字段上创建惟一的索引。

db.products.createIndex( { sku: 1 }, { unique: true } )

b.使用findAndModify读取提交的数据。

使用db.collection.findAndModify()方法对要读取的文档进行简单更新并返回修改后的文档。需要一个{w: "majority"}的写关注点。要指定要读取的文档,必须使用唯一索引支持的精确匹配查询。

下面的findAndModify()操作指定唯一索引字段sku上的精确匹配,并增加匹配文档中名为_dummy_field的字段。虽然不是必需的,但是这个命令的写关注点还包括一个5000毫秒的wtimeout值,以防止在写不能传播到大多数投票成员时操作被永久阻塞。

var updatedDocument = db.products.findAndModify(
   {
     query: { sku: "abc123" },
     update: { $inc: { _dummy_field: 1 } },
     new: true,
     writeConcern: { w: "majority", wtimeout: 5000 }
   }
);

即使在副本集中有两个节点认为它们是主节点的情况下,也只有一个节点能够使用w:“majority”来完成写操作。因此,只有当客户机连接到真正的主服务器来执行操作时,带有“多数”写关注点的findAndModify()方法才会成功。

因为quorum read过程只增加文档中的一个虚拟字段,所以可以安全地重复调用findAndModify(),根据需要调整wtimeout。

四、查询计划

对于查询,MongoDB查询优化器根据可用的索引选择并缓存最有效的查询计划。最有效的查询计划的评估基于查询执行计划在评估候选计划时执行的“工作单元”(工作)的数量。

关联的计划缓存条目用于具有相同查询形状的后续查询。

1.计划缓存入口状态

从MongoDB 4.2开始,缓存条目与一个状态相关联:

StateDescription
Missing

缓存中不存在此形状的条目。
对于查询,如果形状的缓存项状态丢失:

  1. 对候选方案进行评估并选择获胜方案。
  2. 选中的计划将以其works值处于非活动状态添加到缓存中。
Inactive

缓存中的条目是这个形状的占位符条目。也就是说,规划者已经看到了形状并计算了它的成本(工作值),并将其存储为占位符条目,但是查询形状并不用于生成查询计划。

对于查询,如果形状的缓存项状态是不活动的:

  1. 对候选方案进行评估并选择获胜方案。
  2. 选择的计划的工作值与不活动的条目的工作值进行比较。若所选方案的工作值为:
  • 小于或等于非活动的项,

          所选计划将替换占位符非活动项,并具有活动状态。

          如果在替换之前,不活动的条目变为活动的(例如,由于另一个查询操作),那么只有当新活动的条目的work值大于所选计划时,才会替换它。

  • 大于非活动项,

    不活动的条目保持不变,但其工作值增加。

Active缓存中的条目是获胜计划。计划器可以使用这个条目来生成查询计划。
对于查询,如果形状的缓存项状态是活动的:
活动条目用于生成查询计划。
规划器还会评估条目的性能,如果条目的工作值不再满足选择条件,则会过渡到非活动状态。

2. 查询计划和缓存信息

要查看给定查询的查询计划信息,可以使用db.collection.explain()或指针.explain()。
从MongoDB 4.2开始,可以使用$planCacheStats聚合阶段查看集合的计划缓存信息。

3. 计划缓存冲

如果mongod重新启动或关闭,查询计划缓存将不持久。此外:

  • 目录操作(如索引或集合删除)清除计划缓存。
  • 不管状态如何,最近最少使用(LRU)缓存替换机制清除最近最少访问的缓存条目。

用户还可以:

  • 使用PlanCache.clear()方法手动清除整个计划缓存。
  • 使用PlanCache.clearPlansByQuery()方法手动清除特定的计划缓存条目。

4. queryHash和planCacheKey

a.queryHash

为了帮助识别具有相同查询形状的慢查询,从MongoDB 4.2开始,每个查询形状都关联一个queryHash。queryHash是一个十六进制字符串,表示查询形状的散列,仅依赖于查询形状。

请注意:
与任何散列函数一样,两个不同的查询形状可能会导致相同的散列值。但是,不太可能出现不同查询形状之间的哈希冲突。

b.planCacheKey

为了更深入地了解查询计划缓存,MongoDB 4.2引入了planCacheKey。
planCacheKey是与查询关联的计划缓存条目的键的散列。

请注意:
与queryHash不同,planCacheKey是查询形状和当前可用的形状索引的函数。也就是说,如果能够支持查询形状的索引被添加/删除,planCacheKey值可能会改变,而queryHash值不会改变。

例如,考虑一个集合foo具有以下索引:

db.foo.createIndex( { x: 1 } )
db.foo.createIndex( { x: 1, y: 1 } )
db.foo.createIndex( { x: 1, z: 1 }, { partialFilterExpression: { x: { $gt: 10 } } } )

下面查询的集合具有相同的形状:

db.foo.explain().find( { x: { $gt: 5 } } )  // Query Operation 1
db.foo.explain().find( { x: { $gt: 20 } } ) // Query Operation 2

对于这些查询,带有部分筛选器表达式的索引可以支持查询操作2,但不支持查询操作1。由于支持查询操作1的索引不同于查询操作2,所以这两个查询有不同的planCacheKey。

如果删除了一个索引,或者添加了一个新的索引{x: 1, a: 1},那么这两个查询操作的planCacheKey都将改变。

5.可用性

queryHash和planCacheKey可在以下地方获得:

  • explain()输出字段:queryPlanner。queryHash queryPlanner。
  • 记录慢速查询时,planCacheKey profiler记录消息和诊断日志消息(即mongod/mongos记录消息)。
  • $planCacheStats聚合阶段(MongoDB 4.2新增)
  • PlanCache.listQueryShapes / planCacheListQueryShapes命令()方法
  • PlanCache.getPlansByQuery / planCacheListPlans命令()方法

5.1 索引过滤

索引过滤器确定优化器为查询形状计算哪些索引。查询形状由查询、排序和投影规范组成。如果给定查询形状存在索引筛选器,则优化器只考虑筛选器中指定的那些索引。

当查询形状存在索引过滤器时,MongoDB会忽略hint()。要查看MongoDB是否为查询形状应用了索引过滤器,请检查db.collection.explain()或指针.explain()方法的indexFilterSet字段。

索引过滤器只影响优化器计算哪些索引;优化器仍然可以选择集合扫描作为给定查询形状的获胜计划。

索引筛选器存在于服务器进程的持续时间内,并且在关闭后不持久。MongoDB还提供了一个命令来手动删除过滤器。

因为索引过滤器会覆盖优化器的预期行为和hint()方法,所以要少用索引过滤器。

五、查询优化

索引通过减少查询操作需要处理的数据量来提高读取操作的效率。这简化了与在MongoDB中实现查询相关的工作。

1.创建一个支持读操作的索引

如果应用程序查询特定字段或字段集上的集合,则查询字段上的索引或字段集上的复合索引可以防止查询扫描整个集合来查找和返回查询结果。有关索引的更多信息,请参阅MongoDB中索引的完整文档。

例子
应用程序查询类型字段上的库存集合。type字段的值是用户驱动的。

var typeValue = <someUserInput>;
db.inventory.find( { type: typeValue } );

要改进此查询的性能,请向type字段上的inventory集合添加升序或降序索引。在mongo shell中,您可以使用db.collection.createIndex()方法创建索引:

db.inventory.createIndex( { type: 1 } )

该索引可以防止上述类型查询扫描整个集合以返回结果。

要使用索引分析查询的性能,请参阅分析查询性能。
除了优化读操作之外,索引还可以支持排序操作并允许更有效的存储利用。有关创建索引的更多信息,请参见db.collection.createIndex()和索引。

对于单字段索引,升序和降序的选择是无关紧要的。对于复合索引,选择非常重要。有关详细信息,请参阅索引顺序。

2. 查询选择性

查询选择性是指查询谓词在多大程度上排除或过滤集合中的文档。查询选择性可以决定查询是否可以有效地使用索引,甚至可以完全使用索引。
选择性更强的查询匹配的文档比例更小。例如,惟一_id字段上的相等匹配是高度选择性的,因为它最多可以匹配一个文档。

选择性较低的查询匹配更大比例的文档。选择性较低的查询不能有效地甚至根本不能使用索引。
例如,不等操作符$nin和$ne不是很有选择性,因为它们通常匹配索引的很大一部分。因此,在许多情况下,带有索引的$nin或$ne查询的性能可能不比必须扫描集合中的所有文档的$nin或$ne查询更好。

正则表达式的选择性取决于表达式本身。有关详细信息,请参见正则表达式和索引使用。
3. 覆盖查询

覆盖查询是一种可以完全使用索引来满足的查询,并且不需要检查任何文档。一个索引涵盖一个查询时,所有下列应用:

  • 查询中的所有字段都是索引的一部分,
  • 结果中返回的所有字段都在同一个索引中。
  • 查询中没有字段等于null(即{“field”:null}或{“field”:{$eq: null}})。

例如,一个集合目录在类型和项目字段上有以下索引:

db.inventory.createIndex( { type: 1, item: 1 } )

该索引将覆盖以下操作,这些操作对类型和项字段进行查询,只返回项字段:

db.inventory.find(
   { type: "food", item:/^c/ },
   { item: 1, _id: 0 }
)

对于覆盖查询的指定索引,投影文档必须显式地指定_id: 0,以便从结果中排除_id字段,因为索引不包括_id字段。
版本3.6的变化:索引可以覆盖对嵌入文档中的字段的查询。[2]
例如,考虑一个包含以下表单文档的集合userdata:

{ _id: 1, user: { login: "tester" } }

集合有以下索引:

{ "user.login": 1 }

{”用户。登录“:1}索引将覆盖以下查询:

db.userdata.find( { "user.login": "tester" }, { "user.login": 1, _id: 0 } )

a.多键覆盖

从3.6开始,如果索引跟踪导致索引为多键的字段,多键索引可以覆盖非数组字段的查询。在MongoDB 3.4或更高版本的存储引擎上创建的多键索引跟踪这些数据。

b.Performance

因为索引包含查询所需的所有字段,所以MongoDB既可以匹配查询条件,又可以仅使用索引返回结果。
只查询索引要比在索引之外查询文档快得多。索引键通常比它们编录的文档要小,而且索引通常在RAM中可用,或者按顺序位于磁盘上。

c.限制

(1)索引字段的限制

  • 地理空间索引无法覆盖查询。
  • 多键索引不能覆盖数组字段上的查询。

(2)对切分集合的限制

在MongoDB 3.0开始,不能覆盖一个查询的索引分片收集运行时对蒙戈如果索引不包含分片键,_id指数有以下例外:如果一个分片集合的查询只指定一个条件在_id场上,只返回_id字段,_id指数可以覆盖查询运行时对蒙戈即使_id字段不是碎片的关键。

在以前的版本中,当对mongos运行时,索引不能覆盖切分集合上的查询。

d.说明

要确定查询是否为覆盖查询,请使用db.collection.explain()或explain()方法并查看结果。

db.collection.explain()提供关于其他操作执行情况的信息,比如db.collection.update()。有关详细信息,请参见db.collection.explain()。

六、评估当前操作的性能

以下部分描述了评估操作性能的技术。

1. 使用数据库分析器来评估对数据库的操作

MongoDB提供了一个数据库分析器,可以显示针对数据库的每个操作的性能特征。使用分析器来定位任何运行缓慢的查询或写操作。例如,您可以使用这些信息来确定要创建哪些索引。

从MongoDB 4.2开始,用于读写操作的分析器条目和诊断日志消息(即mongod/mongos日志消息)包括:

  • queryHash来帮助识别具有相同查询形状的慢速查询。
  • planCacheKey提供更多关于慢速查询的查询计划缓存的信息。

从4.2版开始(也可以从4.0.6版开始),副本集的次要成员现在会记录oplog条目,这些条目需要比慢速操作阈值更长的时间才能应用。这些慢速的oplog消息被记录在诊断日志的REPL组件下,其中的文本应用op: <oplog条目> take <num>ms。这些慢速oplog条目仅依赖于慢速操作阈值。它们不依赖于日志级别(系统级或组件级)、概要级别或缓慢的操作采样率。分析器不捕获慢的oplog条目。

2. 使用db.currentOp()来计算mongod操作

currentop()方法报告在mongod实例上运行的当前操作。

3. 使用explain评估查询性能

explain()方法和db.collection.explain()方法返回关于查询执行的信息,比如用来完成查询和执行统计的索引MongoDB。您可以在queryPlanner模式、executionStats模式或allPlansExecution模式下运行这些方法来控制返回的信息量。

例子:
在名为records的集合中查询匹配表达式{a: 1}的文档时,要使用类似于mongo shell中的以下操作:

db.records.find( { a: 1 } ).explain("executionStats")

从MongoDB 4.2开始,explain输出包括:

  • queryHash来帮助识别具有相同查询形状的慢速查询。
  • planCacheKey提供更多关于慢速查询的查询计划缓存的信息。

七、优化查询性能

1.创建索引来支持查询

对于通常发出的查询,创建索引。如果查询搜索多个字段,则创建复合索引。扫描索引比扫描集合快得多。索引结构比文档引用小,并按顺序存储引用。

例子:
如果您有一个包含博客文章的posts集合,并且您经常发出对author_name字段进行排序的查询,那么您可以通过在author_name字段上创建索引来优化查询:

db.posts.createIndex( { author_name : 1 } )

索引还可以提高对给定字段进行常规排序的查询的效率。

例子
如果您定期发出一个查询,对时间戳字段排序,那么您可以通过在时间戳字段上创建一个索引来优化查询:

创造这个指数:

db.posts.createIndex( { timestamp : 1 } )

优化这个查询:

db.posts.find().sort( { timestamp : -1 } )

因为MongoDB可以按升序和降序读取索引,所以单键索引的方向并不重要。
索引支持查询、更新操作和聚合管道的某些阶段。
BinData类型的索引键更有效地存储在索引中,如果:

  • 二进制子类型值的范围是0-7或128-135,
  • 字节数组的长度是:0、1、2、3、4、5、6、7、8、10、12、14、16、20、24或32。

2. 限制查询结果的数量以减少网络需求

MongoDB游标在多个文档组中返回结果。如果您知道想要的结果数量,那么可以通过使用limit()方法来减少对网络资源的需求。
这通常与排序操作一起使用。例如,如果您只需要从您的查询到posts集合的10个结果,您将发出以下命令:

db.posts.find().sort( { timestamp : -1 } ).limit(10)

3.使用投影只返回必要的数据

当您只需要文档中的一个字段子集时,您可以通过只返回您需要的字段来获得更好的性能:
例如,如果在对posts集合的查询中,只需要时间戳、标题、作者和抽象字段,则发出以下命令:

db.posts.find( {}, { timestamp : 1 , title : 1 , author : 1 , abstract : 1} ).sort( { timestamp : -1 } )

4. 使用$hint选择特定的索引

在大多数情况下,查询优化器为特定的操作选择最优的索引;但是,可以使用hint()方法强制MongoDB使用特定的索引。使用hint()支持性能测试,或者在某些查询中必须选择一个或多个索引中包含的字段。

5. 使用增量操作符执行服务器端操作

使用MongoDB的$inc操作符来递增或递减文档中的值。操作员在服务器端增加字段的值,作为选择文档的替代方法,在客户端进行简单修改,然后将整个文档写入服务器。$inc操作符还可以帮助避免竞争条件,当两个应用程序实例查询一个文档、手动增加一个字段并同时保存整个文档时,就会出现竞争条件。

八、写操作性能

1.索引

集合上的每个索引都会给写操作的性能增加一些开销。
对于集合上的每个插入或删除写操作,MongoDB从目标集合的每个索引中插入或删除相应的文档键。根据受更新影响的键,更新操作可能导致对集合上的索引子集的更新。

请注意
如果写入操作涉及的文档包含在索引中,MongoDB只更新稀疏或部分索引。

通常,索引为读操作提供的性能收益抵得上插入损失。但是,为了尽可能优化写性能,在创建新索引时要小心,并评估现有索引,以确保查询实际使用这些索引。
有关索引和查询,请参阅查询优化。有关索引的更多信息,请参见索引和索引策略。

2. 储存性能

2.1 硬件

存储系统的能力对MongoDB的写操作的性能造成了一些重要的物理限制。与驱动器的存储系统相关的许多独特因素会影响写性能,包括随机访问模式、磁盘缓存、磁盘预读和RAID配置。
对于随机工作负载,固态硬盘(ssd)的性能要比旋转硬盘(HDDs)高出100倍以上。

2.2 日志

为了在崩溃的情况下提供持久性,MongoDB使用写提前日志到磁盘上的日志。MongoDB首先将内存中的更改写入磁盘上的日志文件。如果MongoDB应该在将更改提交到数据文件之前终止或遇到错误,MongoDB可以使用日志文件对数据文件应用写操作。
虽然日志提供的持久性保证通常超过额外写操作的性能成本,但请考虑日志与性能之间的以下交互:

  • 如果日志和数据文件驻留在同一块设备上,则数据文件和日志可能必须争用有限数量的可用I/O资源。将日志移动到单独的设备可能会增加写操作的能力。
  • 如果应用程序指定了包含j选项的写问题,mongod将减少日志写之间的持续时间,这将增加总体的写负载。
  • 日志写之间的持续时间可以使用commitIntervalMs运行时选项进行配置。减少日志提交之间的时间间隔将增加写操作的数量,这可能会限制MongoDB的写操作能力。增加日志提交之间的时间间隔可能会减少写操作的总数,但也会增加日志在失败时不记录写操作的机会。

九、解释结果

为了返回查询计划和查询计划执行统计信息,MongoDB提供:

"winningPlan" : {
   "stage" : <STAGE1>,
   ...
   "inputStage" : {
      "stage" : <STAGE2>,
      ...
      "inputStage" : {
         "stage" : <STAGE3>,
         ...
      }
   }
},

每个阶段将其结果(即文档或索引键)传递给父节点。叶节点访问集合或索引。内部节点操作子节点产生的文档或索引键。根节点是MongoDB获取结果集的最后一个阶段。

阶段是对操作的描述;如:

  • 收集扫描的COLLSCAN
  • IXSCAN用于扫描索引键
  • 获取以获取文档
  • 用于合并来自碎片的结果的SHARD_MERGE
  • SHARDING_FILTER用于从碎片中过滤孤儿文档

3.解释输出

下面的部分给出了explain操作返回的一些关键字段的列表。

请注意:
字段列表并不意味着是详尽的,而是为了突出显示explain早期版本中的一些关键字段更改。
输出格式可能会在版本之间发生变化。

3.1 queryPlanner

queryPlanner信息详细描述了查询优化器选择的计划。

未分片集合

对于未分片的集合,explain返回以下queryPlanner信息:

"queryPlanner" : {
   "plannerVersion" : <int>,
   "namespace" : <string>,
   "indexFilterSet" : <boolean>,
   "parsedQuery" : {
      ...
   },
   "queryHash" : <hexadecimal string>,
   "planCacheKey" : <hexadecimal string>,
   "optimizedPipeline" : <boolean>, // Starting in MongoDB 4.2, only appears if true
   "winningPlan" : {
      "stage" : <STAGE1>,
      ...
      "inputStage" : {
         "stage" : <STAGE2>,
         ...
         "inputStage" : {
            ...
         }
      }
   },
   "rejectedPlans" : [
      <candidate plan 1>,
      ...
   ]
}

分片集合

对于切分集合,explain在shards字段中包含每个被访问的切分的核心查询规划器和服务器信息:

"queryPlanner" : {
   "mongosPlannerVersion" : <int>,
   "winningPlan" : {
      "stage" : <STAGE1>,
      "shards" : [
         {
            "shardName" : <string>,
            "connectionString" : <string>,
            "serverInfo" : {
               "host" : <string>,
               "port" : <int>,
               "version" : <string>,
               "gitVersion" : <string>
            },
            "plannerVersion" : <int>,
            "namespace" : <string>,
            "parsedQuery" : <document>,
            "queryHash" : <hexadecimal string>,
            "planCacheKey" : <hexadecimal string>,
            "optimizedPipeline" : <boolean>, // Starting in MongoDB 4.2, only appears if true
            "winningPlan" : {
               "stage" : <STAGE2>,
               "inputStage" : {
                  "stage" : <STAGE3>
                  ...,
               }
            },
            "rejectedPlans" : [
               <candidate plan 1>,
               ...
            ]
         },
         ...
      ]
   }
}

3.2 explain.queryPlanner

包含关于查询优化器选择查询计划的信息。

explain.queryPlanner.namespace

指定名称空间的字符串(即, <database>.<collection>)。

explain.queryPlanner.indexFilterSet

一个布尔值,指定MongoDB是否为查询形状应用了索引过滤器。

explain.queryPlanner.queryHash

一个十六进制字符串,表示查询形状的散列,仅依赖于查询形状。queryHash可以帮助识别具有相同查询形状的慢速查询(包括写操作的查询过滤器)。

请注意:
与任何散列函数一样,两个不同的查询形状可能会导致相同的散列值。但是,不太可能出现不同查询形状之间的哈希冲突。

有关queryHash和planCacheKey的更多信息,请参见queryHash和planCacheKey。
新版本4.2。

explain.queryPlanner.planCacheKey

与查询关联的计划缓存项的键的散列。
与queryHash不同,planCacheKey是查询形状和当前可用的形状索引的函数。也就是说,如果能够支持查询形状的索引被添加/删除,planCacheKey值可能会改变,而queryHash值不会改变。
有关queryHash和planCacheKey的更多信息,请参见queryHash和planCacheKey。

explain.queryPlanner.optimizedPipeline

一个布尔值,表示整个聚合管道操作被优化掉了,而是由查询计划执行阶段的树来完成。
例如,从MongodB 4.2开始,以下聚合操作可以通过查询计划执行树来完成,而不是使用聚合管道。

db.example.aggregate([ { $match: { someFlag: true } } ] )

只有当值为真且仅应用于解释聚合管道操作时,该字段才会出现。如果为真,则由于管道已经过优化,因此输出中不显示聚合阶段信息。
新版本4.2。

explain.queryPlanner.winningPlan

详细说明查询优化器选择的计划的文档。MongoDB将计划表示为阶段树;例如,一个阶段可以有一个inputStage,如果该阶段有多个子阶段,则可以有inputStages。

explain.queryPlanner.winningPlan.stage

表示舞台名称的字符串。
每个阶段由特定于该阶段的信息组成。例如,IXSCAN阶段将包括索引边界以及特定于索引扫描的其他数据。如果一个阶段有一个子阶段或多个子阶段,则该阶段将有一个或多个inputStage。

explain.queryPlanner.winningPlan.inputStage

描述子阶段的文档,该阶段向其父阶段提供文档或索引键。如果父级只有一个子级,则显示该字段。

explain.queryPlanner.winningPlan.inputStages

描述子阶段的一系列文档。子阶段向父阶段提供文档或索引键。如果父级有多个子节点,则显示该字段。例如,$或表达式或索引交集的阶段使用来自多个源的输入。

explain.queryPlanner.rejectedPlans

查询优化器考虑并拒绝的候选计划数组。如果没有其他候选计划,则数组可以为空。

executionStats

返回的executionStats信息详细说明了获胜计划的执行。为了在结果中包含executionStats,您必须在以下任一中运行explain:

  • 详细执行状态或
  • allPlansExecution模式。使用allPlansExecution模式来包含在计划选择期间捕获的部分执行数据。

Unsharded集合

对于未分片的集合,explain返回以下executionStats信息:

"executionStats" : {
   "executionSuccess" : <boolean>,
   "nReturned" : <int>,
   "executionTimeMillis" : <int>,
   "totalKeysExamined" : <int>,
   "totalDocsExamined" : <int>,
   "executionStages" : {
      "stage" : <STAGE1>
      "nReturned" : <int>,
      "executionTimeMillisEstimate" : <int>,
      "works" : <int>,
      "advanced" : <int>,
      "needTime" : <int>,
      "needYield" : <int>,
      "saveState" : <int>,
      "restoreState" : <int>,
      "isEOF" : <boolean>,
      ...
      "inputStage" : {
         "stage" : <STAGE2>,
         "nReturned" : <int>,
         "executionTimeMillisEstimate" : <int>,
         ...
         "inputStage" : {
            ...
         }
      }
   },
   "allPlansExecution" : [
      {
         "nReturned" : <int>,
         "executionTimeMillisEstimate" : <int>,
         "totalKeysExamined" : <int>,
         "totalDocsExamined" :<int>,
         "executionStages" : {
            "stage" : <STAGEA>,
            "nReturned" : <int>,
            "executionTimeMillisEstimate" : <int>,
            ...
            "inputStage" : {
               "stage" : <STAGEB>,
               ...
               "inputStage" : {
                 ...
               }
            }
         }
      },
      ...
   ]
}

分片集合

对于切分的集合,explain包含每个已访问切分的执行统计数据。

"executionStats" : {
   "nReturned" : <int>,
   "executionTimeMillis" : <int>,
   "totalKeysExamined" : <int>,
   "totalDocsExamined" : <int>,
   "executionStages" : {
      "stage" : <STAGE1>
      "nReturned" : <int>,
      "executionTimeMillis" : <int>,
      "totalKeysExamined" : <int>,
      "totalDocsExamined" : <int>,
      "totalChildMillis" : <NumberLong>,
      "shards" : [
         {
            "shardName" : <string>,
            "executionSuccess" : <boolean>,
            "executionStages" : {
               "stage" : <STAGE2>,
               "nReturned" : <int>,
               "executionTimeMillisEstimate" : <int>,
               ...
               "chunkSkips" : <int>,
               "inputStage" : {
                  "stage" : <STAGE3>,
                  ...
                  "inputStage" : {
                     ...
                  }
               }
            }
         },
         ...
      ]
   }
   "allPlansExecution" : [
      {
         "shardName" : <string>,
         "allPlans" : [
            {
               "nReturned" : <int>,
               "executionTimeMillisEstimate" : <int>,
               "totalKeysExamined" : <int>,
               "totalDocsExamined" :<int>,
               "executionStages" : {
                  "stage" : <STAGEA>,
                  "nReturned" : <int>,
                  "executionTimeMillisEstimate" : <int>,
                  ...
                  "inputStage" : {
                     "stage" : <STAGEB>,
                     ...
                     "inputStage" : {
                       ...
                     }
                  }
               }
            },
            ...
         ]
      },
      {
         "shardName" : <string>,
         "allPlans" : [
          ...
         ]
      },
      ...
   ]
}

explain.executionStats

包含描述获胜计划的完整查询执行的统计信息。对于写操作,已完成的查询执行指的是将要执行的修改,但不将修改应用于数据库。

explain.executionStats.nReturned

匹配查询条件的文档数量。在MongoDB的早期版本中,nreturn对应于由指针.explain()返回的n字段。

explain.executionStats.executionTimeMillis

查询计划选择和查询执行所需的总时间(毫秒)。executionTimeMillis对应于MongoDB早期版本中由指针.explain()返回的millis字段。

explain.executionStats.totalKeysExamined

扫描的索引项数。totalKeysExamined对应于MongoDB早期版本中由sor.explain()返回的nscan字段。

explain.executionStats.totalDocsExamined

在查询执行期间检查的文档数量。检查文档的常见查询执行阶段是COLLSCAN和FETCH。

请注意:
totaldocs是指已检查的文档总数,而不是返回的文档数量。例如,为了应用筛选器,阶段可以检查文档。如果文档被过滤掉,那么它已经被检查过,但是不会作为查询结果集的一部分返回。
如果在查询执行期间对文档进行多次检查,则totaldocs将对每次检查计数。也就是说,totaldocs不是检查的唯一文档总数的计数。

explain.executionStats.executionStages

以阶段树的形式详细说明获胜方案的完成执行情况;也就是说,一个阶段可以有一个或多个输入阶段。
每个阶段由特定于该阶段的执行信息组成。

explain.executionStats.executionStages.works

指定查询执行阶段执行的“工作单元”的数量。查询执行将其工作划分为小单元。“工作单元”可能包括检查单个索引键、从集合中提取单个文档、对单个文档应用投影或执行一项内部簿记。

explain.executionStats.executionStages.advanced

到此阶段返回或前进到其父阶段的中间结果的数量。

explain.executionStats.executionStages.needTime

没有将中间结果提前到其父阶段的工作周期的数量(参见explain. executionstates . executionstages .advanced)。例如,索引扫描阶段可能花费一个工作周期来寻找索引中的新位置,而不是返回索引键;这个工作循环包括解释、执行状态、执行阶段。需要时间而不是解释。

explain.executionStats.executionStages.needYield

存储层请求查询阶段挂起处理并产生其锁的次数。

explain.executionStats.executionStages.saveState

查询阶段挂起处理并保存其当前执行状态的次数,例如在准备生成其锁时的次数。

explain.executionStats.executionStages.restoreState

查询阶段恢复已保存的执行状态的次数,例如在恢复以前产生的锁之后。

explain.executionStats.executionStages.isEOF

指定执行阶段是否到达流的末端:

  • 如果为真或1,则执行阶段已经到达流的末端。
  • 如果为false或0,则阶段仍然可能返回结果。例如,考虑一个具有限制的查询,它的执行阶段由一个限制阶段和查询的输入阶段IXSCAN组成。如果查询返回的值超过指定的限制,limit阶段将报告isEOF: 1,但是其底层IXSCAN阶段将报告isEOF: 0。

explain.executionStats.executionStages.inputStage.keysExamined

对于扫描索引的查询执行阶段(例如IXSCAN), keys是在索引扫描过程中检查的内键和外键的总数。如果索引扫描包含单个连续范围的键,则只需要检查边界内键。如果索引范围由几个键范围组成,则索引扫描执行过程可能会检查出界限外的键,以便从一个范围的末尾跳到下一个范围的开头。

考虑下面的例子,其中有一个字段x的索引,集合包含100个文档,x值从1到100:

db.keys.find( { x : { $in : [ 3, 4, 50, 74, 75, 90 ] } } ).explain( "executionStats" )

查询将扫描键3和键4。然后,它将扫描键5,检测它是否越界,并跳到下一个键50。
继续这个过程,查询扫描键3、4、5、50、51、74、75、76、90和91。键5、51、76和91是仍在检查的越界键。keys的值是10。

explain.executionStats.executionStages.inputStage.docsExamined

指定在查询执行阶段扫描的文档数量。
表示COLLSCAN阶段,以及从集合中检索文档的阶段(例如FETCH)

explain.executionStats.executionStages.inputStage.seeks

新版本3.4:仅用于索引扫描(IXSCAN)阶段。
为了完成索引扫描,我们必须将索引光标定位到新位置的次数。

explain.executionStats.allPlansExecution

包含在计划选择阶段获取的部分执行信息,包括获胜和被拒绝的计划。只有当explain在allPlansExecution verbosity模式下运行时,才会出现该字段。

serverInfo

Unsharded Collections

对于未切分的集合,explain为MongoDB实例返回以下serverInfo信息:

"serverInfo" : {
   "host" : <string>,
   "port" : <int>,
   "version" : <string>,
   "gitVersion" : <string>
}

Sharded Collections

对于切分的集合,explain为每个被访问的切分返回serverInfo。

"queryPlanner" : {
   ...
   "winningPlan" : {
      "stage" : <STAGE1>,
      "shards" : [
         {
            "shardName" : <string>,
            "connectionString" : <string>,
            "serverInfo" : {
               "host" : <string>,
               "port" : <int>,
               "version" : <string>,
               "gitVersion" : <string>
            },
            ...
         }
         ...
      ]

4. 3.0格式改变

从MongoDB 3.0开始,explain结果的格式和字段与以前的版本有所不同。下面列出了一些关键的区别。

4.1 集合扫描与索引使用

如果查询计划器选择一个集合扫描,则解释结果包括一个COLLSCAN阶段。
如果查询计划器选择了一个索引,那么解释结果将包括一个IXSCAN阶段。该阶段包括诸如索引键模式、遍历方向和索引边界等信息。

在以前的MongoDB版本中,cursor.explain()返回的游标字段的值为:

  • 用于收集扫描的BasicCursor,
  • 和BtreeCursor <索引名>[<方向>]用于索引扫描。

4.2 Covered Queries

当索引覆盖查询时,MongoDB可以匹配查询条件并仅使用索引键返回结果;例如,MongoDB不需要检查集合中的文档来返回结果。
当一个索引覆盖了一个查询时,explain结果的IXSCAN阶段不是FETCH阶段的后代,在executionStats中,totaldocs为0。
在MongoDB的早期版本中,sor.explain()返回了indexOnly字段,以指示索引是否覆盖了查询。

4.3 Index Intersection

对于索引交集计划,结果将包括一个and_ordered阶段或一个AND_HASH阶段,其中包含详细说明索引的inputStages数组;例如:

{
   "stage" : "AND_SORTED",
   "inputStages" : [
      {
         "stage" : "IXSCAN",
         ...
      },
      {
         "stage" : "IXSCAN",
         ...
      }
   ]
}

在以前的MongoDB版本中,cursor.explain()返回游标字段,其值为索引交叉点的复杂计划。

4.4 $or Expression

如果MongoDB对$or表达式使用索引,结果将包含包含详细说明索引的inputStages数组的or;例如:

{
   "stage" : "OR",
   "inputStages" : [
      {
         "stage" : "IXSCAN",
         ...
      },
      {
         "stage" : "IXSCAN",
         ...
      },
      ...
   ]
}

在以前的MongoDB版本中,sor.explain()返回详细说明索引的子句数组。

4.5 Sort Stage

如果MongoDB可以使用索引扫描来获得请求的排序顺序,结果将不包括排序阶段。否则,如果MongoDB不能使用索引进行排序,explain结果将包括一个排序阶段。
在MongoDB 3.0之前,sor.explain()返回scanAndOrder字段来指定MongoDB是否可以使用索引顺序来返回排序后的结果。

十、分析查询性能

explain(“executionStats”)和db.collection.explain(“executionStats”)方法提供关于查询性能的统计信息。这些统计信息对于度量查询是否使用索引以及如何使用索引非常有用。
explain()提供关于其他操作执行情况的信息,比如db.collection.update()。有关详细信息,请参见db.collection.explain()。

1.评估查询的性能

考虑一个收集清单与下列文件:

{ "_id" : 1, "item" : "f1", type: "food", quantity: 500 }
{ "_id" : 2, "item" : "f2", type: "food", quantity: 100 }
{ "_id" : 3, "item" : "p1", type: "paper", quantity: 200 }
{ "_id" : 4, "item" : "p2", type: "paper", quantity: 150 }
{ "_id" : 5, "item" : "f3", type: "food", quantity: 300 }
{ "_id" : 6, "item" : "t1", type: "toys", quantity: 500 }
{ "_id" : 7, "item" : "a1", type: "apparel", quantity: 250 }
{ "_id" : 8, "item" : "a2", type: "apparel", quantity: 400 }
{ "_id" : 9, "item" : "t2", type: "toys", quantity: 50 }
{ "_id" : 10, "item" : "f4", type: "food", quantity: 75 }

2. 无索引查询

下面的查询检索quantity字段值在100到200之间的文档,包括:

db.inventory.find( { quantity: { $gte: 100, $lte: 200 } } )

查询返回以下文件:

{ "_id" : 2, "item" : "f2", "type" : "food", "quantity" : 100 }
{ "_id" : 3, "item" : "p1", "type" : "paper", "quantity" : 200 }
{ "_id" : 4, "item" : "p2", "type" : "paper", "quantity" : 150 }

要查看所选的查询计划,请将光标.explain(“executionStats”)光标方法链接到find命令的末尾:

db.inventory.find(
   { quantity: { $gte: 100, $lte: 200 } }
).explain("executionStats")

explain()返回以下结果:

{
   "queryPlanner" : {
         "plannerVersion" : 1,
         ...
         "winningPlan" : {
            "stage" : "COLLSCAN",
            ...
         }
   },
   "executionStats" : {
      "executionSuccess" : true,
      "nReturned" : 3,
      "executionTimeMillis" : 0,
      "totalKeysExamined" : 0,
      "totalDocsExamined" : 10,
      "executionStages" : {
         "stage" : "COLLSCAN",
         ...
      },
      ...
   },
   ...
}
  • queryPlanner.winningPlan.stage显示COLLSCAN以指示收集扫描。
  • 收集扫描表明mongod必须逐文档扫描整个收集文档以识别结果。这通常是一个开销很大的操作,可能会导致查询速度变慢。
  • executionStats.nReturned显示3表示查询匹配并返回三个文档。
  • executionStats.totalkeys显示0表示此查询没有使用索引。
  • executionStats.totalDocsExamined显示10表明MongoDB必须扫描10个文档(即集合中的所有文档)才能找到三个匹配的文档。

匹配文档数量和检查文档数量之间的差异可能表明,为了提高效率,查询可能会受益于索引的使用。

3. 查询与索引

要支持数量字段上的查询,请在数量字段上添加一个索引:

db.inventory.createIndex( { quantity: 1 } )

要查看查询计划统计信息,请使用explain(“executionStats”)方法:

db.inventory.find(
   { quantity: { $gte: 100, $lte: 200 } }
).explain("executionStats")

explain()方法返回以下结果:

{
   "queryPlanner" : {
         "plannerVersion" : 1,
         ...
         "winningPlan" : {
               "stage" : "FETCH",
               "inputStage" : {
                  "stage" : "IXSCAN",
                  "keyPattern" : {
                     "quantity" : 1
                  },
                  ...
               }
         },
         "rejectedPlans" : [ ]
   },
   "executionStats" : {
         "executionSuccess" : true,
         "nReturned" : 3,
         "executionTimeMillis" : 0,
         "totalKeysExamined" : 3,
         "totalDocsExamined" : 3,
         "executionStages" : {
            ...
         },
         ...
   },
   ...
}
  • queryPlanner.winningPlan.inputStage.stage显示IXSCAN以指示索引的使用。
  • executionStats.nReturned显示3表示查询匹配并返回三个文档。
  • executionStats.totalKeysExamined显示3表明MongoDB扫描了三个索引条目。检查的键的数量与返回的文档的数量相匹配,这意味着mongod只需要检查索引键就可以返回结果。mongod不必扫描所有的文档,只需将三个匹配的文档放入内存。这将产生一个非常有效的查询。
  • executionStats.totaldocsdisplay 3表明MongoDB扫描了三个文档。

如果没有索引,查询将扫描整个10个文档集合,以返回3个匹配的文档。该查询还必须扫描每个文档的全部内容,可能会将它们拖到内存中。这将导致昂贵且可能缓慢的查询操作。

当使用索引运行时,查询扫描3个索引条目和3个文档,以返回3个匹配的文档,从而得到一个非常高效的查询。

4. 指标性能比较

要使用多个索引手动比较查询的性能,可以将hint()方法与explain()方法结合使用。

考虑以下查询:

db.inventory.find( {
   quantity: {
      $gte: 100, $lte: 300
   },
   type: "food"
} )

查询返回以下文件:

{ "_id" : 2, "item" : "f2", "type" : "food", "quantity" : 100 }
{ "_id" : 5, "item" : "f3", "type" : "food", "quantity" : 300 }

要支持查询,请添加一个复合索引。对于复合索引,字段的顺序很重要。
例如,添加以下两个复合索引。第一个索引按数量字段排序,然后是type字段。第二个索引首先按类型排序,然后是quantity字段。

db.inventory.createIndex( { quantity: 1, type: 1 } )
db.inventory.createIndex( { type: 1, quantity: 1 } )

评估第一个索引对查询的影响:

db.inventory.find(
   { quantity: { $gte: 100, $lte: 300 }, type: "food" }
).hint({ quantity: 1, type: 1 }).explain("executionStats")

explain()方法返回以下输出:

{
   "queryPlanner" : {
      ...
      "winningPlan" : {
         "stage" : "FETCH",
         "inputStage" : {
            "stage" : "IXSCAN",
            "keyPattern" : {
               "quantity" : 1,
               "type" : 1
            },
            ...
            }
         }
      },
      "rejectedPlans" : [ ]
   },
   "executionStats" : {
      "executionSuccess" : true,
      "nReturned" : 2,
      "executionTimeMillis" : 0,
      "totalKeysExamined" : 5,
      "totalDocsExamined" : 2,
      "executionStages" : {
      ...
      }
   },
   ...
}

MongoDB扫描了5个索引键(executionstats . totalkeys)来返回2个匹配的文档(executionStats.nReturned)。
评估第二个索引对查询的影响:

db.inventory.find(
   { quantity: { $gte: 100, $lte: 300 }, type: "food" }
).hint({ type: 1, quantity: 1 }).explain("executionStats")

explain()方法返回以下输出:

{
   "queryPlanner" : {
      ...
      "winningPlan" : {
         "stage" : "FETCH",
         "inputStage" : {
            "stage" : "IXSCAN",
            "keyPattern" : {
               "type" : 1,
               "quantity" : 1
            },
            ...
         }
      },
      "rejectedPlans" : [ ]
   },
   "executionStats" : {
      "executionSuccess" : true,
      "nReturned" : 2,
      "executionTimeMillis" : 0,
      "totalKeysExamined" : 2,
      "totalDocsExamined" : 2,
      "executionStages" : {
         ...
      }
   },
   ...
}

MongoDB扫描了2个索引键(executionstats . totalkeys)来返回2个匹配的文档(executionStats.nReturned)。
对于这个示例查询,复合索引{type: 1, quantity: 1}比复合索引{quantity: 1, type: 1}更有效。

十一、Tailable游标

默认情况下,当客户机耗尽游标中的所有结果时,MongoDB将自动关闭游标。但是,对于有上限的集合,您可以使用一个Tailable游标,它在客户端使用完初始游标中的结果后仍然保持打开状态。Tailable游标在概念上相当于tail Unix命令的-f选项(即“follow”模式)。在客户端将新的附加文档插入一个有上限的集合之后,tailable游标将继续检索文档。

在索引不实用的有高写卷的封顶集合上使用可尾游标。例如,MongoDB复制使用可跟踪的游标跟踪主节点的oplog。

请注意:
如果您的查询是在索引字段上,不要使用tailable游标,而是使用常规游标。跟踪查询返回的索引字段的最后一个值。要检索新添加的文档,请再次使用查询条件中索引字段的最后一个值来查询集合,如下面的示例所示:

db.<collection>.find( { indexedField: { $gt: <lastvalue> } } )

考虑以下与可裁减游标相关的行为:

  • 可裁减的游标不使用索引并按自然顺序返回文档。
  • 因为tailable游标不使用索引,所以对查询的初始扫描可能比较昂贵;但是,在最初耗尽游标之后,后续检索新添加的文档是不昂贵的。
  • 可尾随的光标可能会死亡或无效,如果:(1)查询没有返回匹配项。(2)游标在集合的“末端”返回文档,然后应用程序删除该文档。

死游标的id为0。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值