MongoDB 基础系列十五:增删查改 CRUD 之 Query - Cursor in Mongo Shell

前言

此篇博文是 Mongdb 基础系列之一;

本文为作者的原创作品,转载需注明出处;

简介

The db.collection.find() method returns a cursor. To access the documents, you need to iterate the cursor. However, in the mongo shell, if the returned cursor is not assigned to a variable using the var keyword, then the cursor is automatically iterated up to 20 times [1] to print up to the first 20 documents in the results.

db.collection.find() 返回一个cursor;你需要通过一个cursor来访问文档;在 mongo shell 中,如果返回的 cursor 对象并没有赋值给任何一个局部变量,那么默认,该 cursor 将会执行 20 次,并输出前 20 个 documents;

本章,我们使用下面的测试用例,来对本章的内容进行讲解,

1
2
3
4
5
6
7
db.inventory.insertMany([
{ item: "journal", qty: 25, size: { h: 14, w: 21, uom: "cm" }, status: "A" },
{ item: "notebook", qty: 50, size: { h: 8.5, w: 11, uom: "in" }, status: "A" },
{ item: "paper", qty: 100, size: { h: 8.5, w: 11, uom: "in" }, status: "D" },
{ item: "planner", qty: 75, size: { h: 22.85, w: 30, uom: "cm" }, status: "D" },
{ item: "postcard", qty: 45, size: { h: 10, w: 15.25, uom: "cm" }, status: "A" }
]);

手动的对 Cursor 进行遍历

直接打印 cursor

1
> var myCursor = db.inventory.find({})

我们通过查询 inventory 中所有的 elements 元素返回一个 cursor,并将该 cursor 引用赋值给 myCursor 局部变量;通过直接输出 myCursor,mongo shell 会输出头 20 个 documents 元素,当然,因为我们的测试用例中总共就 5 个元素,所以,这里只会输出这 5 个元素;

1
2
3
4
5
6
> myCursor
{ "_id" : ObjectId("597704eecb568ef80824a9c9"), "item" : "journal", "qty" : 25, "size" : { "h" : 14, "w" : 21, "uom" : "cm" }, "status" : "A" }
{ "_id" : ObjectId("597704eecb568ef80824a9ca"), "item" : "notebook", "qty" : 50, "size" : { "h" : 8.5, "w" : 11, "uom" : "in" }, "status" : "A" }
{ "_id" : ObjectId("597704eecb568ef80824a9cb"), "item" : "paper", "qty" : 100, "size" : { "h" : 8.5, "w" : 11, "uom" : "in" }, "status" : "D" }
{ "_id" : ObjectId("597704eecb568ef80824a9cc"), "item" : "planner", "qty" : 75, "size" : { "h" : 22.85, "w" : 30, "uom" : "cm" }, "status" : "D" }
{ "_id" : ObjectId("597704eecb568ef80824a9cd"), "item" : "postcard", "qty" : 45, "size" : { "h" : 10, "w" : 15.25, "uom" : "cm" }, "status" : "A" }

使用 next() 方法迭代输出

你也可以使用与 cursor 的 next() 方法来遍历访问 documents,如下所述,

1
2
3
4
5
var myCursor = db.inventory.find({});

while (myCursor.hasNext()) {
print(tojson(myCursor.next()));
}

输出结果,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
{
"_id" : ObjectId("597704eecb568ef80824a9c9"),
"item" : "journal",
"qty" : 25,
"size" : {
"h" : 14,
"w" : 21,
"uom" : "cm"
},
"status" : "A"
}
{
"_id" : ObjectId("597704eecb568ef80824a9ca"),
"item" : "notebook",
"qty" : 50,
"size" : {
"h" : 8.5,
"w" : 11,
"uom" : "in"
},
"status" : "A"
}
{
"_id" : ObjectId("597704eecb568ef80824a9cb"),
"item" : "paper",
"qty" : 100,
"size" : {
"h" : 8.5,
"w" : 11,
"uom" : "in"
},
"status" : "D"
}
{
"_id" : ObjectId("597704eecb568ef80824a9cc"),
"item" : "planner",
"qty" : 75,
"size" : {
"h" : 22.85,
"w" : 30,
"uom" : "cm"
},
"status" : "D"
}
{
"_id" : ObjectId("597704eecb568ef80824a9cd"),
"item" : "postcard",
"qty" : 45,
"size" : {
"h" : 10,
"w" : 15.25,
"uom" : "cm"
},
"status" : "A"
}

注意,上述的输出方法 print(tojson(myCursor.next())) 可以直接替换为 printjson()

1
2
3
4
5
var myCursor = db.inventory.find({});

while (myCursor.hasNext()) {
printjson(myCursor.next());
}

使用 forEach() 方法输出

除了使用 next() 来进行输出以外,我们还可以使用 forEach() 方法来进行输出,

1
2
> var myCursor = db.inventory.find({})
> myCursor.forEach(printjson);

输出结果,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
{
"_id" : ObjectId("597704eecb568ef80824a9c9"),
"item" : "journal",
"qty" : 25,
"size" : {
"h" : 14,
"w" : 21,
"uom" : "cm"
},
"status" : "A"
}
{
"_id" : ObjectId("597704eecb568ef80824a9ca"),
"item" : "notebook",
"qty" : 50,
"size" : {
"h" : 8.5,
"w" : 11,
"uom" : "in"
},
"status" : "A"
}
{
"_id" : ObjectId("597704eecb568ef80824a9cb"),
"item" : "paper",
"qty" : 100,
"size" : {
"h" : 8.5,
"w" : 11,
"uom" : "in"
},
"status" : "D"
}
{
"_id" : ObjectId("597704eecb568ef80824a9cc"),
"item" : "planner",
"qty" : 75,
"size" : {
"h" : 22.85,
"w" : 30,
"uom" : "cm"
},
"status" : "D"
}
{
"_id" : ObjectId("597704eecb568ef80824a9cd"),
"item" : "postcard",
"qty" : 45,
"size" : {
"h" : 10,
"w" : 15.25,
"uom" : "cm"
},
"status" : "A"
}

References

See JavaScript cursor methods and your driver documentation for more information on cursor methods.

将 Cursor 转换成数组

在 mongo shell 中,可以使用 toArray() 方法遍历一个 cursor 并将相关的结果通过一个数组进行返回;看下面这个例子,如果我们想要快速的获取得到当前 cursor 所返回的 documents 的第四个元素,注意,第一个元素从下表 0 开始;

1
2
3
> var myCursor = db.inventory.find({})
> var documentArray = myCursor.toArray()
> var myDocument = documentArray[3]

输出

1
2
3
4
5
6
7
8
9
10
11
12
> myDocument
{
"_id" : ObjectId("597704eecb568ef80824a9cc"),
"item" : "planner",
"qty" : 75,
"size" : {
"h" : 22.85,
"w" : 30,
"uom" : "cm"
},
"status" : "D"
}

额外的,某些 drivers 允许直接在 cursor 对象上使用 index 来快速查询某个下标所对应的 document,比如使用 cursor[index],这实际上是通过 toArray() 实现下标访问的一种快捷方法,下面的两种调用方式是等价的,

1
2
> var myCursor = db.inventory.find({})
> var myDocument = myCursor[1];

使用 toArray() 的方式,

1
myCursor.toArray() [1];

Cursor 特性

关闭不活跃的 Cursor

默认情况下,在 10 分钟以后 MongoDB 将会自动关闭不活跃的 Cursor 连接;如果要禁用这个行为,可以使用 cursor 方法 cursor.noCursorTimeout()

1
var myCursor = db.users.find().noCursorTimeout();

记住,一旦你设置了 noCursorTimeout,就记得一定要通过 cursor.close() 方法来手动关闭连接;

Cursor 隔离

当一个 cursor 正在返回 documents 的时候,其它的操作可能对正在返回的 documents 进行并发操作;比如当前我们所使用的是 MMAPv1 存储引擎,当在返回 documents 的同时对该文档进行写入操作(比如 udpate ),那么会导致一个 cursor 有可能对某个特定的 document 返回多次;如果解决这个问题,参考 snapshot mode

Cursor Batches

MongoDB Servers 以 batch 的方式返回查询的结果;batch 中数据的总大小不会超过 BSON document size 的最大值,也就是 16M;(注意,BSON Document size 这里有两层意思,一个是如果是单个文档,那么最大值不能超过这个数;如果是 query 的返回结果的 batch,那么这个 batch 的总大小也不能超过这个数 );可以通过 batchSize()limit()方法来修改这个默认值;

New in version 3.4: Operations of type find(), aggregate(), listIndexes, and listCollections return a maximum of 16 megabytes per batch. batchSize() can enforce a smaller limit, but not a larger one.

注意,这里提到了,通过 batchSize() 方法只能讲 batch size 调小,没有办法增大;

For queries that include a sort operation without an index, the server must load all the documents in memory to perform the sort before returning any results.

这里提到了,如果在查询的过程中,有 sort 的操作,而查询的对象没有 index,那么将会把所有的 documents 全部加载进入内存中,进行排序操作;

当你遍历完当前的 cursor batch 以后,如果还有更多的数据,cursor.next() 会执行一个 getMore operation 去获取下一个 batch 的数据;如果要查看当前 batch 中还有多少个 documents 没有遍历,可以使用 objsLeftInBatch() 方法;看下面这个例子,

1
2
> var myCursor = db.inventory.find({})
> var myFirstDocument = myCursor.hasNext() ? myCursor.next() : null;

我们遍历了 myCursor 中的一个元素,并将其保存到 myFirstDocument 变量中,那么当前的 batch 中嗨剩下多少个元素呢?

1
2
> myCursor.objsLeftInBatch();
4

Cursor 的当前状态信息

db.serverStatus() 将会返回一个文档,包含了一个 metrics 字段,该字段的内容包含了当前 MongoDB 实例的运行状态和使用情况;该 metrics 字段的内容中包含了一个 metrics.cursor 字段,包含了如下的信息,

  • number of timed out cursors since the last server restart
    自动服务启动以来,有多少个 cursors 超时了;
  • number of open cursors with the option DBQuery.Option.noTimeout set to prevent timeout after a period of inactivity
  • number of “pinned” open cursors
  • total number of open cursors

获取当前 cursor 的信息,

1
> db.serverStatus().metrics.cursor

将会输出如下的信息,

1
2
3
4
5
6
7
8
{
"timedOut" : NumberLong(0),
"open" : {
"noTimeout" : NumberLong(0),
"pinned" : NumberLong(0),
"total" : NumberLong(0)
}
}

References

https://docs.mongodb.com/manual/tutorial/iterate-a-cursor/