在本文中我们将学习流式聚合操作,并深入了解语句的执行效率。然后深入学习能够提高数据服务可用性的复制集。接着了解 MongoDB 的水平扩展能力,学习 MongoDB 数据的备份与还原方法,并为数据服务开启访问控制。
基础篇 一 文档的 CRUD 操作
CRUD 操作指的是对文档进行 create
,read
,update
and delete
操作,即增删改查。文档 CRUD 操作的内容将分为 Create Operations
, Read Operations
, Update Operations
, Delete Operations
和 Cursor
等 5 个部分进行介绍。
Create Operations
创建操作或者插入操作会向集合添加新的文档。之前有提到过,如果插入时集合不存在,插入操作会创建对应的集合。MongoDB 提供了 3 个插入文档的方法:
插入单个文档
其中,db.collection.insertOne()
用于向集合插入单个文档。而 db.collection.insertMany()
和 db.collection.insert()
可以向集合插入多个文档。db.collection.insertOne()
示例如下:
> db.zenrust.insertOne({
... nickname: "Rust 之禅",
... name: "zenrust",
... types: "订阅号",
... descs:"超酷人生,我用 Rust"
... })
自动命令执行后会返回一个结果文档,文档输出如下:
{
"acknowledged" : true,
"insertedId" : ObjectId("5d157fe26fcb85935e9cb786")
}
这说明文档插入成功。其中,acknowledged
代表本次操作的操作状态,状态值包括 true
和 false
。insertedId
即该文档的 _id
。
提示:示例中的省略号是 MongoShell 的换行标识符。换行标识符对命令输入和执行并没有影响,所以本文也不会注重风格的统一,即示例中有时会带有换行符,有时则不带有换行符。
插入多个文档
db.collection.insertMany()
示例如下:
> db.zenrust.insertMany([
... {nickname: "Rust 之禅", name: "zenrust", types: "订阅号", descs: "超酷人生,我用 Rust"},
... {nickname: "进击的 Coder", name: "FightingCoder", types: "订阅号", descs: "分享爬虫技术和机器学习方面的编程经验"}
... ])
由于本次插入了 2 个文档,所以返回的结果文档会显示两个 _id
。返回文档内容如下:
{
"acknowledged" : true,
"insertedIds" : [
ObjectId("5d1582136fcb85935e9cb787"),
ObjectId("5d1582136fcb85935e9cb788")
]
}
db.collection.insert()
示例如下:
> db.zenrust.insert({title: "全面认识 RUST,掌控未来的雷电"})
示例演示的是单个文档的插入,实际上插入多个文档也是没问题的。db.collection.insert()
插入单个文档时返回的是一个带有操作状态的 WriteResult
对象:WriteResult({ "nInserted" : 1 })
。其中,nInserted
表明了插入文档的总数。但如果插入操作遇到错误,那么 WriteResult
对象将包含错误提示信息。
db.collection.insert()
插入多个文档的示例如下:
> db.zenrust.insert([{nickname: "进击的 Coder"}, {nickname: "Rust 之禅"}])
BulkWriteResult({
"writeErrors" : [ ],
"writeConcernErrors" : [ ],
"nInserted" : 2,
"nUpserted" : 0,
"nMatched" : 0,
"nModified" : 0,
"nRemoved" : 0,
"upserted" : [ ]
})
可以看到,db.collection.insert()
插入多个文档和插入单个文档得到的返回结果是不同的。
Read Operations
MongoDB 提供了 db.collection.find()
方法从集合中读取文档。在开始练习之前,需要准备用于练习的基础数据。在 MongoShell 中执行以下文档插入操作:
> db.inven.insertMany([
{ name: "詹姆斯", number: 6, attribute: { h: 203, w: 222, p: "前锋" }, status: "A" },
{ name: "韦德", number: 3, attribute: { h: 193, w: 220, p: "得分后卫" }, status: "R" },
{ name: "科比", number: 24, attribute: { h: 198, w: 212, p: "得分后卫" }, status: "R" },
{ name: "姚明", number: 11, attribute: { h: 226, w: 308, p: "中锋" }, status: "R" },
{ name: "乔丹", number: 23, attribute: { h: 198, w: 216, p: "得分后卫" }, status: "R" }
])
查询文档
将一个空位当作为查询过滤器参数传递给 db.collection.find()
方法就可以得到所有文档,对应示例如下:
> db.inven.find({})
或者什么都不传,直接使用 find()
,对应示例如下:
> db.inven.find()
这等效于 SQL 中的 SELECT * FROM inven
。
指定等式条件
如果要指定相等条件,可以使用 {<field1>: <value1>, ...}
这样的过滤表达式,例如过滤出已退役球员("R" 代表退役)的查询语句如下:
> db.inven.find({status: "R"})
{ "_id" : ObjectId("5d159e794d3d891430a2512e"), "name" : "韦德", "number" : 3, "attribute" : { "h" : 193, "w" : 220, "p" : "得分后卫" }, "status" : "R" }
{ "_id" : ObjectId("5d159e794d3d891430a2512f"), "name" : "科比", "number" : 24, "attribute" : { "h" : 198, "w" : 212, "p" : "得分后卫" }, "status" : "R" }
{ "_id" : ObjectId("5d159e794d3d891430a25130"), "name" : "姚明", "number" : 11, "attribute" : { "h" : 226, "w" : 308, "p" : "中锋" }, "status" : "R" }
{ "_id" : ObjectId("5d159e794d3d891430a25131"), "name" : "乔丹", "number" : 23, "attribute" : { "h" : 198, "w" : 216, "p" : "得分后卫" }, "status" : "R" }
这等效于 SQL 中的 SELECT * FROM inven WHERE status = "R"
。
根据嵌套文档字段查询
我们还可以根据嵌入式文档中的字段进行查询,例如过滤出球员属性中身高为 193
的球员,对应示例如下:
> db.inven.find({"attribute.h": 193})
{ "_id" : ObjectId("5d159e794d3d891430a2512e"), "name" : "韦德", "number" : 3, "attribute" : { "h" : 193, "w" : 220, "p" : "得分后卫" }, "status" : "R" }
要注意的是,访问嵌入式文档中的字段时使用的并不是 attribute.h
,而是使用 "attribute.h"
。
查询与投影操作
查询的情况非常复杂,MongoDB 提供了多种查询操作符来应对这些问题。MongoDB 提供的查询操作符分为以下几类:
- 比较查询操作符
- 逻辑查询操作符
- 元素查询操作符
- 评估查询操作符
- 地理空间查询操作符
- 数组查询操作符
- 按位查询操作符
接下来,我们将学习每一种查询操作符的规则和语法。
比较查询操作符
比较是最常见的操作之一,它分为同类型比较和非同类型比较。在面对不同的 BSON 类型值时,比较的并不是值的大小,而是值的类型,即按类比较。MongoDB 使用以下比较顺序,顺序从低到高:
- MinKey (internal type)
- Null
- Numbers (ints, longs, doubles, decimals)
- Symbol, String
- Object
- Array
- BinData
- ObjectId
- Boolean
- Date
- Timestamp
- Regular Expression
- MaxKey (internal type)
同类型比较的情况则稍微复杂一些。数字类型比较的是值的大小,例如 5
大于 3
。字符串类型比较的是其值的二进制,例如 R
大于 A
是因为 R
的二进制值 0101 0010
大于 A
的二进制值 0100 0001
。数组的小于比较或者升序排序比较的是数组中的最小元素,大于比较或降序排序比较的是数组中的最大元素。我们可以通过一个例子来了解这些知识。准备如下数据:
> db.arrs.insertMany([
... {name: "James", attr: [5, 6, 7]},
... {name: "Wade", attr: [1, 7, 8]},
... {name: "Kobe", attr: [1, 9, 9]},
... {name: "Bosh", attr: [2, 9, 9]}
... ])
假设要将文档按 name
升序排序,即字符串升序排序。对应示例如下:
> db.arrs.find().sort({name: 1})
{ "_id" : ObjectId("5d1eb7fef91d329d7e731d4f"), "name" : "Bosh", "attr" : [ 2, 9, 9 ] }
{ "_id" : ObjectId("5d1eb7fef91d329d7e731d4c"), "name" : "James", "attr" : [ 5, 6, 7 ] }
{ "_id" : ObjectId("5d1eb7fef91d329d7e731d4e"), "name" : "Kobe", "attr" : [ 1, 9, 9 ] }
{ "_id" : ObjectId("5d1eb7fef91d329d7e731d4d"), "name" : "Wade", "attr" : [ 1, 7, 8 ] }
排序结果为 Bosh- James - Kobe - Wade
,那么字符串降序排序的结果一定是 Wade - Kobe - James - Bosh
。对应示例如下:
> db.arrs.find().sort({name: -1})
{ "_id" : ObjectId("5d1eb7fef91d329d7e731d4d"), "name" : "Wade", "attr" : [ 1, 7, 8 ] }
{ "_id" : ObjectId("5d1eb7fef91d329d7e731d4e"), "name" : "Kobe", "attr" : [ 1, 9, 9 ] }
{ "_id" : ObjectId("5d1eb7fef91d329d7e731d4c"), "name" : "James", "attr" : [ 5, 6, 7 ] }
{ "_id" : ObjectId("5d1eb7fef91d329d7e731d4f"), "name" : "Bosh", "attr" : [ 2, 9, 9 ] }
现有:[ 5, 6, 7 ], [ 1, 7, 8 ], [ 1, 9, 9 ], [ 2, 9, 9 ]
4 个数组,上面提到,数组升序排序比较的是最小元素。4 个数组中最小的值分别是 5, 1, 1, 2
,其中数组 [1, 7, 8]
和数组 [1, 9, 9]
的最小值相同,则比较第二小的值,即 7, 9
。那么正确的升序排序结果因该是 [ 1, 7, 8 ], [ 1, 9, 9 ], [ 2, 9, 9 ], [5, 6, 7]
。数组升序排序命令如下:
> db.arrs.find().sort({attr: 1})
{ "_id" : ObjectId("5d1eb7fef91d329d7e731d4d"), "name" : "Wade", "attr" : [ 1, 7, 8 ] }
{ "_id" : ObjectId("5d1eb7fef91d329d7e731d4e"), "name" : "Kobe", "attr" : [ 1, 9, 9 ] }
{ "_id" : ObjectId("5d1eb7fef91d329d7e731d4f"), "name" : "Bosh", "attr" : [ 2, 9, 9 ] }
{ "_id" : ObjectId("5d1eb7fef91d329d7e731d4c"), "name" : "James", "attr" : [ 5, 6, 7 ] }
排序结果与分析结果一致。数组降序排序比较的是最大元素。4 个数组中最大的值分别是 7, 8, 9, 9
,其中数组 [1, 9, 9]
和 [2, 9, 9]
的最大值和第二大的值相同,则比较第三大的值,即 1, 2
。那么正确的降序排序结果应该是 [2, 9, 9], [1, 9, 9], [1, 7, 8], [5, 6, 7]
。数组降序排序命令如下:
> db.els.find().sort({attr: -1})
{ "_id" : ObjectId("5d1edd28eb81ddef9df74fff"), "name" : "Kobe", "attr" : [ 1, 9, 9 ] }
{ "_id" : ObjectId("5d1edd28eb81ddef9df75000"), "name" : "Bosh", "attr" : [ 2, 9, 9 ] }
{ "_id" : ObjectId("5d1edd28eb81ddef9df74ffe"), "name" : "Wade", "attr" : [ 1, 7, 8 ] }
{ "_id" : ObjectId("5d1edd28eb81ddef9df74ffd"), "name" : "James", "attr" : [ 5, 6, 7 ] }
排序结果和分析结果并不同,这是为什么呢?难道不是按最大元素比较大小吗?
文档中并没有提到,但我们可以通过例子寻找答案。准备以下数据:
> db.parts.insertMany([
... {name: 1, attr: [9, 9, 0, 5]},
... {name: 2, attr: [9, 9, 0, 1]},
... {name: 3, attr: [9, 0, 9, 0]},
... {name: 4, attr: [9, 8, 7, 6]},
... {name: 5, attr: [5, 2, 3, 6]},
... {name: 6, attr: [9, 0, 0, 0]},
... {name: 7, attr: [30, 0]},
... {name: 8, attr: [22, 0]},
... {name: 9, attr: [30, 5]},
... {name: 10, attr: [30, 3]}
... ])
数组降序排序示例如下:
> db.parts.find().sort({attr: -1})
{ "_id" : ObjectId("5d1eef27882bacc4ec4cb121"), "name" : 7, "attr" : [ 30, 0 ] }
{ "_id" : ObjectId("5d1eef27882bacc4ec4cb123"), "name" : 9, "attr" : [ 30, 5 ] }
{ "_id" : ObjectId("5d1eef27882bacc4ec4cb124"), "name" : 10, "attr" : [ 30, 3 ] }
{ "_id" : ObjectId("5d1eef27882bacc4ec4cb122"), "name" : 8, "attr" : [ 22, 0 ] }
{ "_id" : ObjectId("5d1eef27882bacc4ec4cb11b"), "name" : 1, "attr" : [ 9, 9, 0, 5 ] }
{ "_id" : ObjectId("5d1eef27882bacc4ec4cb11c"), "name" : 2, "attr" : [ 9, 9, 0, 1 ] }
{ "_id" : ObjectId("5d1eef27882bacc4ec4cb11d"), "name" : 3, "attr" : [ 9, 0, 9, 0 ] }
{ "_id" : ObjectId("5d1eef27882bacc4ec4cb11e"), "name" : 4, "attr" : [ 9, 8, 7, 6 ] }
{ "_id" : ObjectId("5d1eef27882bacc4ec4cb120"), "name" : 6, "attr" : [ 9, 0, 0, 0 ] }
{ "_id" : ObjectId("5d1eef27882bacc4ec4cb11f"), "name" : 5, "attr" : [ 5, 2, 3, 6 ] }
数组升序排序示例如下:
> db.parts.find().sort({attr: 1})
{ "_id" : ObjectId("5d1eef27882bacc4ec4cb11b"), "name" : 1, "attr" : [ 9, 9, 0, 5 ] }
{ "_id" : ObjectId("5d1eef27882bacc4ec4cb11c"), "name" : 2, "attr" : [ 9, 9, 0, 1 ] }
{ "_id" : ObjectId("5d1eef27882bacc4ec4cb11d"), "name" : 3, "attr" : [ 9, 0, 9, 0 ] }
{ "_id" : ObjectId("5d1eef27882bacc4ec4cb120"), "name" : 6, "attr" : [ 9, 0, 0, 0 ] }
{ "_id" : ObjectId("5d1eef27882bacc4ec4cb121"), "name" : 7, "attr" : [ 30, 0 ] }
{ "_id" : ObjectId("5d1eef27882bacc4ec4cb122"), "name" : 8, "attr" : [ 22, 0 ] }
{ "_id" : ObjectId("5d1eef27882bacc4ec4cb11f"), "name" : 5, "attr" : [ 5, 2, 3, 6 ] }
{ "_id" : ObjectId("5d1eef27882bacc4ec4cb124"), "name" : 10, "attr" : [ 30, 3 ] }
{ "_id" : ObjectId("5d1eef27882bacc4ec4cb123"), "name" : 9, "attr" : [ 30, 5 ] }
{ "_id" : ObjectId("5d1eef27882bacc4ec4cb11e"), "name" : 4, "attr" : [ 9, 8, 7, 6 ] }
根据以上结果,我们可以推测出排序规律:如果被比较的值相同,那么就按照插入顺序(即 ObjectId)排序。降序排序比较的是最大元素,即 30 - 30 - 30 - 22 -9 - 9 - 9 - 9 - 9 - 6
,其中 30
和 9
均有重复。可以发现:
- 第一个
30
对应的name
值为7
,第二个30
对应的name
值为9
,第三个30
对应的name
值为10
; - 第一个
9
对应的name
值为1
,第二个9
对应的name
值为2
,第三个9
对应的name
值为3
,第四个9
对应的name
值为4
,第五个9
对应的name
值为6
;
升序排序比较的是最小元素,即 0 - 0 - 0 - 0 - 0 - 0 - 2 - 3 - 5 - 6
,其中重复的只有 0
。0
对应的 name
值依次为 1, 2, 3, 6, 7, 8
。无论是升序排序还是降序排序,实际得到的结果与我们推测出来的规律相同,这说明我们推测出来的规律是正确的。其他类型的比较或排序规则可查阅官方文档 comparison-sort-order 。
MongoDB 提供了一系列用于比较的比较符,它们分别是:
名称 | 描述 |
---|---|
$eq |
匹配等于指定值的值 |
$gt |
匹配大于指定值的值 |
$gte |
匹配大于或等于指定值的值 |
$in |
匹配数组中指定的任何值 |
$lt |
匹配小于指定值的值 |
$lte |
匹配小于或等于指定值的值 |
$ne |
匹配所有不等于指定值的值 |
$nin |
不匹配数组中指定的任何值 |
其中,$eq
, $gte
, $lt
, $lte
, $gt
,$ne
的语法是相同的。以 $eq
为例,其语法格式如下:
{ <field>: { $eq: <value> } }
假设要匹配集合 els
中名称为 James
的文档,对应示例如下:
> db.els.find({name: {$eq: "James"}})
{ "_id" : ObjectId("5d1edd28eb81ddef9df74ffd"), "name" : "James", "attr" : [ 5, 6, 7 ] }
这等效于 SQL 中的 SELECT * FROM els WHERE name = "James"
。
$in
和 $nin
的语法相同。以 $in
为例,其格式如下:
{ field: { $in: [<value1>, <value2>, ... <valueN> ] } }
假设要过滤出集合 els
中文档字段 attr
中包含 6
或者 9
的文档,对应示例如下:
> db.els.find({attr: {$in: [6, 9]}})
{ "_id" : ObjectId("5d1edd28eb81ddef9df74ffd"), "name" : "James", "attr" : [ 5, 6, 7 ] }
{ "_id" : ObjectId("5d1edd28eb81ddef9df74fff"), "name" : "Kobe", "attr" : [ 1, 9, 9 ] }
{ "_id" : ObjectId("5d1edd28eb81ddef9df75000"), "name" : "Bosh", "attr" : [ 2, 9, 9 ] }
另外,$in
和 $nin
均支持正则表达式。例如要过滤出集合 inven
中 name
字段值以 詹
或者 韦
开头的文档,对应示例如下:
> db.inven.find({name: {$in: [/^詹/, /^韦/]}})
{ "_id" : ObjectId("5d200b986c39176e3a421af2"), "name" : "詹姆斯", "number" : 6, "attribute" : { "h" : 203, "w" : 222, "p" : "前锋" }, "status" : "A" }
{ "_id" : ObjectId("5d200b986c39176e3a421af3"), "name" : "韦德", "number" : 3, "attribute" : { "h" : 193, "w" : 220, "p" : "得分后卫" }, "status" : "R" }
反过来,过滤出集合 inven
中 name
字段值非 詹
或者非 韦
开头的文档。对应示例如下:
> db.inven.find({name: {$nin: [/^詹/, /^韦/]}})
{ "_id" : ObjectId("5d200b986c39176e3a421af4"), "name" : "科比", "number" : 24, "attribute" : { "h" : 198, "w" : 212, "p" : "得分后卫" }, "status" : "R" }
{ "_id" : ObjectId("5d200b986c39176e3a421af5"), "name" : "姚明", "number" : 11, "attribute" : { "h" : 226, "w" : 308, "p" : "中锋" }, "status" : "R" }
{ "_id" : ObjectId("5d200b986c39176e3a421af6"), "name" : "乔丹", "number" : 23, "attribute" : { "h" : 198, "w" : 216, "p" : "得分后卫" }, "status" : "R" }
以上就是比较查询操作符的相关知识,更多关于比较查询操作符的知识可查阅官方文档 Comparison Query Operators。
逻辑查询操作符
MongoDB 中的逻辑查询操作符共有 4 种,它们是:
名称 | 描述 |
---|---|
$and |
匹配符合多个条件的文档 |
$not |
匹配不符合条件的文档 |
$nor |
匹配不符合多个条件的文档 |
$or |
匹配符合任一条件的文档 |
其中,$and
, $nor
和 $or
语法格式相同:
{ $keyword: [ { <expression1> }, { <expression2> } , ... , { <expressionN> } ] }
语法中的 keyword
代表 and/nor/or
。而 $not
语法格式如下
{ field: { $not: { <operator-expression> } } }
$and
是隐式的,这意味着我们不必在查询语句中表明 and
或 AND
。 假设要过滤出集合 inven
中 球衣号大于 10
的 退役球员
,此时有两个条件:球衣号大于 10
,退役球员
。对应示例如下:
> db.inven.find({status: "R", number: {$gt: 10}})
{ "_id" : ObjectId("5d159e794d3d891430a2512f"), "name" : "科比", "number" : 24, "attribute" : { "h" : 198, "w" : 212, "p" : "得分后卫" }, "status" : "R" }
{ "_id" : ObjectId("5d159e794d3d891430a25130"), "name" : "姚明", "number" : 11, "attribute" : { "h" : 226, "w" : 308, "p" : "中锋" }, "status" : "R" }
{ "_id" : ObjectId("5d159e794d3d891430a25131"), "name" : "乔丹", "number" : 23, "attribute" : { "h" : 198, "w" : 216, "p" : "得分后卫" }, "status" : "R" }
这等效于 SQL 中的 SELECT * FROM inven WHERE status = "R" AND number > 10
。当然,也可以采用显式写法,对应示例如下:
> db.inven.find({$and: [{status: "R"}, {number: {$gt: 10}}]})
{ "_id" : ObjectId("5d159e794d3d891430a2512f"), "name" : "科比", "number" : 24, "attribute" : { "h" : 198, "w" : 212, "p" : "得分后卫" }, "status" : "R" }
{ "_id" : ObjectId("5d159e794d3d891430a25130"), "name" : "姚明", "number" : 11, "attribute" : { "h" : 226, "w" : 308, "p" : "中锋" }, "status" : "R" }
{ "_id" : ObjectId("5d159e794d3d891430a25131"), "name" : "乔丹", "number" : 23, "attribute" : { "h" : 198, "w" : 216, "p" : "得分后卫" }, "status" : "R" }
$or
, $not
,$nor
均采用显式写法。假设要过滤出集合 inven
中 球衣号大于 10
或者 退役球员
的文档,对应示例如下:
> db.inven.find({$or: [{status: "R"}, {number: {$gt: 10}}]})
{ "_id" : ObjectId("5d159e794d3d891430a2512e"), "name" : "韦德", "number" : 3, "attribute" : { "h" : 193, "w" : 220, "p" : "得分后卫" }, "status" : "R" }
{ "_id" : ObjectId("5d159e794d3d891430a2512f"), "name" : "科比", "number" : 24, "attribute" : { "h" : 198, "w" : 212, "p" : "得分后卫" }, "status" : "R" }
{ "_id" : ObjectId("5d159e794d3d891430a25130"), "name" : "姚明", "number" : 11, "attribute" : { "h" : 226, "w" : 308, "p" : "中锋" }, "status" : "R" }
{ "_id" : ObjectId("5d159e794d3d891430a25131"), "name" : "乔丹", "number" : 23, "attribute" : { "h" : 198, "w" : 216, "p" : "得分后卫" }, "status" : "R" }
这等效于 SQL 中的 SELECT * FROM inven WHERE status = "R" OR number > 10
。
假设要过滤出集合 inven
中 number
不等于 11
且 number
不等于 23
的文档,对应示例如下:
> db.inven.find({$nor: [{number: 23}, {number: 11}]})
{ "_id" : ObjectId("5d200b986c39176e3a421af2"), "name" : "詹姆斯", "number" : 6, "attribute" : { "h" : 203, "w" : 222, "p" : "前锋" }, "status" : "A" }
{ "_id" : ObjectId("5d200b986c39176e3a421af3"), "name" : "韦德", "number" : 3, "attribute" : { "h" : 193, "w" : 220, "p" : "得分后卫" }, "status" : "R" }
{ "_id" : ObjectId("5d200b986c39176e3a421af4"), "name" : "科比", "number" : 24, "attribute" : { "h" : 198, "w" : 212, "p" : "得分后卫" }, "status" : "R" }
假设要过滤出集合 inven
中 number
不大于 20
的文档,对应示例如下:
> db.inven.find({number: {$not: {$gt: 20}}})
{ "_id" : ObjectId("5d200b986c39176e3a421af2"), "name" : "詹姆斯", "number" : 6, "attribute" : { "h" : 203, "w" : 222, "p" : "前锋" }, "status" : "A" }
{ "_id" : ObjectId("5d200b986c39176e3a421af3"), "name" : "韦德", "number" : 3, "attribute" : { "h" : 193, "w" : 220, "p" : "得分后卫" }, "status" : "R" }
{ "_id" : ObjectId("5d200b986c39176e3a421af5"), "name" : "姚明", "number" : 11, "attribute" : { "h" : 226, "w" : 308, "p" : "中锋" }, "status" : "R" }
这个例子将比较查询操作符和逻辑查询操作符结合使用,实现了更细致的查询。
更多关于逻辑查询操作符的知识可查阅官方文档 Logical Query Operators。
元素查询操作符
MongoDB 中的元素查询操作符只有 2 种,它们是:
名称 | 描述 |
---|---|
$exists |
匹配具有指定字段的文档 |
$type |
匹配字段值符合类型的文档 |
exists
在开始学习 $exists
前,我们需要准备以下数据:
> db.elem.insertMany([
... {title: "湖人今夏交易频繁", author: "Asyncins", date: "2019-07-01", article: "..."},
... {title: "詹姆斯现身 MIA-CHN 比赛现场", date: "2019-07-06", article: "..."},
... {title: "伦纳德迟迟不肯表态", author: "Asyncins"}
... ])
$exists
语法格式如下:
{ field: { $exists: <boolean> } }
假设要过滤出集合 elem
中包含 author
字段的文档,对应示例如下:
> db.elem.find({author: {$exists: true}})
{ "_id" : ObjectId("5d203f1a6c39176e3a421af7"), "title" : "湖人今夏交易频繁", "author" : "Asyncins", "date" : "2019-07-01", "article" : "..." }
{ "_id" : ObjectId("5d203f1a6c39176e3a421af9"), "title" : "伦纳德迟迟不肯表态", "author" : "Asyncins" }
反过来,要过滤出集合 elem
中不包含 author
字段的文档,对应示例如下:
> db.elem.find({author: {$exists: false}})
{ "_id" : ObjectId("5d203f1a6c39176e3a421af8"), "title" : "詹姆斯现身 MIA-CHN 比赛现场", "date" : "2019-07-06", "article" : "..." }
type
在开始学习 $type
前,我们需要准备如下数据:
> db.ops.insertMany([
... {title: "北京高温持续,注意避暑", weight: 5, rec: false},
... {title: "广西持续降雨,最大降雨量 200 ml", weight: 5, rec: false},
... {title: "高考分数线已出,高分学子增多", weight: "hot", rec: true},
... {title: "秋老虎是真是假?", weight: 3, rec: false}
... ])
$type
语法如下:
{ field: { $type: <BSON type> } }
它也支持阵列写法:
{ field: { $type: [ <BSON type1> , <BSON type2>, ... ] } }
假设要过滤出 weight
值类型为 String
的文档,对应示例如下:
> db.ops.find({weight: {$type: "string"}})
{ "_id" : ObjectId("5d1838eb51b88758035de5b7"), "title" : "高考分数线已出,高分学子增多", "weight" : "hot", "rec" : true }
同理,过滤出 weight
值类型为 Number
的文档的对应示例如下:
> db.ops.find({weight: {$type: "number"}})
{ "_id" : ObjectId("5d1838eb51b88758035de5b5"), "title" : "北京高温持续,注意避暑", "weight" : 5, "rec" : false }
{ "_id" : ObjectId("5d1838eb51b88758035de5b6"), "title" : "广西持续降雨,最大降雨量 200 ml", "weight" : 5, "rec" : false }
{ "_id" : ObjectId("5d1838eb51b88758035de5b8"), "title" : "秋老虎是真是假?", "weight" : 3, "rec" : false }
阵列写法中的 BSON 类型为 or
关系,例如要过滤出 weight
值类型为 String
或者 Number
的文档,对应示例如下:
> db.ops.find({weight: {$type: ["string", "number"]}})
{ "_id" : ObjectId("5d1838eb51b88758035de5b5"), "title" : "北京高温持续,注意避暑", "weight" : 5, "rec" : false }
{ "_id" : ObjectId("5d1838eb51b88758035de5b6"), "title" : "广西持续降雨,最大降雨量 200 ml", "weight" : 5, "rec" : false }
{ "_id" : ObjectId("5d1838eb51b88758035de5b7"), "title" : "高考分数线已出,高分学子增多", "weight" : "hot", "rec" : true }
{ "_id" : ObjectId("5d1838eb51b88758035de5b8"), "title" : "秋老虎是真是假?", "weight" : 3, "rec" : false }
这等效于 SELECT * FROM ops WHERE weight.type = string OR weight.type = number
这样的 SQL 伪代码表示。要注意的是,$type
支持所有 BSON 类型的字符串标识符和整数标识符,例如 String
类型的字符串标识符 sting
及其整数标识符 2
,即 {$type: "string"}
等效于 {$type: 2}
。
更多关于元素查询操作符的知识可查阅官方文档 Element Query Operators。
评估查询操作符
MongoDB 中的评估查询操作符共有 6 种,它们是:
名称 | 描述 |
---|---|
$expr |
允许在查询语句中使用聚合表达式 |
$jsonSchema |
根据给定的 JSON 模式验证文档 |
$mod |
对字段的值执行模运算,并选择具有指定结果的文档 |
$regex |
匹配与正则表达式规则相符的文档 |
$text |
执行文本搜索 |
$where |
匹配满足 JavaScript 表达式的文档 |
expr
在学习 $expr
前,我们需要准备以下数据:
> db.acbook.insertMany([
... {_id: 1, category: "衣", 预算: 300, 开支: 600},
... {_id: 2, category: "食", 预算: 1000, 开支: 600},
... {_id: 3, category: "住", 预算: 800, 开支: 800},
... {_id: 4, category: "行", 预算: 220, 开支: 360},
... {_id: 5, category: "医", 预算: 200, 开支: -50}
... ])
$expr
语法格式如下:
{ $expr: { <expression> } }
假设要过滤出超出预算的文档,对应示例如下:
> db.acbook.find({$expr: {$gt: ["$开支", "$预算"]}})
{ "_id" : 1, "category" : "衣", "预算" : 300, "开支" : 600 }
{ "_id" : 4, "category" : "行", "预算" : 220, "开支" : 360 }
示例中使用了 $gt
表达式,用于比较 开支
和 预算
。我们也可以使用 $lt
表达式,对应命令如下:
> db.acbook.find({$expr: {$lt: ["$预算", "$开支"]}})
{ "_id" : 1, "category" : "衣", "预算" : 300, "开支" : 600 }
{ "_id" : 4, "category" : "行", "预算" : 220, "开支" : 360 }
$expr
支持的表达式非常多,详见官方文档 Expressions。
mod
$mod
的作用是对字段的值执行模运算,并选择具有指定结果的文档。其语法格式如下:
{ field: { $mod: [ divisor, remainder ] } }
假设要过滤出集合 acbook
中满足 mod(开支, 6) = 0
的文档。对应示例如下:
> db.acbook.find({开支: {$mod: [6, 0]}})
{ "_id" : 1, "category" : "衣", "预算" : 300, "开支" : 600 }
{ "_id" : 2, "category" : "食", "预算" : 1000, "开支" : 600 }
{ "_id" : 4, "category" : "行", "预算" : 220, "开支" : 360 }
即 mod(600, 6) = 0
, mod(360, 6) = 0
。同理,要过滤出集合 acbook
中满足 mod(开支, 6) = 2
的文档,对应示例如下:
> db.acbook.find({开支: {$mod: [6, 2]}})
{ "_id" : 3, "category" : "住", "预算" : 800, "开支" : 800 }
即 mod(800, 6) = 2
。要注意的是,$mod
只接受 2 个参数:divisor
和 remainder
。如果只传入 1 个参数,例如 db.acbook.find({开支: {$mod: [6]}})
, 就会得到如下错误提示:
Error: error: {
"ok" : 0,
"errmsg" : "malformed mod, not enough elements",
"code" : 2,
"codeName" : "BadValue"
}
不传入值或传入多个值也是不被允许的,在返回文档中的 errmsg
处会给出对应的提示。例如不传入值对应的提示为 malformed mod, not enough elements
,而多个值对应的提示为 malformed mod, too many elements
。
提示:在 2.6
版本中,传入单个值时会默认补上 0
,传入多个值时会忽略多余的值。例如 db.acbook.find({开支: {$mod: [6]}})
和 db.acbook.find({开支: {$mod: [6, 2, 3, 5]}})
等效于 db.acbook.find({开支: {$mod: [6, 0]}})
。但 4.0
版本不允许这样做。
regex
MongoDB 提供的 $regex
让开发者可以在查询语句中使用正则表达式,这实在是令人惊喜。MongoDB 中的正则表达式是 PCRE,即 Perl 语言兼容的正则表达式。$regex
语法格式如下:
{ <field>: { $regex: /pattern/, $options: '<options>' } }
{ <field>: { $regex: 'pattern', $options: '<options>' } }
{ <field>: { $regex: /pattern/<options> } }
三种格式任选其一,特定语法的使用限制可参考 $regex vs./pattern/Syntax 。也可以用下面这种语法:
{ <field>: /pattern/<options> }
正则表达式中有一些特殊选项(又称模式修正符),例如不区分大小写或允许使用点字符等,MongoDB 中支持的选项如下:
选项 | 描述 | 语法限制 |
---|---|---|
i |
不区分大小写字母。 | |
m |
支持多行匹配。 | |
x |
忽略空格和注释(#),注释以 \n 结尾。 |
必须使用 $option |
s |
允许点(.)字符匹配括换行符在内的所有字符,也可以理解为允许点(.)字符匹配换行符后面的字符。 | 必须使用 $option |
在开始学习之前,准备以下数据:
> db.regexs.insertMany([
... {_id: 1, nickname: "abc123", desc: "Single Line Description."},
... {_id: 2, nickname: "abc299", desc: "First line \nSecond line"},
... {_id: 3, nickname: "xyz5566", desc: "Many spaces before line"},
... {_id: 4, nickname: "xyz8205", desc: "Multiple\nline description"}
... ])
假设要过滤出 nickname
值结尾为 299
的文档,对应示例如下:
> db.regexs.find({nickname: {$regex: /299$/}})
{ "_id" : 2, "nickname" : "abc299", "desc" : "First line \nSecond line" }
这相当于 SQL 中的模糊查询,对应的 SQL 语句为 SELECT * FROM regexs WHERE nickname like "%299"
。接下来使用模式修正符 i
实现不区分大小写的匹配,对应示例如下:
> db.regexs.find({nickname: {$regex: /^aBc/i}})
{ "_id" : 1, "nickname" : "abc123", "desc" : "Single Line Description." }
{ "_id" : 2, "nickname" : "abc299", "desc" : "First line \nSecond line" }
这个语句的作用是过滤出集合 regexs
中 nickname
字段值由 aBc
开头的文档,并在匹配时忽略大小写字母。接下来我们再通过一个例子了解模式修正符 m
的用法和作用,对应示例如下:
> db.regexs.find({desc: {$regex: /^s/, $options: "im"}})
{ "_id" : 1, "nickname" : "abc123", "desc" : "Single Line Description." }
{ "_id" : 2, "nickname" : "abc299", "desc" : "First line \nSecond line" }
这个语句的作用是过滤出集合 regexs
中 desc
字段值由 s
开头的文档,匹配时忽略大小写字母,并进行多行匹配。虽然 _id
为 2 的文档中的 desc
并不是 s
或 S
开头,但由于使用了模式修正符 m
,所以能够匹配到 \n
符号后面的 Second
。如果没有使用模式修正符 m
,那么匹配结果将会是 { "_id" : 1, "nickname" : "abc123", "desc" : "Single Line Description." }
。
点字符和星号在正则表达式中是最常用的组合,MongoDB 也支持这个组合。假设要过滤出集合 regexs
中 desc
字段值由 m
开头且 line
结尾的文档,对应示例如下:
> db.regexs.find({desc: {$regex: /m.*line/, $options: "is"}})
{ "_id" : 3, "nickname" : "xyz5566", "desc" : "Many spaces before line" }
{ "_id" : 4, "nickname" : "xyz8205", "desc" : "Multiple\nline description" }
如果不使用模式修正符 s
,.*
组合也是可用的,但无法匹配到换行符后面的内容,那么匹配结果将会是 { "_id" : 3, "nickname" : "xyz5566", "desc" : "Many spaces before line" }
。
模式修正符 x
的描述为:“忽略空格和注释(#),注释以 \n
结尾”。这理解起来有些困难,但你不用担心,只要跟着本文指引和案例,就能够掌握模式修正符 x
的正确用法。示例如下:
> var pattern = "abc #category code\n123 #item number"
> db.regexs.find({nickname: {$regex: pattern, $options: "x"}})
{ "_id" : 1, "nickname" : "abc123", "desc" : "Single Line Description." }
正则规则为 abc #category code\n123 #item number
,根据模式修正符 x
的描述,我们可以将其转换为 abc123
。即过滤出集合 regexs
中 nickname
值为 abc123
的文档,所以执行结果为:
{ "_id" : 1, "nickname" : "abc123", "desc" : "Single Line Description." }
再来看一个示例:
> var pattern = "abc #category code\n xyz#item number"
> db.regexs.find({nickname: {$regex: pattern, $options: "x"}})