安装

Windows平台安装MongoDB

下载地址:https://www.mongodb.com/download-center/community

安装过程中,你可以通过点击 “Custom“ 按钮来设置你的安装目录,下一步安装时 “install mongoDB compass” 不勾选,否则可能要很长时间都一直在执行安装,MongoDB Compass 是一个图形界面管理工具,我们可以在后面自己到官网下载安装。

4.x之前的版本需要自己配置data、log文件夹,手动将MongoDB添加为Windows服务,4.x之后的版本在安装完成之后会自动生成好data、log文件夹,并且将MongoDB添加到Windows服务中。

安装完成之后需配置环境变量

  1. PathE:\Program Files\MongoDB\Server\4.0\bin

可视化工具

Robomongo

Robomongo 是一个基于 Shell 的跨平台开源 MongoDB 可视化管理工具,支持 Windows、Linux 和 Mac,嵌入了 JavaScript 引擎和 MongoDB mongo,只要你会使用 mongo shell,你就会使用 Robomongo,它还提供了语法高亮、自动补全、差别视图等。

Robomongo 下载地址

MongoChef

MongoChef 是另一款强大的 MongoDB 可视化管理工具,支持 Windows、Linux 和 Mac。

MongoChef 下载地址,我们选择左侧的非商业用途的免费版下载。

MongoDB基本概念

不管我们学习什么数据库都应该学习其中的基础概念,在mongodb中基本的概念是文档、集合、数据库,下面我们挨个介绍。

下表将帮助您更容易理解Mongo中的一些概念:

SQL术语/概念 MongoDB术语/概念 解释/说明
database database 数据库
table collection 数据库表/集合
row document 数据记录行/文档
column field 数据字段/域
index index 索引
table joins 表连接,MongoDB不支持
primary key primary key 主键,MongoDB自动将_id字段设置为主键

通过下图实例,我们也可以更直观的了解Mongo中的一些概念:

MongoDB - 图1

数据库(database)

MongoDB的默认数据库为”test”,该数据库存储在data目录中。

“show dbs” 命令可以显示所有数据库

  1. > show dbs
  2. local 0.078GB
  3. test 0.078GB

执行 “db” 命令可以显示当前数据库对象或集合。

  1. > db
  2. test

运行”use”命令,可以连接到一个指定的数据库。

  1. > use local
  2. switched to db local
  3. > db
  4. local

数据库也通过名字来标识。数据库名可以是满足以下条件的任意UTF-8字符串。

  • 不能是空字符串(””)
  • 不得含有’ ‘(空格)、.、$、/、\和\0 (空字符)
  • 应全部小写
  • 最多64字节

MongoDB默认会创建local、admin、config数据库,可以直接访问这些有特殊作用的数据库。

  • admin: 从权限的角度来看,这是”root”数据库。要是将一个用户添加到这个数据库,这个用户自动继承所有数据库的权限。一些特定的服务器端命令也只能从这个数据库运行,比如列出所有的数据库或者关闭服务器。
  • local: 这个数据永远不会被复制,可以用来存储限于本地单台服务器的任意集合
  • config: 当Mongo用于分片设置时,config数据库在内部使用,用于保存分片的相关信息。

慎用local数据库

local数据库,从名字可以看出,它只会在本地存储数据,即local数据库里的内容不会同步到副本集里其他节点上去;目前local数据库主要存储副本集的配置信息、oplog信息,这些信息是每个Mongod进程独有的,不需要同步到副本集中其他节点。

在使用MongoDB时,重要的数据千万不要存储在local数据库中,否则当一个节点故障时,存储在local里的数据就会丢失。

另外,对于重要的数据,除了不能存储在local数据库,还要注意MongoDB默认的WriteConcern{w: 1},即数据写到Primary上(不保证journal已经写成功)就向客户端确认,这时同样存在丢数据的风险。对于重要的数据,可以设置更高级别的如{w: "majority"}来保证数据写到大多数节点后再向客户端确认,当然这对写入的性能会造成一定的影响。

慎用admin数据库

当Mongod启用auth选项时,用户需要创建数据库帐号,访问时根据帐号信息来鉴权,而数据库帐号信息就存储在admin数据库下。

  1. > use admin
  2. switched to db admin
  3. > db.getCollectionNames()
  4. [ "system.users", "system.version" ]
  • system.version存储authSchema的版本信息
  • system.users存储了数据库帐号信息
  • 如果用户创建了自定义的角色,还会有system.roles集合

用户可以在admin数据库下建立任意集合,存储任何数据,但强烈建议不要使用admin数据库存储应用业务数据,最好创建新的数据库。

admin数据库里的system.users、system.roles2个集合的数据,MongoDB会cache在内存里,这样不用每次鉴权都从磁盘加载用户角色信息。目前cache的维护代码,只有在保证system.users、system.roles的写入都串行化的情况下才能正确工作,详情参考官方issue SERVER-16092

MongoDB将将admin数据库上的意向写锁(MODE_IX)直接升级为写锁(MODE_X),也就是说admin数据库的写入操作的锁级别只能到DB级别,不支持多个collection并发写入,在写入时也不支持并发读取。如果用户在admin数据库里存储业务数据,则可能遭遇性能问题。

集合(collections)

集合就是 MongoDB 文档组,类似于 RDBMS (关系数据库管理系统:Relational Database Management System)中的表。

集合存在于数据库中,集合没有固定的结构,这意味着你在对集合可以插入不同格式和类型的数据,但通常情况下我们插入集合的数据都会有一定的关联性。

比如,我们可以将以下不同数据结构的文档插入到集合中:

  1. {"site":"www.baidu.com"}
  2. {"site":"www.google.com","name":"Google"}
  3. {"site":"www.runoob.com","name":"菜鸟教程","num":5}

当第一个文档插入时,集合就会被创建。

合法的集合名:

  • 集合名不能是空字符串””
  • 集合名不能含有\0字符(空字符),这个字符表示集合名的结尾
  • 集合名不能以”system.”开头,这是为系统集合保留的前缀
  • 用户创建的集合名字不能含有保留字符。有些驱动程序的确支持在集合名里面包含,这是因为某些系统生成的集合中包含该字符。除非你要访问这种系统创建的集合,否则千万不要在名字里出现$。

如下实例:

  1. db.col.findOne()

文档(document)

文档是一组键值对,MongoDB 的文档不需要设置相同的字段,并且相同的字段不需要相同的数据类型,这与关系型数据库有很大的区别,也是 MongoDB 非常突出的特点。

一个简单的文档例子如下:

  1. {"site":"www.runoob.com", "name":"菜鸟教程"}

下表列出了 RDBMS 与 MongoDB 对应的术语:

RDBMS MongoDB
数据库 数据库
表格 集合
文档
字段
表联合 嵌入文档
主键 主键 (MongoDB 提供了 key 为 _id )

需要注意的是:

  1. 文档中的键/值对是有序的
  2. 文档中的值不仅可以是在双引号里面的字符串,还可以是其他几种数据类型(甚至可以是整个嵌入的文档)
  3. MongoDB区分类型和大小写
  4. MongoDB的文档不能有重复的键。
  5. 文档的键是字符串,除了少数例外情况,键可以使用任意UTF-8字符

文档键命名规范:

  • 键不能含有\0 (空字符)。这个字符用来表示键的结尾
  • .和$有特别的意义,只有在特定环境下才能使用
  • 以下划线”_”开头的键是保留的(不是严格要求的)

MongoDB 数据类型

MongoDB通过BSON(Binary JSON)来描述和存放数据。BSON是一种可进行二进制序列化的,类JSON格式的文档对象,下表为MongoDB中常用的几种数据类型:

数据类型 描述
String 字符串,在 MongoDB 中,只支持UTF-8 编码的字符串
Integer 整数值,根据你所采用的服务器,可分为 32 位或 64 位
Double 双精度浮点数,数字默认都是双精度浮点数
Boolean 布尔值
Array 数组
Object 对象
Null 表示空对象
Date 日期(UTC 时间)
Timestamp 时间戳,记录文档修改或添加的具体时间
ObjectId 对象 ID,一般用于默认主键
Binary Data 二进制数据
Code 代码类型,用于在文档中存储 JavaScript 代码

下面说明下几种重要的数据类型。

ObjectId

ObjectId 类似唯一主键,包含 12 bytes,含义是:

  • 前 4 个字节表示 Unix 时间戳 UTC 时间,比北京时间晚了 8 个小时)
  • 接下来的 3 个字节是机器标识码
  • 紧接的两个字节由进程 id 组成(PID)
  • 最后三个字节是随机数

MongoDB - 图2

MongoDB 中存储的文档必须有一个 _id 键。这个键的值可以是除数组外的任何类型,默认是个 ObjectId 对象。

由于 ObjectId 中保存了文档创建的时间戳,所以你不需要为你的文档保存时间戳字段,你可以通过 getTimestamp 函数来获取文档的创建时间:

  1. > var newObject = ObjectId()
  2. > newObject.getTimestamp()
  3. ISODate("2017-11-25T07:21:10Z")

ObjectId 转为字符串

  1. > newObject.str
  2. 5a1919e63df83ce79df8b38f

字符串

BSON 字符串都是 UTF-8 编码。

时间戳

BSON 有一个特殊的时间戳类型用于 MongoDB 内部使用,与普通的 日期 类型不相关。 时间戳值是一个 64 位的值。其中:

  • 前32位是一个 time_t 值(与Unix新纪元相差的秒数)
  • 后32位是在某秒中操作的一个递增的序数

在单个 mongod 实例中,时间戳值通常是唯一的。

在复制集中, oplog 有一个 ts 字段。这个字段中的值使用BSON时间戳表示了操作时间。

BSON 时间戳类型主要用于 MongoDB 内部使用。在大多数情况下的应用开发中,你可以使用 BSON 日期类型。

日期

表示当前距离 Unix新纪元(1970年1月1日)的毫秒数。日期类型是有符号的, 负数表示 1970 年之前的日期。

  1. > var mydate1 = new Date() //格林尼治时间
  2. > mydate1
  3. ISODate("2018-03-04T14:58:51.233Z")
  4. > typeof mydate1
  5. object
  6. > var mydate2 = ISODate() //格林尼治时间
  7. > mydate2
  8. ISODate("2018-03-04T15:00:45.479Z")
  9. > typeof mydate2
  10. object

这样创建的时间是日期类型,可以使用 JS 中的 Date 类型的方法。

返回一个时间类型的字符串:

  1. > var mydate1str = mydate1.toString()
  2. > mydate1str
  3. Sun Mar 04 2018 14:58:51 GMT+0000 (UTC)
  4. > typeof mydate1str
  5. string

或者

  1. > Date()
  2. Sun Mar 04 2018 15:02:59 GMT+0000 (UTC)

MongoDB基本操作(数据库、集合)

创建数据库

语法

  1. use DATABASE_NAME

如果数据库不存在,则创建数据库,否则切换到指定数据库。

实例

以下实例我们创建了数据库 runoob:

  1. > use runoob
  2. switched to db runoob
  3. > db
  4. runoob
  5. >

如果你想查看所有数据库,可以使用 show dbs 命令:

  1. > show dbs
  2. admin 0.000GB
  3. local 0.000GB
  4. >

可以看到,我们刚创建的数据库 runoob 并不在数据库的列表中, 要显示它,我们需要向 runoob 数据库插入一些数据。

  1. > db.runoob.insert({"name":"菜鸟教程"})
  2. WriteResult({ "nInserted" : 1 })
  3. > show dbs
  4. local 0.078GB
  5. runoob 0.078GB
  6. test 0.078GB
  7. >

MongoDB 中默认的数据库为 test,如果你没有创建新的数据库,集合将存放在 test 数据库中。

删除数据库

语法

  1. db.dropDatabase()

删除当前数据库,默认为 test,你可以使用 db 命令查看当前数据库名。

实例

以下实例我们删除了数据库 runoob。

首先,查看所有数据库:

  1. > show dbs
  2. local 0.078GB
  3. runoob 0.078GB
  4. test 0.078GB

接下来我们切换到数据库 runoob:

  1. > use runoob
  2. switched to db runoob
  3. >

执行删除命令:

  1. > db.dropDatabase()
  2. { "dropped" : "runoob", "ok" : 1 }

最后,我们再通过 show dbs 命令数据库是否删除成功:

  1. > show dbs
  2. local 0.078GB
  3. test 0.078GB
  4. >

创建集合

语法

  1. db.createCollection(name, options)

参数说明:

  • name: 要创建的集合名称
  • options: 可选参数, 指定有关内存大小及索引的选项

options 可以是如下参数:

字段 类型 描述
capped 布尔 (可选)如果为 true,则创建固定集合。固定集合是指有着固定大小的集合,当达到最大值时,它会自动覆盖最早的文档。 当该值为 true 时,必须指定 size 参数。
autoIndexId 布尔 (可选)如为 true,自动以 _id 字段创建索引。默认为 false。
size 数值 (可选)为固定集合指定一个最大值(以字节计)。 如果 capped 为 true,也需要指定该字段。
max 数值 (可选)指定固定集合中包含文档的最大数量。

在插入文档时,MongoDB 首先检查固定集合的 size 字段,然后检查 max 字段。

实例

在 test 数据库中创建 runoob 集合:

  1. > use test
  2. switched to db test
  3. > db.createCollection("runoob")
  4. { "ok" : 1 }
  5. >

如果要查看已有集合,可以使用 show collections 命令:

  1. > show collections
  2. runoob
  3. system.indexes

下面是带有几个关键参数的 createCollection() 的用法:

创建固定集合 mycol,整个集合空间大小 6142800 KB, 文档最大个数为 10000 个。

  1. > db.createCollection("mycol", { capped : true, autoIndexId : true, size :
  2. 6142800, max : 10000 } )
  3. { "ok" : 1 }
  4. >

在 MongoDB 中,可以不需要显式创建集合。当你插入一些文档时,MongoDB 会自动创建集合。

  1. > db.col.insert({"name" : "菜鸟教程"})
  2. > show collections
  3. col
  4. ...

删除集合

语法

  1. db.collection.drop()

参数说明:

返回值

如果成功删除选定集合,则 drop() 方法返回 true,否则返回 false。

实例

在数据库 mydb 中,我们可以先通过 show collections 命令查看已存在的集合:

  1. >use mydb
  2. switched to db mydb
  3. >show collections
  4. mycol
  5. mycol2
  6. system.indexes
  7. runoob
  8. >

接着删除集合 mycol2 :

  1. >db.mycol2.drop()
  2. true
  3. >

通过 show collections 再次查看数据库 mydb 中的集合:

  1. >show collections
  2. mycol
  3. system.indexes
  4. runoob
  5. >

从结果中可以看出 mycol2 集合已被删除。

MongoDB基本操作之CRUD

插入文档

MongoDB 中文档的数据结构和 JSON 基本一样,所有存储在集合中的数据都是BSON格式,BSON 是一种类似 JSON 的二进制形式的存储格式,是 Binary JSON 的简称。

语法

  1. db.collection.insert(document or array of documents)

实例

以下文档可以存储在 MongoDB 的 test 数据库 的 col 集合中:

  1. db.col.insert({
  2. title: 'MongoDB 教程',
  3. description: 'MongoDB 是一个 Nosql 数据库',
  4. by: '菜鸟教程',
  5. url: 'http://www.runoob.com',
  6. tags: ['mongodb', 'database', 'NoSQL'],
  7. likes: 100
  8. })

以上实例中 col 是我们的集合名,如果该集合不在该数据库中, MongoDB 会自动创建该集合并插入文档。

查看已插入文档:

  1. > db.col.find()
  2. {
  3. "_id": ObjectId("56064886ade2f21f36b03134"),
  4. "title": "MongoDB 教程",
  5. "description": "MongoDB 是一个 Nosql 数据库",
  6. "by": "菜鸟教程",
  7. "url": "http://www.runoob.com",
  8. "tags": ["mongodb", "database", "NoSQL"],
  9. "likes": 100
  10. }

我们也可以将数据定义为一个变量,如下所示:

  1. > document=({title: 'MongoDB 教程',
  2. description: 'MongoDB 是一个 Nosql 数据库',
  3. by: '菜鸟教程',
  4. url: 'http://www.runoob.com',
  5. tags: ['mongodb', 'database', 'NoSQL'],
  6. likes: 100
  7. });

执行后显示结果如下:

  1. {
  2. "title": "MongoDB 教程",
  3. "description": "MongoDB 是一个 Nosql 数据库",
  4. "by": "菜鸟教程",
  5. "url": "http://www.runoob.com",
  6. "tags": ["mongodb", "database", "NoSQL"],
  7. "likes": 100
  8. }

执行插入操作:

  1. > db.col.insert(document)
  2. WriteResult({ "nInserted" : 1 })

一次插入多条数据

1、先创建数组

2、将数据放在数组中

3、一次 insert 到集合中

  1. var arr = [];
  2. for(var i = 1; i<=20000; i++){ arr.push({num: i}); }
  3. db.numbers.insert(arr);

其他语法

3.2 版本后还有以下几种语法可用于插入文档:

  • db.collection.insertOne():向指定集合中插入一条文档数据
  • db.collection.insertMany():向指定集合中插入多条文档数据
  1. // 插入单条数据
  2. > var document = db.collection.insertOne({"a": 3})
  3. > document
  4. {
  5. "acknowledged": true,
  6. "insertedId": ObjectId("571a218011a82a1d94c02333")
  7. }
  8. // 插入多条数据
  9. > var res = db.collection.insertMany([{"b": 3}, {'c': 4}])
  10. > res
  11. {
  12. "acknowledged": true,
  13. "insertedIds": [
  14. ObjectId("571a22a911a82a1d94c02337"),
  15. ObjectId("571a22a911a82a1d94c02338")
  16. ]
  17. }

插入文档你也可以使用 db.col.save(document) 命令。如果不指定 _id 字段 save() 命令将会调用insert() 命令,创建新文档。

  1. db.col.save({
  2. "title": "MongoDB 新教程",
  3. "description": "MongoDB 是一个 Nosql 数据库",
  4. "by": "菜鸟教程",
  5. "url": "http: //www.runoob.com",
  6. "tags": ["mongodb", "database", "NoSQL"],
  7. "likes": 1000
  8. })

查询文档

语法

MongoDB 查询数据的语法格式如下:

  1. db.collection.find(query, projection)
  • query :可选,使用查询操作符指定查询条件的文档
  • projection :可选,使用投影操作符指定返回的键值。查询时返回文档中所有键值, 只需省略该参数即可(默认省略)。

如果你需要以易读的方式来读取数据,可以使用 pretty() 方法,语法格式如下:

  1. >db.col.find().pretty()

pretty() 方法以格式化的方式来显示所有文档。

实例

以下实例我们查询了集合 col 中的数据:

  1. > db.col.find().pretty()
  2. {
  3. "_id": ObjectId("56063f17ade2f21f36b03133"),
  4. "title": "MongoDB 教程",
  5. "description": "MongoDB 是一个 Nosql 数据库",
  6. "by": "菜鸟教程",
  7. "url": "http://www.runoob.com",
  8. "tags": ["mongodb", "database", "NoSQL"],
  9. "likes": 100
  10. }

除了 find() 方法之外,还有一个 findOne() 方法,它只返回一个文档。

  1. // 匹配查询
  2. db.col.find({title: 'MongoDB 教程'})
  3. // 模糊匹配查询
  4. // 查询 title 包含"MongoDB"字的文档:
  5. db.col.find({title:/MongoDB/})
  6. // 查询 title 字段以"MongoDB"字开头的文档:
  7. db.col.find({title:/^MongoDB/})
  8. // 查询 titl e字段以"教程"字结尾的文档:
  9. db.col.find({title:/教程$/})

文档投影

  1. db.collection.find(query, projection)

若不指定 projection,则默认返回所有键,指定 projection 格式如下,有两种模式

  1. db.collection.find(query, {title: 1, by: 1}) // inclusion模式 指定返回的键,不返回其他键
  2. db.collection.find(query, {title: 0, by: 0}) // exclusion模式 指定不返回的键,返回其他键

_id 键默认返回,需要主动指定 _id: 0 才会隐藏

两种模式不可混用(因为这样的话无法推断其他键是否应返回)

  1. db.collection.find(query, {title: 1, by: 0}) // 错误

只能全1或全0,除了在inclusion模式时可以指定_id为0

  1. db.collection.find(query, {_id:0, title: 1, by: 1}) // 正确

若不想指定查询条件参数 query 可以 用 {} 代替,但是需要指定 projection 参数:

  1. querydb.collection.find({}, {title: 1})

$slice操作符可以返回数组字段中的部分元素

  1. db.col.find({title:/^MongoDB/}, {_id:0, title: 1, tags: {$slice: 1}})

查询操作符

比较操作符

语法:

{ field: {$<opetator>: value} }

$eq:匹配字段值相等的文档

$ne:匹配字段不等的文档($ne也会筛选出不包含查询字段的文档,如复合主键)

$gt:匹配字段值大于查询值的文档

$gte:匹配字段值大于或等于查询值的文档

$lt:匹配字段值小于查询值的文档

$lte:匹配字段值小于或等于查询值的文档

$in:匹配字段值与任一查询值相等的文档

$nin:匹配字段值与任何查询值都不等的文档(nin也会筛选出不包含查询字段的文档,如复合主键)

  1. // 比较运算符
  2. db.col.find({title: {$eq: 'MongoDB 教程'}})
  3. db.col.find({title: 'MongoDB 教程'})
  4. db.col.find({title: {$ne: 'MongoDB 教程'}})
  5. db.col.find({likes: {$gt: 80}})
  6. db.col.find({likes: {$gte: 70}})
  7. db.col.find({likes: {$lt: 70}})
  8. db.col.find({likes: {$lte: 10}})
  9. db.col.find({likes: {$lte: 10}})
  10. db.col.find({likes: {$in: [10, 100]}})
  11. db.col.find({likes: {$nin: [10, 100]}})

逻辑操作符

逻辑操作符都是元操作符,意味着他们可以放在其他任何操作符之上

$not

语法:

{ field: { $not: { <operator-expression> } } }

$not 会筛选出不包含查询字段的文档,如复合主键

$and

语法:

{ $and: [ { <expression1> }, { <expression2> } , ... , { <expressionN> } ] }

简写:

{ <expression1> }, { <expression2> } , ... , { <expressionN> }

$or

语法:

{ $or: [ { <expression1> }, { <expression2> }, ... , { <expressionN> } ] }

  • 第一个expression1应匹配尽量多的结果
  • 使用$or查询,每个expression可以优先选用其自己的索引(而非符合索引)
  • 除非所有的expression都对应相应的索引,不然$or没有办法使用索引
  • 因为text查询必须使用索引,所以当同时使用or和$text的时候,必须所有的expression都有索引,不然会抛出错误
  • 如果有可能,尽量用in代替or

$nor

语法:

{ $nor: [ { <expression1> }, { <expression2> }, ... { <expressionN> } ] }

  • 当使用$nor去查询时,不仅包括不符合这个表达式的,还包括不存在于这个表达式中的field字段
  • 可以和$exists配合使用,例如:
  1. db.inventory.find( { $nor: [
  2. { price: 1.99 }, { price: { $exists: false } },
  3. { sale: true }, { sale: { $exists: false } }
  4. ] } )
  1. // 逻辑操作符
  2. db.col.find({'likes': {$not: {$lte: 70}}})
  3. db.col.find({$and:[{'likes': {$lte: 70}}, {'title': 'MongoDB 教程'}]})
  4. db.col.find({'title': 'MongoDB 教程', 'likes': {$lte: 70})
  5. db.col.find({'likes': {$lte: 70}, 'title': 'MongoDB 教程'}) // 简写
  6. db.col.find( { 'likes': { $lt: 70 , $gt: 10}} ) // 简写
  7. db.col.find({$or:[{'likes': {$lte: 70}}, {'title': 'MongoDB 教程'}]})
  8. db.col.find({$nor:[{'likes': {$gt: 70}}, {'title': 'MongoDB进阶'}]})

字段操作符

$exists

语法: { field: { $exists: <boolean> } }

匹配包含该字段的文档,即使该字段的值为null

$type

语法:

{ field: { $type: <BSON type> } }

{ field: { $type: [ <BSON type1> , <BSON type2>, ... ] } }

匹配字段类型符合查询值的文档

  1. // 字段操作符
  2. db.col.find({'_id.type': {$exists: true}})
  3. db.col.find({"title" : {$type : 'string'}})

数组操作符

$all

语法: { <field>: { $all: [ <value1> , <value2> ... ] } }

匹配数组字段中包含所有查询值的文档

$elemMatc

语法: { <field>: { $elemMatch: { <query1>, <query2>, ... } } }

匹配数组字段中至少存在一个值满足筛选条件的文档

$size

语法: { <field>: { $size: length} }

匹配满足指定数组的长度的文档,只能是固定值,不能是范围

运算操作符

$regex

{ : { $regex: /pattern/, $options: ‘’ } }
{ : { $regex: /pattern/ } }

兼容PCRE v8.41正则表达库,在和$in操作符一起使用时,只能使用 /pattern/,不能使用 $regex 运算符表达式。

在设置索弓的字段上进行正则匹配可以提高查询速度,而且当正则表达式使用的是前缀表达式时,查询速度会进一步提高,例如:{ name: { $regex: /^a/ }。

option参数的含义:

选项 含义 使用要求
i 大小写不敏感
m 多行匹配模式
x 忽视所有空白字符 要求MongoDB - 图3option合用
s 单行匹配模式 要求MongoDB - 图4option合用
  1. // 运算操作符
  2. db.col.find({title: { $in: [/^M/i, /阶$/]}})
  3. db.col.find({title: { $regex: /^m/, $options: 'i'}})

文档游标

基本概念

db.collection.find()返回一个文档集合的游标

在不遍历游标的情况下,只列出20个文档,我们可以使用游标下标直接访问文档集合中的某个文档

游标的销毁

  • 客户端发来信息让其销毁
  • 游标迭代完毕
  • 默认游标超过10分钟自动清除

可以使用noCursorTimeout()函数来保持游标一直有效,在不遍历游标的情况下,你需要主动关闭游标,否则会一直在数据库中消耗服务器资源

  1. // 文档游标
  2. var myCursor = db.col.find()
  3. var myCursor = db.col.find().noCursorTimeout()
  4. myCursor // 前 20 条文档
  5. myCursor.close()

游标函数

cursor.hasNext() 判断游标是否取到尽头

cursor.next() 获取游标的下一个单元

  1. // 游标遍历
  2. while(myCursor.hasNext()){
  3. printjson(myCursor.next())
  4. }
  5. myCursor.forEach(function(item){
  6. printjson(item)
  7. })

cursor.limit() 读取指定数量的数据记录,参数为 0 相当于不使用limit

cursor.skip() 跳过指定数量的数据

  1. db.col.find().limit(1)
  2. db.col.find().limit(0) // 参数为 0 相当于不使用limit
  3. db.col.find().limit(1).skip(1)

cursor.count() 获取游标中总的文档数量

默认情况下,为false,即cursor.count()不会考虑limit()、skip()的效果,在不提供筛选条件时,cursor.count(会从)集合的元数据Metadata中取得结果。当数据库分布式结构较为复杂时,元数据中的文档数量可能不准确,所有我们应该避免使用不提供筛选条件的cursor.count()函数,而使用聚合管道来计算文档数量。

cursor.sort() 对游标中的文档进行排序,定义了排序要求 { field: ordering}

1表示从小到大正向排序,-1表示从大到小逆向排序

组合使用时注意:

  • cursor.skip()在cursor.limit()之前执行
  • cursor.sort()在cursor.skip()和cursor.limit()之前执行
  • 当结合在一起使用时,游标函数的应用顺序是sort()、skip()、limit()
  1. db.col.find().limit(1).skip(1).count()
  2. db.col.find().limit(1).skip(1).count(true)
  3. db.col.find().sort({likes: 1})
  4. db.col.find().sort({likes: 1}).limit(4).skip(1)

更新文档

MongoDB 使用 update()save() 方法来更新集合中的文档。更新一个文档有两种方式,一种是替换现有的文档内容,另外一种是利用更新操作符对部分字段进行修改。

update() 方法

update() 方法用于更新已存在的文档。语法格式如下:

  1. db.collection.update(
  2. <query>,
  3. <update>,
  4. <upsert>,
  5. <multi>,
  6. <writeConcern>
  7. )

参数说明:

  • query : update的查询条件,类似sql update查询内where后面的。
  • update : 新的文档或者是使用更新操作符对某一个文档进行修改(如MongoDB - 图5inc…)。
  • upsert : 可选,默认情况下,如果update命令的筛选条件没有匹配任何文档,则不会进行任何操作,将upsert设置为true,如果update命令的筛选条件没有匹配任何文档,则会创建新文档。
  • multi : 可选,mongodb 默认是false,只更新找到的第一条文档,如果这个参数为true,就把按条件查出来多条文档全部更新。
  • writeConcern : 可选,安全写级别,等级越高,其数据的安全性就越高。

注意:MongoDB只能保证单个文档操作的原子性,不能保证多个文档操作的原子性,更新多个文档虽然在单一线程中执行,但是线程在执行过程中可能被挂起,以便其他线程也有机会对数据进行操作,如果需要保证多个文档操作的原子性,需要使用MongoDB 4.0版本引入的事务功能进行操作。有关事务功能的讲解,可以参考MongoDB 4.0新特性课程

实例

我们在集合 col 中插入如下数据:

  1. >db.col.insert({
  2. title: 'MongoDB 教程',
  3. description: 'MongoDB 是一个 Nosql 数据库',
  4. by: '菜鸟教程',
  5. url: 'http://www.runoob.com',
  6. tags: ['mongodb', 'database', 'NoSQL'],
  7. likes: 100
  8. })

接着我们通过 update() 方法来更新标题(title):

更新整个文档

  1. db.col.update({'title':'MongoDB 教程'},{{'title':'MongoDB'}})
  2. WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 }) # 输出信息
  3. db.col.find().pretty()
  4. {
  5. "title" : "MongoDB",
  6. }

通过字段更新操作符更新title字段

  1. >db.col.update({'title':'MongoDB 教程'},{$set:{'title':'MongoDB'}})
  2. WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 }) # 输出信息
  3. > db.col.find().pretty()
  4. {
  5. "_id" : ObjectId("56064f89ade2f21f36b03136"),
  6. "title" : "MongoDB",
  7. "description" : "MongoDB 是一个 Nosql 数据库",
  8. "by" : "菜鸟教程",
  9. "url" : "http://www.runoob.com",
  10. "tags" : [
  11. "mongodb",
  12. "database",
  13. "NoSQL"
  14. ],
  15. "likes" : 100
  16. }

可以看到标题(title)由原来的 “MongoDB 教程” 更新为了 “MongoDB”。

以上语句只会修改第一条发现的文档,如果你要修改多条相同的文档,则需要设置 multi 参数为 true。

  1. >db.col.update({'title':'MongoDB 教程'},{$set:{'title':'MongoDB'}},{multi:true})

save() 方法

save() 方法通过传入的文档来替换已有文档。语法格式如下:

  1. db.collection.save(
  2. <document>,
  3. <writeConcern>
  4. )

参数说明:

  • document : 文档数据,如果document中包含了_id字段,save()命令将会调用update()命令,且upsert选项为true,如果文档中的_id字段没有匹配到任何文档,则会创建新文档。
  • writeConcern:可选,安全写级别,等级越高,其数据的安全性就越高。

实例

以下实例中我们替换了 _id 为 56064f89ade2f21f36b03136 的文档数据:

  1. >db.col.save({
  2. "_id" : ObjectId("56064f89ade2f21f36b03136"),
  3. "title" : "MongoDB",
  4. "description" : "MongoDB 是一个 Nosql 数据库",
  5. "by" : "Runoob",
  6. "url" : "http://www.runoob.com",
  7. "tags" : [
  8. "mongodb",
  9. "NoSQL"
  10. ],
  11. "likes" : 110
  12. })

替换成功后,我们可以通过 find() 命令来查看替换后的数据

  1. >db.col.find().pretty()
  2. {
  3. "_id": ObjectId("56064f89ade2f21f36b03136"),
  4. "title": "MongoDB",
  5. "description": "MongoDB 是一个 Nosql 数据库",
  6. "by": "Runoob",
  7. "url": "http://www.runoob.com",
  8. "tags": ["mongodb", "NoSQL"],
  9. "likes": 110
  10. }

更新操作符

字段更新操作符
操作符 功能
$set 更新、新增字段(字段不存在时)
$unset 删除字段
$rename 重命名字段(可用于更改字段位置,不能操作为数组元素的字段)
$inc 加、减字段值,取决于更新值得正负
$mul 相乘字段值
$min 比较字段值和更新值取最小值
$max 比较字段值和更新值取最大值

注意事项:

  1. inc和mul操作符只能应用于数值字段上,用在非数值字段上会报错,如果被更新的字段不存在,inc操作会把更新的字段添加进文档中,并且字段值取0然后加上更新值。对于mul字段值取0会乘上更新值,所以得到的字段值将会是0。
  2. min和max操作符不局限于数值字段,如果被更新的字段和更新值类型不一致,min和max会按照BSON数据类型的排序规则进行比较,同样的,当被更新字段不存在时,min和max会创建字段,并且将字段值设为更新值。
  3. BSON排序规则 | 最小 | Null | | —- | —- | | | Number(ints, longs, doubles, decimals) | | | Symbol, String | | | Object | | | Array | | | BinData | | | ObjectId | | | Boolean | | | Date | | | Timestamp | | 最大 | Regular Expression |
  1. // 字段更新操作符
  2. db.col.update({title: 'MongoDB 教程'},{$set: {likes: 300}})
  3. db.col.update({'title':'MongoDB 教程'},{$set:{'title':'MongoDB'}}, false, true)
  4. db.col.updateMany({'title':'MongoDB'},{$set:{'title':'MongoDB 教程'}})
  5. db.col.updateMany({'title':'MongoDB 教程'},{$set:{'info': {date: Date(), adress: 'ZH'}}})
  6. db.col.updateMany({'title':'MongoDB 教程'},{$set:{'tags.0': 'mongo'}})
  7. db.col.updateMany({'title':'MongoDB 教程'},{$set:{'info.adress': 'AS'}})
  8. db.col.updateMany({'title':'MongoDB 教程'},{$unset:{'arr': ''}})
  9. db.col.updateMany({'title':'MongoDB 教程'},{$rename:{'title':'article'}})
  10. db.col.updateMany({'article':'MongoDB 教程'},{$rename:{'info.adress': 'adress', 'article': 'info.article'}})

数组更新操作符
操作符 功能
$ 占位符
$push 添加元素到数组
$addToSet 添加元素到数组
$pop 删除数组第一个或最后一个元素
$pull 删除数组中匹配的元素
$pullAll 删除数组中匹配的多个元素
$each 结合 $push 和 $addToSet 操作多个值
$position 结合 $push 将元素插入数组中指定的位置
$sort 结合 push、each 对数组进行排序
$slice 结合push、each 截取部分数组

position、sort、$slice可以一起使用:

这三个操作符的执行顺序是:position、sort、$slice,写在命令中的操作符顺序并不重要,并不会影响命令的执行顺序

  1. // 更新文档
  2. // 匹配更新
  3. db.col.update({title: 'MongoDB 进阶'},{name: 'gongyz'})
  4. db.col.update({name:'gongyz'},{adress: '江西'}, {upsert: true})
  5. // 如果从筛选条件中可以确定字段值,那么新创建的文档将包含筛选条件涉及的字段
  6. db.col.update({name:'gongyz'},{$set: {adress: '江西'}}, {upsert: true})
  7. // 不过,如果无法从筛选条件中推断出确定的字段值,那么新创建的文档就不会包含筛选条件涉及的字段
  8. db.col.update({num: {$gt: 20000}},{$set: {name: 'nick'}}, {upsert: true})
  9. // 字段更新操作符
  10. db.col.update({title: 'MongoDB 教程'},{$set: {likes: 300}})
  11. db.col.update({'title':'MongoDB 教程'},{$set:{'title':'MongoDB'}}, false, true)
  12. db.col.updateMany({'title':'MongoDB'},{$set:{'title':'MongoDB 教程'}})
  13. db.col.updateMany({'title':'MongoDB 教程'},{$set:{'info': {date: Date(), adress: 'ZH'}}})
  14. db.col.updateMany({'title':'MongoDB 教程'},{$set:{'tags.0': 'mongo'}})
  15. db.col.updateMany({'title':'MongoDB 教程'},{$set:{'info.adress': 'AS'}})
  16. db.col.updateMany({'title':'MongoDB 教程'},{$unset:{'arr': ''}})
  17. db.col.updateMany({'title':'MongoDB 教程'},{$rename:{'title':'article'}})
  18. db.col.updateMany({'article':'MongoDB 教程'},{$rename:{'info.adress': 'adress', 'article': 'info.article'}})
  19. // 数组更新操作符
  20. // $addToSet会将数组插入被更新的数组字段中,成为内嵌数组,如果想将多个元素直接添加到数组字段中,则需要使用$each操作符
  21. db.col.updateMany({'title':'MongoDB 教程'},{$addToSet: {'arr': [3, 4]}})
  22. db.col.updateMany({'title':'MongoDB 教程'},{$addToSet: {'arr': {'name': 'gongyz', age: 23}}})
  23. db.col.updateMany({'title':'MongoDB 教程'}, {$addToSet: {arr: {$each: [1, 2]}}})
  24. db.col.updateMany({'title':'MongoDB 教程'}, {$pop: {arr: 1}}) // 删除最后一个元素
  25. db.col.updateMany({'title':'MongoDB 教程'}, {$pop: {arr: -1}}) // 删除第一个元素
  26. db.col.updateMany( {'title':'MongoDB 教程'}, { $pull: { tags: {$regex: /mo/} } } )
  27. db.col.updateMany( {'title':'MongoDB 教程'}, { $pull: { tags: {$regex: /mo/} } } )
  28. // 如果要删除的元素是一个数组,数组元素的值和排列顺序都必须和被删除的元素完全一致
  29. db.col.updateMany( {'title':'MongoDB 教程'}, { $pullAll: { arr: [[3, 4]] } } )
  30. // 如果要删除的元素是一个文档,$pullAll要求文档的值和排列顺序都必须和被删除的元素完全一致,$pull不需要完全一致
  31. db.col.updateMany( {'title':'MongoDB 教程'}, { $pullAll: { arr: [{'name': 'gongyz', 'age': 23}] } } )
  32. db.col.updateMany( {'title':'MongoDB 教程'}, { $pull: { arr: {'name': 'gongyz'} } } )
  33. // $push和$addToSet命令相似,但是$push命令的功能更加强大,$push和$addToSet一样,如果$push命令中指定的字段不存在,这个字段会被添加到集合中
  34. db.col.updateMany({'title':'MongoDB 教程'},{$push: {'arr': [5, 6]}})
  35. db.col.updateMany({'title':'MongoDB 教程'}, {$push: {arr: {$each: [7, 8]}}})
  36. // 使用$position将元素插入数组中指定的位置
  37. db.col.updateMany({'title':'MongoDB 教程'}, {$push: {arr: {$each: [0], $position: 0}}})
  38. // 位置倒过来计算,插入到最后一个元素前面
  39. db.col.updateMany({'title':'MongoDB 教程'}, {$push: {arr: {$each: [9], $position: -1}}})
  40. // 使用$sort对数组进行排序 1(从小到大) -1(从大到小),使用$sort时必须要使用$push和$each
  41. db.col.updateMany({'title':'MongoDB 教程'}, {$push: {arr: {$each: [10], $sort: -1}}})
  42. // 如果插入的是内嵌文档,可以根据内嵌文档的字段排序
  43. db.col.updateMany({'title':'MongoDB 教程'}, {$push: {arr: {$each: [{value: 100}, {value: 200}], $sort: {value: -1}}}})
  44. // 如果不想插入元素,只想对文档中的数组字段进行排序
  45. db.col.updateMany({'title':'MongoDB 教程'}, {$push: {arr: {$each: [], $sort: -1}}})
  46. // 使用$slice截取部分数组
  47. db.col.updateMany({'title':'MongoDB 教程'}, {$push: {arr: {$each: [1], $slice: -2}}})
  48. // 如果不想插入元素,只想截取部分数组
  49. db.col.updateMany({'title':'MongoDB 教程'}, {$push: {arr: {$each: [], $slice: 2}}})
  50. // $position、$sort、$slice可以一起使用,但这三个操作符的执行顺序是:$position、$sort、$slice,写在命令中操作符的顺序并不重要,并不会影响命令的执行顺序
  51. db.col.updateMany({'title':'MongoDB 教程'}, {$push: {arr: {$each: [6, 8], $position: 0, $sort: -1, $slice: 2}}})
  52. // 更新数组中所有元素
  53. db.col.updateMany({'title':'MongoDB 教程'}, {$set: {'arr.$[]': 'updated'}})
  54. // $ 是数组中第一个符合筛选条件的数组元素的占位符(query中需要指明要更新的数组元素)
  55. db.col.updateMany({'title':'MongoDB 教程','arr': 'updated'}, {$set: {'arr.$': 1}})
  56. // save()命令更新文档
  57. db.col.save({
  58. _id: ObjectId("5ce5ef8caa5dacd1c36450e1"),
  59. title: 'MongoDB 新教程',
  60. description: 'MongoDB 是一个 Nosql 数据库',
  61. by: '菜鸟教程',
  62. url: 'http://www.runoob.com',
  63. tags: ['mongodb', 'database', 'NoSQL'],
  64. likes: 1000
  65. })

删除文档

语法

remove() 方法的基本语法格式如下所示:

  1. db.collection.remove(
  2. <query>,
  3. <justOne>
  4. )

如果你的 MongoDB 是 2.6 版本以后的,语法格式如下:

  1. db.collection.remove(
  2. <query>,
  3. {
  4. justOne: <boolean>,
  5. writeConcern: <document>
  6. }
  7. )

参数说明:

  • query :(可选)删除的文档的条件。
  • justOne : (可选)如果设为 true 或 1,则只删除一个文档,如果不设置该参数,或使用默认值 false,则删除所有匹配条件的文档。
  • writeConcern : (可选),安全写级别,等级越高,其数据的安全性就越高。
  1. // 删除文档
  2. // 默认情况下,remove命令会删除所有符合筛选条件的文档
  3. db.col.remove({'title':'MongoDB 教程'})
  4. // 删除符合筛选条件的第一篇文档
  5. db.col.remove({'title':'MongoDB 教程'}, 1)
  6. db.col.remove({'title':'MongoDB 教程'}, {justOne: true})
  7. db.col.remove({'title':'MongoDB 教程'}, true)
  8. // 删除所有文档(不会删除集合)
  9. db.col.remove({})
  10. // 如果集合中文档的数量很多,使用remove命令删除所有文档的效率不高,在这种情况下,更加有效率的方法,
  11. // 是使用drop命令删除集合,然后再创建空集合并创建索引

MongoDB基本操作进阶之聚合

MongoDB中聚合(aggregate)主要用于处理数据(诸如统计平均值,求和等),并返回计算后的数据结果,MongoDB中聚合的方法使用aggregate(),该方法返回的是文档游标

aggregate() 方法

语法

  1. db.<collection>.aggregate(<pipeline>, <options>)

定义了操作中使用的聚合表达式和管道操作符

声明了一些聚合操作的参数

聚合表达式

字段路径表达式

$<filed> 使用$来指示文档字段

$<filed>.<sub-filed> 使用$和.来指示内嵌文档字段

系统变量表达式

<variable> 使用来指示系统变量

$$CURRENT 指示管道中当前操作的文档

  1. $$CURRENT.<filed> $<filed>是等效的

常量表达式

$literal: <value> 指示常量字

$literal: '$name' 指示常量字符串’name’,这里被当作常量处理,而不是字段路径表达式

聚合管道操作符

  • $project:对输入文档进行再次投影
  • $match:对输入文档进行筛选
  • $limit:筛选出管道内前N篇文档
  • $skip:跳过管道内前N篇文档
  • $unwind:展开输入文档中的数组字段
  • $sort:对输入文档进行排序
  • $lookup:对输入文档进行查询操作
  • $group:对输入文档进行分组
  • $out:将管道中的文档输出
  1. // 聚合操作
  2. db.user.insertMany([
  3. {
  4. name: { firstName: 'alice', lastName: 'wong' },
  5. balance: 50
  6. },
  7. {
  8. name: { firstName: 'bob', lastName: 'yang' },
  9. balance: 50
  10. }
  11. ])
  12. db.user.update(
  13. {
  14. 'name.firstName': 'alice'
  15. },
  16. {
  17. $set: { currency: ['CNY', 'USD'] }
  18. }
  19. )
  20. db.user.update(
  21. {
  22. 'name.firstName': 'bob'
  23. },
  24. {
  25. $set: { currency: 'GBP' }
  26. }
  27. )
  28. db.user.insertMany([
  29. {
  30. name: { firstName: 'charlie', lastName: 'gordon' },
  31. balance: 100
  32. },
  33. {
  34. name: { firstName: 'david', lastName: 'wu' },
  35. balance: 200,
  36. currency: []
  37. },
  38. {
  39. name: { firstName: 'eddie', lastName: 'kim' },
  40. balance: 20,
  41. currency: null
  42. }
  43. ])
  44. db.forex.insertMany([
  45. {
  46. ccy: 'USD',
  47. rate: 6.91,
  48. date: new Date('2018-12-21')
  49. },
  50. {
  51. ccy: 'GBP',
  52. rate: 68.72,
  53. date: new Date('2018-8-21')
  54. },
  55. {
  56. ccy: 'CNY',
  57. rate: 1.0,
  58. date: new Date('2018-12-21')
  59. }
  60. ])
  61. db.transactions.insertMany([
  62. {
  63. symbol: '600519',
  64. qty: 100,
  65. price: 567.4,
  66. currency: 'CNY'
  67. },
  68. {
  69. symbol: '600518',
  70. qty: 2,
  71. price: 5677.4,
  72. currency: 'USD'
  73. },
  74. {
  75. symbol: '31312',
  76. qty: 1010,
  77. price: 5167.4,
  78. currency: 'USD'
  79. }
  80. ])
  81. db.user.remove({})
  82. // $project 对输入文档进行再次投影
  83. db.user.aggregate([
  84. {
  85. $project: {
  86. _id: 0,
  87. balance: 1,
  88. clientName: '$name.firstName'
  89. }
  90. }
  91. ])
  92. // 字段路径表达式指向的是原文档中不存在的字段
  93. db.user.aggregate([
  94. {
  95. $project: {
  96. _id: 0,
  97. balance: 1,
  98. newArr: ['$name.firstName', '$name.middleName', '$name.lastName']
  99. }
  100. }
  101. ])
  102. // $project是一个很常用的聚合操作符,可以用来灵活控制输出文档的格式,也可以用来剔除不相关的字段,以优化聚合管道操作的性能
  103. // $match 对输入文档进行筛选
  104. db.user.aggregate([
  105. {
  106. $match: {
  107. 'name.firstName': 'alice'
  108. }
  109. }
  110. ])
  111. db.user.aggregate([
  112. {
  113. $match: {
  114. $or: [{ balance: { $gt: 40, $lt: 80 } }, { 'name.firstName': 'yang' }]
  115. }
  116. }
  117. ])
  118. // 将筛选和投影操作符结合在一起
  119. db.user.aggregate([
  120. {
  121. $match: {
  122. $or: [{ balance: { $gt: 40, $lt: 80 } }, { 'name.firstName': 'yang' }]
  123. }
  124. },
  125. {
  126. $project: {
  127. _id: 0
  128. }
  129. }
  130. ])
  131. // $match也是一个很常用的聚合操作符,应该尽量在聚合管道的开始阶段应用$match,这样可以减少后续阶段中需要处理的文档数量,优化聚合操作的性能
  132. // $limit $skip
  133. db.user.aggregate([
  134. {
  135. $limit: 1
  136. }
  137. ])
  138. db.user.aggregate([
  139. {
  140. $skip: 1
  141. }
  142. ])
  143. // $unwind 展开输入文档中的数组字段,会将指定字段数组元素拿出来创建新文档,新文档的主键_id都相同
  144. db.user.aggregate([
  145. {
  146. $unwind: {
  147. path: '$currency'
  148. }
  149. }
  150. ])
  151. // 展开时将数组元素在原数组中的下标位置写入一个指定的字段中
  152. db.user.aggregate([
  153. {
  154. $unwind: {
  155. path: '$currency',
  156. includeArrayIndex: 'ccyIndex'
  157. }
  158. }
  159. ])
  160. // 展开数组时保留空数组或不存在数组的文档
  161. db.user.aggregate([
  162. {
  163. $unwind: {
  164. path: '$currency',
  165. preserveNullAndEmptyArrays: true
  166. }
  167. }
  168. ])
  169. // sort 对输入文档进行排序
  170. db.user.aggregate([
  171. {
  172. $sort: { balance: 1, 'name.lastName': -1 }
  173. }
  174. ])
  175. // $lookup:对输入文档进行查询操作,需要另一个查询集合参与,查询结果会多出一个新字段
  176. // $lookup: {
  177. // from: <collection to join>,
  178. // localField: <field from the input document>,
  179. // foreignField: <field from the documents of the 'from' collection>,
  180. // as: <output array field>
  181. // }
  182. // from: 同一数据库中的另一个查询集合
  183. // localFiled: 输入文档的字段
  184. // foreignFiled:查询集合中字段
  185. // as:给新插入的字段取一个名字
  186. // 单一字段查询
  187. db.user.aggregate([
  188. {
  189. $lookup: {
  190. from: 'forex',
  191. localField: 'currency',
  192. foreignField: 'ccy',
  193. as: 'forexData'
  194. }
  195. }
  196. ])
  197. // 如果localField是一个数组字段,可以先对数组字段进行展开
  198. db.user.aggregate([
  199. {
  200. $unwind: {
  201. path: '$currency',
  202. preserveNullAndEmptyArrays: true
  203. }
  204. },
  205. {
  206. $lookup: {
  207. from: 'forex',
  208. localField: 'currency',
  209. foreignField: 'ccy',
  210. as: 'forexData'
  211. }
  212. }
  213. ])
  214. // 使用复杂条件进行查询
  215. // 对查询集合中的文档使用管道操作符处理时,如果需要参考输入文档中的字段,则必须使用let参数对字段进行声明
  216. // $lookup: {
  217. // from: <collection to join>,
  218. // let: {<var_1>: <expression>, ..., <var_n>: <expression>},
  219. // pipeline: [<pipeline to execute on the collection to join>],
  220. // as: <output array field>
  221. // }
  222. // 不相关查询,查询条件和输入文档直接没有直接的联系,$lookup从3.6版本开始支持不相关查询
  223. db.user.aggregate([
  224. {
  225. $lookup: {
  226. from: 'forex',
  227. pipeline: [
  228. {
  229. $match: {
  230. date: new Date('2018-12-21')
  231. }
  232. }
  233. ],
  234. as: 'forexData'
  235. }
  236. }
  237. ])
  238. // 相关查询(使用let声明定义了需要使用的输入文档中的字段时,pipeline中需要使用$expr操作符)
  239. db.user.aggregate([
  240. {
  241. $lookup: {
  242. from: 'forex',
  243. let: { bal: '$balance' },
  244. pipeline: [
  245. {
  246. $match: {
  247. $expr: {
  248. $and: [{ $eq: ['$date', new Date('2018-12-20')] }, { $gt: ['$$bal', 100] }]
  249. }
  250. }
  251. }
  252. ],
  253. as: 'forexData'
  254. }
  255. }
  256. ])
  257. // $group:对输入文档进行分组
  258. // $group: {
  259. // _id: <expression>,
  260. // <field1>: {<accumulator1> : <expression1>},
  261. // ...
  262. // }
  263. // _id: 定义分组规则
  264. db.transactions.aggregate([
  265. {
  266. $group: {
  267. _id: '$currency'
  268. }
  269. }
  270. ])
  271. // 不使用聚合操作符的情况下,$group可以返回输入文档中某一字段的所有(不重复的)值
  272. // 使用聚合操作符计算分值聚合值
  273. db.transactions.aggregate([
  274. {
  275. $group: {
  276. _id: '$currency',
  277. totalQty: { $sum: '$qty' },
  278. totalNotional: { $sum: { $multiply: ['$price', '$qty'] } },
  279. avgPrice: { $avg: '$price' },
  280. count: { $sum: 1 },
  281. maxNotional: { $max: { $multiply: ['$price', '$qty'] } },
  282. minNotional: { $min: { $multiply: ['$price', '$qty'] } }
  283. }
  284. }
  285. ])
  286. // 使用聚合操作符计算所有文档聚合值
  287. db.transactions.aggregate([
  288. {
  289. $group: {
  290. _id: null,
  291. totalQty: { $sum: '$qty' },
  292. totalNotional: { $sum: { $multiply: ['$price', '$qty'] } },
  293. avgPrice: { $avg: '$price' },
  294. count: { $sum: 1 },
  295. maxNotional: { $max: { $multiply: ['$price', '$qty'] } },
  296. minNotional: { $min: { $multiply: ['$price', '$qty'] } }
  297. }
  298. }
  299. ])
  300. // 使用聚合操作符创建数组字段
  301. db.transactions.aggregate([
  302. {
  303. $group: {
  304. _id: '$currency',
  305. symbols: { $push: '$symbol' }
  306. }
  307. }
  308. ])
  309. // $out 将管道中的文档输出写入一个新集合
  310. db.transactions.aggregate([
  311. {
  312. $group: {
  313. _id: '$currency',
  314. symbols: { $push: '$symbol' }
  315. }
  316. },
  317. {
  318. $out: 'output'
  319. }
  320. ])
  321. // $out 将管道中的文档输出写入一个已存在的集合,新集合会覆盖旧的集合
  322. db.transactions.aggregate([
  323. {
  324. $group: {
  325. _id: '$currency',
  326. totalNotional: { $sum: { $multiply: ['$price', '$qty'] } }
  327. }
  328. },
  329. {
  330. $out: 'output'
  331. }
  332. ])
  333. // 如果聚合管道操作遇到错误,$out不会创建新集合或者是覆盖已存在的集合内容

MongoDB局限

每个聚合管道阶段使用的内存不能超过100MB,如果数据量较大,为了防止聚合管道阶段超出内存上线并且抛出错误,可以启用allowDiskUse选项,在allowDiskUse启用之后,聚合阶段可以在内存量不足时,将操作数据写入临时文件中,临时文件会被写入dbPath下的_tmp文件夹,dbPath的默认值为/data/db。

MongoDB对聚合操作的优化

  1. // 聚合阶段顺序优化
  2. // $project + $match
  3. // $match阶段会在$project阶段之前运行
  4. db.transactions.aggregate([
  5. {
  6. $project: {
  7. _id: 0,
  8. symbol: 1,
  9. currency: 1,
  10. notional: { $multiply: ['$price', '$qty'] }
  11. }
  12. },
  13. {
  14. $match: {
  15. currency: 'USD',
  16. notional: { $gt: 1000 }
  17. }
  18. }
  19. ])
  20. // 相当于
  21. db.transactions.aggregate([
  22. {
  23. $match: {
  24. currency: 'USD'
  25. }
  26. },
  27. {
  28. $project: {
  29. _id: 0,
  30. symbol: 1,
  31. currency: 1,
  32. notional: { $multiply: ['$price', '$qty'] }
  33. }
  34. },
  35. {
  36. $match: {
  37. notional: { $gt: 1000 }
  38. }
  39. }
  40. ])
  41. // $project + $sort
  42. // $match阶段会在$sort阶段之前运行
  43. db.transactions.aggregate([
  44. {
  45. $sort: {
  46. price: 1
  47. }
  48. },
  49. {
  50. $match: {
  51. currency: 'USD'
  52. }
  53. }
  54. ])
  55. // 相当于
  56. db.transactions.aggregate([
  57. {
  58. $match: {
  59. currency: 'USD'
  60. }
  61. },
  62. {
  63. $sort: {
  64. price: 1
  65. }
  66. }
  67. ])
  68. // $project + $skip
  69. // $skip阶段会在$project阶段之前运行
  70. db.transactions.aggregate([
  71. {
  72. $project: {
  73. _id: 0,
  74. symbol: 1,
  75. currency: 1,
  76. notional: { $multiply: ['$price', '$qty'] }
  77. }
  78. },
  79. {
  80. $skip: 2
  81. }
  82. ])
  83. // 相当于
  84. db.transactions.aggregate([
  85. {
  86. $skip: 2
  87. },
  88. {
  89. $project: {
  90. _id: 0,
  91. symbol: 1,
  92. currency: 1,
  93. notional: { $multiply: ['$price', '$qty'] }
  94. }
  95. }
  96. ])
  97. // 聚合阶段合并优化
  98. // $sort + $limit
  99. // 如果两者之间没有夹杂着会改变文档数量的聚合阶段,$sort和$limit阶段可以合并
  100. db.transactions.aggregate([
  101. {
  102. $sort: { price: 1 }
  103. },
  104. {
  105. $project: {
  106. _id: 0,
  107. symbol: 1,
  108. currency: 1,
  109. notional: { $multiply: ['$price', '$qty'] }
  110. }
  111. },
  112. {
  113. $limit: 2
  114. }
  115. ])
  116. // $limit + $limit
  117. // $skip + $skip
  118. // $match + $match
  119. // 连续的$limit,$skip,$match阶段排列在一起时,可以合并为一个阶段
  120. // {$limit: 10},
  121. // {$limit: 5}
  122. // 合并
  123. // {$limit: 15}
  124. // {$skip: 10},
  125. // {$skip: 5}
  126. // 合并
  127. // {$skip: 15}
  128. // {$match: {currency: 'USD'}},
  129. // {$match: {qty: 1}}
  130. // 合并
  131. {
  132. $match: {
  133. $and: [{ currency: 'USD' }, { qty: 1 }]
  134. }
  135. }
  136. // $lookup + $unwind
  137. // 连续排列在一起的$lookup和$unwind阶段,如果$unwind应用在$lookup阶段创建的as字段上,则两者可以合并
  138. db.transactions.aggregate([
  139. {
  140. $lookup: {
  141. from: 'forex',
  142. localField: 'currency',
  143. foreignField: 'ccy',
  144. as: 'forexData'
  145. }
  146. },
  147. {
  148. $unwind: '$forexData'
  149. }
  150. ])

论MongoDB中索引的重要性

基本概念

  • 对文档指定字段进行排序的数据结构
  • 加快文档查询和排序的速度
  • 复合键索引只能支持前缀子查询

例:A、B、C三个字段的复合索引,对索引中A、AB、ABC字段的查询称之为前缀子查询

基本操作

创建索引

createIndex()方法基本语法格式如下所示:

  1. >db.collection.createIndex(keys, options)

语法中 Key 值为你要创建的索引字段,1 为指定按升序创建索引,如果你想按降序来创建索引指定为 -1 即可。

  1. // 创建一个单键索引
  2. db.userWithIndex.createIndex({name: 1})
  3. // 创建一个复合索引
  4. db.userWithIndex.createIndex({name: 1, balance: -1})
  5. // 创建一个多键索引(用于数组的索引,数组字段中的每一个元素,都会在多键索引中创建一个键)
  6. db.userWithIndex.createIndex({currency: 1})
  7. // 查看集合中已经存在的索引
  8. db.userWithIndex.getIndexes()

查看索引

  1. db.userWithIndex.getIndexes()

查询分析

  • 检视索引的效果
  • explain()
  1. // 使用没有创建索引的字段进行搜索
  2. // COLLSCAN (Collection Scan 扫描整个集合,低效的查询)
  3. db.userWithIndex.find({balance: 100}).explain()
  4. // 使用已经创建索引的字段进行搜索
  5. // IXSCAN —> FETCH
  6. // 通过索引完成初步筛选,再根据索引中指示的文档储存地址,把对应的文档提取出来
  7. db.userWithIndex.find({name: 'alice'}).explain()
  8. // 仅返回创建了索引的字段(查询效率更高)
  9. // PROJECTION —> IXSCAN
  10. db.userWithIndex.find({name: 'alice'}, {_id: 0, name: 1}).explain()
  11. // 使用已经创建索引的字段进行排序
  12. // IXSCAN —> FETCH
  13. db.userWithIndex.find().sort({name: 1, balance: -1}).explain()
  14. // 使用未创建索引的字段进行排序
  15. // COLLSCAN —> SORT_KEY_GENERATOR —> SORT
  16. db.userWithIndex.find().sort({name: 1, balance: 1}).explain()
  17. // 删除索引
  18. db.userWithIndex.dropIndex()
  19. // 如果需要更改某些字段上已经创建的索引,必须首先删除原有索引,再重新创建新索引,否则,新索引不会包含原有文档
  20. // 使用索引名称删除索引
  21. db.userWithIndex.dropIndex('name_1')
  22. // 使用索引定义删除索引
  23. db.userWithIndex.dropIndex({name: 1, balance: -1})

删除索引

  1. db.userWithIndex.dropIndex()
  2. // 如果需要更改某些字段上已经创建的索引,必须首先删除原有索引,再重新创建新索引,否则,新索引不会包含原有文档
  3. // 使用索引名称删除索引
  4. db.userWithIndex.dropIndex('name_1')
  5. // 使用索引定义删除索引
  6. db.userWithIndex.dropIndex({name: 1, balance: -1})

索引选项

  1. // options 定义了创建索引时可以使用的一些参数,也可以设定索引的特性
  2. // 创建具有唯一性的索引
  3. db.userWithIndex.createIndex({balance: 1}, {unique: true})
  4. // 如果已有文档中的某个字段出现了重复值,就不可以在这个字段上创建唯一性索引
  5. db.userWithIndex.createIndex({name: 1}, {unique: true})
  6. // 如果新增的文档不包含唯一性索引,只有第一个缺少改字段的文档可以被写入数据库,索引中该文档的键值被默认为null
  7. db.userWithIndex.insert({name: 'cherlie', lastAccess: new Date()})
  8. db.userWithIndex.insert({name: 'david', lastAccess: new Date()})
  9. // 复合键索引也可以具有唯一性,在这种情况下,不同的文档之间,其所包含的复合键字段值的组合,不可以重复
  10. // 创建具有稀疏性的索引
  11. db.userWithIndex.createIndex({balance: 1}, {sparse: true})
  12. // 只将包含索引字段的文档加入到索引中(即便索引字段值为 null)
  13. db.userWithIndex.insert({name: 'cherlie', lastAccess: new Date()})
  14. // 如果同一个索引既具有唯一性,又具有稀疏性,就可以保存多篇缺失索引键值得文档了
  15. db.userWithIndex.createIndex({balance: 1}, {unique: true, sparse: true})
  16. db.userWithIndex.insert({name: 'cherlie', lastAccess: new Date()})
  17. // 复合键索引也可以具有稀疏性,在这种情况下,只有在缺失复合键所包含的所有字段的情况下,文档才不会被加入到索引中

索引的生存时间

  1. // 针对日期字段,或者包含日期字段的数组字段,可以使用设定了生存时间的索引,来自动删除字段值超过生存时间的文档
  2. // 在 lastAccess 字段上创建一个生存时间是20s的索引
  3. db.userWithIndex.createIndex({lastAccess: 1}, {expireAfterSeconds: 20})
  4. db.userWithIndex.insert({name: 'eddie', lastAccess: new Date()})
  5. // 复合键索引不具备生存时间特效
  6. // 当索引建是包含日期元素的数组字段时,数组中最小的日期将被用来计算文档是否过期
  7. // 数据库使用一个后台线程来监测和删除过期的文档,删除操作可能会有一定的延迟

MongoDB之数据模型

待更新…