超高性价比的 MongoDB 零基础快速入门实战教程

超高性价比的 MongoDB 快速入门教程

在上一篇 《MongoDB 实战教程:数据库与集合的 CRUD 操作篇》 中,我们学习了MongoDB 与 NoSQL 的关系、 MongoDB 的安装、数据类型、MongoShell、创建数据库、显式创建集合和隐式创建集合,还学习了如何更改集合名称以及删除数据库和集合的方法,并对每种操作都进行了实例演示。

在本篇 chat 中我们将学习流式聚合操作,并深入了解语句的执行效率。然后深入学习能够提高数据服务可用性的复制集。接着了解 MongoDB 的水平扩展能力,学习 MongoDB 数据的备份与还原方法,并为数据服务开启访问控制。

基础篇 一 文档的 CRUD 操作

CRUD 操作指的是对文档进行 createreadupdate and delete 操作,即增删改查。文档 CRUD 操作的内容将分为 Create Operations, Read Operations, Update Operations, Delete OperationsCursor 等 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 代表本次操作的操作状态,状态值包括 truefalseinsertedId 即该文档的 _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,其中 309 均有重复。可以发现:

  • 第一个 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,其中重复的只有 00 对应的 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 均支持正则表达式。例如要过滤出集合 invenname 字段值以 或者 开头的文档,对应示例如下:

> 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" }

反过来,过滤出集合 invenname 字段值非 或者非 开头的文档。对应示例如下:

> 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 是隐式的,这意味着我们不必在查询语句中表明 andAND 。 假设要过滤出集合 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

假设要过滤出集合 invennumber 不等于 11number 不等于 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" }

假设要过滤出集合 invennumber 不大于 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) = 0mod(360, 6) = 0。同理,要过滤出集合 acbook 中满足 mod(开支, 6) = 2 的文档,对应示例如下:

> db.acbook.find({开支: {$mod: [6, 2]}})
{ "_id" : 3, "category" : "住", "预算" : 800, "开支" : 800 }

mod(800, 6) = 2。要注意的是,$mod 只接受 2 个参数:divisorremainder。如果只传入 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" }

这个语句的作用是过滤出集合 regexsnickname 字段值由 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" }

这个语句的作用是过滤出集合 regexsdesc 字段值由 s 开头的文档,匹配时忽略大小写字母,并进行多行匹配。虽然 _id 为 2 的文档中的 desc 并不是 sS 开头,但由于使用了模式修正符 m,所以能够匹配到 \n 符号后面的 Second。如果没有使用模式修正符 m,那么匹配结果将会是 { "_id" : 1, "nickname" : "abc123", "desc" : "Single Line Description." }

点字符和星号在正则表达式中是最常用的组合,MongoDB 也支持这个组合。假设要过滤出集合 regexsdesc 字段值由 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。即过滤出集合 regexsnickname 值为 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"}})

这个命令并没有得到文档输出,也就是说没有文档符合其规则,这是因为 abc #category code\n xyz#item number 等效于 abcxyzpattern 中的 # 代表注释,而 \n 表示注释结束。所以 #category code\n#item number 是没有用的,有用的是 abcxyz。根据这个规则,我们可以过滤出集合 regexsnickname 值包含 abc 的文档。对应命令如下:

> var pattern = "abc # xyz #category code\n # item number 123"
> db.regexs.find({nickname: {$regex: pattern, $options: "x"}})
{ "_id" : 1, "nickname" : "abc123", "desc" : "Single Line Description." }
{ "_id" : 2, "nickname" : "abc299", "desc" : "First line \nSecond line" }

此时 #xyz#category code\n# item number 123 均无效,有效的只有 abc,所以能够匹配到两个文档。再来看下面这个示例:

> var pattern = "# abc\n 299 # xyz #category code\n # item number"
> db.regexs.find({nickname: {$regex: pattern, $options: "x"}})
{ "_id" : 2, "nickname" : "abc299", "desc" : "First line \nSecond line" }

回顾一下模式修正符 x 的描述:“忽略空格和注释(#),注释以 \n 结尾”。此时 # abc\n# xyz #category code\n# item number 均无效,由于注释以 \n 结尾,所以 # abc\n 299 中的 299 有效,才会输出文档:

{ "_id" : 2, "nickname" : "abc299", "desc" : "First line \nSecond line" }

text

开发者可以通过 $text 搜索符合条件的文档,其语法格式如下:

{
  $text:
    {
      $search: <string>,
      $language: <string>,
      $caseSensitive: <boolean>,
      $diacriticSensitive: <boolean>
    }
}

$text 中可以使用 $search$language$caseSensitive$diacriticSensitive 等指令,指令的类型和对应描述如下:

指令

类型

描述

$search

string

MongoDB 解析并用于查询文本索引的字符串。查询时采用 OR 逻辑,除非指定字符串为短语。

$language

string

确定搜索停止词列表以及词干分析其和标记器规则的语言。如果未指定,则搜索时使用默认语言。

$caseSensitive

boolean

是否启用大小写区分。

$diacriticSensitive

boolean

是否启用变音符敏感搜索。

在开始学习之前,准备以下数据:

> db.news.insertMany([
... { _id: 1, subject: "coffee", author: "xyz", views: 50 },
... { _id: 2, subject: "Coffee Shopping", author: "efg", views: 5 },
... { _id: 3, subject: "Baking a cake", author: "abc", views: 90  },
... { _id: 4, subject: "baking", author: "xyz", views: 100 },
... { _id: 5, subject: "Café Con Leche", author: "abc", views: 200 },
... { _id: 6, subject: "Сырники", author: "jkl", views: 80 },
... { _id: 7, subject: "coffee and cream", author: "efg", views: 10 },
... { _id: 8, subject: "Cafe con Leche", author: "xyz", views: 10 },
... { _id: 9, subhect: "NBA", author: "coffee", views: 300}
... ])

接着为 subjectauthor 建立文本索引。对应命令如下:

> db.news.createIndex({subject: "text", author: "text"})
{
    "createdCollectionAutomatically" : false,
    "numIndexesBefore" : 1,
    "numIndexesAfter" : 2,
    "ok" : 1
}

$search 可以搜索单个词、多个词和短语。假设要过滤出集合 news 中建立了文本索引的字段 subjectauthor 中值包含 coffee 的文档,对应示例如下:

> db.news.find({$text: {$search: "coffee"}})
{ "_id" : 9, "subhect" : "NBA", "author" : "coffee", "views" : 300 }
{ "_id" : 1, "subject" : "coffee", "author" : "xyz", "views" : 50 }
{ "_id" : 7, "subject" : "coffee and cream", "author" : "efg", "views" : 10 }
{ "_id" : 2, "subject" : "Coffee Shopping", "author" : "efg", "views" : 5 }

多词搜索中,多个词以空格进行分隔,多词搜索示例如下:

> db.news.find({$text: {$search: "cream Shopping"}})
{ "_id" : 7, "subject" : "coffee and cream", "author" : "efg", "views" : 10 }
{ "_id" : 2, "subject" : "Coffee Shopping", "author" : "efg", "views" : 5 }

上面提到,多次搜索时采用 OR 逻辑。在此示例中,搜索语句释义为:过滤出集合 news 中建立了文本索引的字段 subjectauthor 中值包含 coffeeShopping 的文档。按短语搜索的语法与按词搜索不同,其示例如下:

> db.news.find({$text: {$search: "\"coffee and\""}})
{ "_id" : 7, "subject" : "coffee and cream", "author" : "efg", "views" : 10 }

搜索语句释义为:过滤出集合 news 中建立了文本索引的字段 subjectauthor 中值包含 coffee and的文档。其中,以 \"keyword"\ 格式将搜索词 keyword 设置为短语,示例中的短语表示为 \"coffee and\"

$search 中用前缀减号(-)作为否定词,这样我们就能够实现类似于 NOT 的过滤规则。假设要过滤出集合 news 中建立了文本索引的字段 subjectauthor 中值包含 coffee 但不包含 Shopping 的文档,对应示例如下:

> db.news.f
                        
收藏 收藏
分享
购买文章 ¥9.99