Bootstrap

mongodb学习笔记[课程来自图灵课堂]


MongoDB相关概念


业务应用场景

传统的关系型数据库(如MySQL),在数据操作的“三高”需求以及应对Web2.0的网站需求面前,显得力不从心。

解释:“三高”需求:

  • High performance - 对数据库高并发读写的需求。
  • Huge Storage - 对海量数据的高效率存储和访问的需求。
  • High Scalability && High Availability- 对数据库的高可扩展性和高可用性的需求。

而MongoDB可应对“三高”需求。

具体的应用场景如:

  1. 社交场景,使用 MongoDB 存储存储用户信息,以及用户发表的朋友圈信息,通过地理位置索引实现附近的人、地点等功能。
  2. 游戏场景,使用 MongoDB 存储游戏用户信息,用户的装备、积分等直接以内嵌文档的形式存储,方便查询、高效率存储和访问。
  3. 物流场景,使用 MongoDB 存储订单信息,订单状态在运送过程中会不断更新,以 MongoDB 内嵌数组的形式来存储,一次查询就能将订单所有的变更读取出来。
  4. 物联网场景,使用 MongoDB 存储所有接入的智能设备信息,以及设备汇报的日志信息,并对这些信息进行多维度的分析。
  5. 视频直播,使用 MongoDB 存储用户信息、点赞互动信息等。

这些应用场景中,数据操作方面的共同特点是:

  1. 数据量大
  2. 写入操作频繁(读写都很频繁)
  3. 价值较低的数据,对事务性要求不高

对于这样的数据,我们更适合使用MongoDB来实现数据的存储。

什么时候选择MongoDB?

在架构选型上,除了上述的三个特点外,如果你还犹豫是否要选择它?可以考虑以下的一些问题:

应用不需要事务及复杂 join 支持
新应用,需求会变,数据模型无法确定,想快速迭代开发
应用需要2000-3000以上的读写QPS(更高也可以)
应用需要TB甚至 PB 级别数据存储
应用发展迅速,需要能快速水平扩展
应用要求存储的数据不丢失
应用需要99.999%高可用
应用需要大量的地理位置查询、文本查询

如果上述有1个符合,可以考虑 MongoDB,2个及以上的符合,选择 MongoDB 绝不会后悔。

思考:如果用MySQL呢?

答:相对MySQL,可以以更低的成本解决问题(包括学习、开发、运维等成本)

MongoDB简介

MongoDB是一个开源、高性能、无模式的文档型数据库,当初的设计就是用于简化开发和方便扩展,是NoSQL数据库产品中的一种。是最
像关系型数据库(MySQL)的非关系型数据库。

它支持的数据结构非常松散,是一种类似于 JSON 的 格式叫BSON,所以它既可以存储比较复杂的数据类型,又相当的灵活。

MongoDB中的记录是一个文档,它是一个由字段和值对(field:value)组成的数据结构。MongoDB文档类似于JSON对象,即一个文档认
为就是一个对象。字段的数据类型是字符型,它的值除了使用基本的一些类型外,还可以包括其他文档、普通数组和文档数组。

体系结构

SQL术语/概念MongoDB术语/概念解释/说明
databasedatabase数据库
tablecollection数据库表/集合
rowdocument数据记录行/文档
column field数据字段/域
indexindex索引
table joins表连接,MongoDB不支持
嵌入文档MongoDB通过嵌入式文档来替代多表连接
primary keyprimary key主键,MongoDB自动将_id字段设置为主键

数据模型

MongoDB的最小存储单位就是文档(document)对象。文档(document)对象对应于关系型数据库的行。数据在MongoDB中以 BSON(Binary-JSON)文档的格式存储在磁盘上。

BSON(Binary Serialized Document Format)是一种类json的一种二进制形式的存储格式,简称Binary JSON。BSON和JSON一样,支持内嵌的文档对象和数组对象,但是BSON有JSON没有的一些数据类型,如Date和BinData类型。

BSON采用了类似于 C 语言结构体的名称、对表示方法,支持内嵌的文档对象和数组对象,具有轻量性、可遍历性、高效性的三个特点,可以有效描述非结构化数据和结构化数据。这种格式的优点是灵活性高,但它的缺点是空间利用率不是很理想。

Bson中,除了基本的JSON类型:string,integer,boolean,double,null,array和object,mongo还使用了特殊的数据类型。这些类型包括

date,object id,binary data,regular expression 和code。每一个驱动都以特定语言的方式实现了这些类型,查看你的驱动的文档来获取详细信息。

BSON数据类型参考列表:

数据类型描述举例
字符串UTF-8字符串都可表示为字符串类型的数据{“x” : “foobar”}
对象id对象id是文档的12字节的唯一 ID{“X” :ObjectId() }
布尔值真或者假:true或者false{“x”:true}+
数组值的集合或者列表可以表示成数组{“x” : [“a”, “b”, “c”]}
32位整数类型不可用。JavaScript仅支持64位浮点数,所以32位整数会被自动转换。shell是不支持该类型的,shell中默认会转换成64位浮点数
64位整数不支持这个类型。shell会使用一个特殊的内嵌文档来显示64位整数shell是不支持该类型的,shell中默认会转换成64位浮点数
64位浮点数shell中的数字就是这一种类型{“x”:3.14159,“y”:3}
null表示空值或者未定义的对象{“x”:null}
undefined文档中也可以使用未定义类型{“x”:undefined}
符号shell不支持,shell会将数据库中的符号类型的数据自动转换成字符串
正则表达式文档中可以包含正则表达式,采用JavaScript的正则表达式语法 {“x” : /foobar/i}
代码文档中还可以包含JavaScript代码{“x” : function() { /* …… */ }}
二进制数据二进制数据可以由任意字节的串组成,不过shell中无法使用
最大值/最小值BSON包括一个特殊类型,表示可能的最大值。shell中没有这个类型

提示:

shell默认使用64位浮点型数值。{“x”:3.14}或{“x”:3}。对于整型值,可以使用NumberInt(4字节符号整数)或NumberLong(8字节符号整数),{“x”:NumberInt(“3”)}{“x”:NumberLong(“3”)}

MongoDB的特点

MongoDB主要有如下特点:

(1)高性能:

MongoDB提供高性能的数据持久性。特别是对嵌入式数据模型的支持减少了数据库系统上的I/O活动。

索引支持更快的查询,并且可以包含来自嵌入式文档和数组的键。(文本索引解决搜索的需求、TTL索引解决历史数据自动过期的需求、地理位置索引可用于构建各种 O2O 应用)

mmapv1、wiredtiger、mongorocks(rocksdb)、in-memory 等多引擎支持满足各种场景需求。

Gridfs解决文件存储的需求。

(2)高可用性:

MongoDB的复制工具称为副本集(replica set),它可提供自动故障转移和数据冗余。

(3)高扩展性:

MongoDB提供了水平可扩展性作为其核心功能的一部分。分片将数据分布在一组集群的机器上。(海量数据存储,服务能力水平扩展)从3.4开始,MongoDB支持基于片键创建数据区域。在一个平衡的集群中,MongoDB将一个区域所覆盖的读写只定向到该区域内的那些片。

(4)丰富的查询支持:

MongoDB支持丰富的查询语言,支持读和写操作(CRUD),比如数据聚合、文本搜索和地理空间查询等。

(5)其他特点:如无模式(动态模式)、灵活的文档模型

docker 安装 mongodb


如果不知道 windows 如何安装 docker 的可以看我这篇文章

打开一个 cmd,按照下列步骤操作

  1. 拉取最新镜像:docker pull mongo

  2. 启动 mongodb 命令:docker run -d --name mongo -p 27017:27017 mongo --auth

    • docker run: 启动一个容器
    • -d 后台启动模式
    • –name mongo 容器的名字叫做 mongo,这里也可以叫其他的名字
    • -p 27017:27017[主机端口号:容器端口号],端口号映射,到时候直接访问主机的 27017 就是在访问 mongo 数据库
    • mongo 这里的mongo是镜像名,指定使用哪个镜像启动容器
    • –auth: mongo 默认是不需要账号密码校验的,这里开启校验
  3. 通过 docker 开启一个窗口访问 mongo:docker exec -it mongo mongosh

    • docker exec: docker 进入一个容器的命令
    • -it:以交互模式进入
    • mongo:这里的 mongo 是容器名字,而非镜像,因为我前面创建容器的时候就指定了这个名字。
    • mongosh:进入容器的时候要执行的命令,而 mongosh, 就是启动一个与mongodb 交互的 shell 窗口给我们使用。

至此安装完成。

在这里插入图片描述

在这里插入图片描述

进来的时候什么也没有。其他的安装方式我不太清楚,通过我这种方式安装的 mongodb,因为没有账号,因此什么也做不了,需要做一下初始化工作。而一旦初始化完成之后,再以无账号的身份进去的话,是没办法创建用户的。

初始化

在admin数据库中通过创建一个用户,赋予用户root权限。

# 进入 admin 数据库
test> use admin;
switched to db admin

# 创建 root 用户
admin> db.createUser(
...     {
...         user:"root",
...         pwd:"123456",
...         roles:[{role:"root",db:"admin"}]
...     }
... );
{ ok: 1 }

# 授权登录
admin> db.auth('root', '123456');
{ ok: 1 }

再次尝试查询 admin 数据库有哪些集合

在这里插入图片描述

关于 mongodb 的用户校验,权限校验什么的,需要专门讲述,后续再讲。

数据库


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

数据库名可以是满足以下条件的任意UTF-8字符串。

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

有一些数据库名是保留的,可以直接访问这些有特殊作用的数据库。

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

mongodb 的数据库无需主动创建,也没有显示的创建命令,比如你使用 use dbname,哪怕是一个不存在的数据库,也会给你已经进去了的错觉,其实什么都没有,数据库也没有创建。

在这里插入图片描述

这里我先进入了 test,数据库,然后查询所有的数据库,当前数据库中没有 test 数据库,然后我进入了 test2 数据库,还是没有 test2 数据库。

可以选择进入任何一个数据库,哪怕是不存在的数据库,只有向数据库中写入数据,才会真正的创建数据库。比如我们用命令显示的创建一个集合(表)

在这里插入图片描述

假如我们删除了这个表,这个数据库就会消失。


数据库删除

  1. 数据库中的数据都没了,数据库也就没了,包括这个数据库的用户权限角色什么的,这些后面再说。
  2. 显示的数据库删除命令。

在这里插入图片描述

集合


集合的命名规范:

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

创建集合

直接显示创建集合:

test2> db.createCollection('category');
{ ok: 1 }
test2> show collections;
category

在这里插入图片描述

隐式创建集合:

当向一个集合中插入一个文档的时候,如果集合不存在,则会自动创建集合。通常我们使用隐式创建即可。

test2> show collections;
category
test2> db.partner.insert({})
DeprecationWarning: Collection.insert() is deprecated. Use insertOne, insertMany, or bulkWrite.
{
  acknowledged: true,
  insertedIds: { '0': ObjectId('658ceddc24b29552c8e7befb') }
}
test2> show collections;
category
partner
test2>

在这里插入图片描述

第一条集合展示命令显示 test2 数据库中只有 category 这一个集合,随后我们使用 db.partner.insert({})命令尝试向 partner 集合中插入一个空文档。这个时候 mongodb 首先会隐式创建一个 partner 的集合,然后向其中写入一条空文档。

注意:mongodb 告诉我们,insert 方法过时了,让我们使用 insertOne 等新的方法。

集合删除

test2> show collections;
category
partner
test2> db.category.drop();
true
test2> db.partner.drop();
true
test2> show collections;

test2>

在这里插入图片描述

文档


文档(document)的数据结构和 JSON 基本一样。所有存储在集合中的数据都是 BSON 格式.

插入

3.2 版本之后新增了 db.collection.insertOne()db.collection.insertMany()

单条插入 支持writeConcern

db.collection.insertOne(
	<document>,
	{writeConcern: <document>}
)

writeConcern 决定一个写操作落到多少个节点上才算成功。writeConcern 的取值包括:

  • 0:发起写操作,不关心是否成功;
  • 1~集群最大数据节点数:写操作需要被复制到指定节点数才算成功;
  • majority:写操作需要被复制到大多数节点上才算成功.

insert: 若插入的数据主键已经存在,则会抛 DuplicateKeyException 异常,提示主键重复,不保存当前数据。

save: 如果 _id 主键存在则更新数据,如果不存在就插入数据。

批量插入

  • insertMany: 向指定集合中插入多条文档数据
db.collection.insertMany(
	[ <document 1> , <document 2>, ... ],
	{
		writeConcern: <document>,
		ordered: <boolean>
	}
)
  • writeConcern:写入策略,默认为 1,即要求确认写操作,0 是不要求。
  • ordered:指定是否按顺序写入,默认 true,按顺序写入。

mongodb 是可以直接执行 js 代码的,这里使用 js 代码批量生成数据。

测试:批量插入50条随机数据

var tags = ["nosql","mongodb","document","developer","popular"];
var types = ["technology","sociality","travel","novel","literature"];
var books=[];
for(var i=0;i<50;i++){
    var typeIdx = Math.floor(Math.random()*types.length);
    var tagIdx = Math.floor(Math.random()*tags.length);
    var favCount = Math.floor(Math.random()*100);
    var book = {
        title: "book-"+i,
        type: types[typeIdx],
        tag: tags[tagIdx],
        favCount: favCount,
        author: "xxx"+i
    };
    books.push(book);
}
db.books.insertMany(books);

提示:

  • 插入时指定了 _id ,则主键就是该值。
  • 如果某条数据插入失败,将会终止插入,但已经插入成功的数据不会回滚掉。
  • 因为批量插入由于数据较多容易出现失败,因此,可以使用try catch进行异常捕捉处理,测试的时候可以不处理。

查询


find 查询集合中的若干文档。语法格式如下:

db.collection.find(query, projection)
  • query :可选,使用查询操作符指定查询条件
  • projection :可选,使用投影操作符指定返回的键。查询时返回文档中所有键值, 只需省略该参数即可(默认省略)。投影时,id为1的时候,其他字段必须是1;id是0的时候,其他字段可以是0;如果没有_id字段约束,多个其他字段必须同为0或同为1。

如果查询返回的条目数量较多,mongo shell则会自动实现分批显示。默认情况下每次只显示20条,可以输入it命令读取下一批。

findOne查询集合中的第一个文档。语法格式如下:

db.collection.findOne(query, projection)

条件查询

指定条件查询

#查询带有nosql标签的book文档:
db.books.find({tag:"nosql"})
#按照id查询单个book文档:
db.books.find({_id:ObjectId("61caa09ee0782536660494d9")})
#查询分类为“travel”、收藏数超过60个的book文档:
db.books.find({type:"travel",favCount:{$gt:60}})

查询条件对照表

SQLMSQL
a = 1{a: 1}
a <> 1{a: {$ne: 1}}
a > 1{a: {$gt: 1}}
a >= 1{a: {$gte: 1}}
a < 1{a: {$lt: 1}}
a <= 1{a: {$lte: 1}}

查询逻辑对照表

SQLMSQL
a = 1 AND b = 1{a: 1, b: 1} 或 {$and : [{a: 1}, {b: 1}]}
a = 1 OR b = 1{$or: [{a: 1}, {b: 1}]}
a IS NULL{a: {$exists: false}}
a IN (1, 2, 3){a: {$in: [1, 2, 3]}}

查询逻辑运算符

  • $lt: 存在并小于
  • $lte: 存在并小于等于
  • $gt: 存在并大于
  • $gte: 存在并大于等于
  • $ne: 不存在或存在但不等于
  • $in: 存在并在指定数组中
  • $nin: 不存在或不在指定数组中
  • $or: 匹配两个或多个条件中的一个
  • $and: 匹配全部条件

排序&分页

指定排序

在 MongoDB 中使用 sort() 方法对数据进行排序

#指定按收藏数(favCount)降序返回
db.books.find({type:"travel"}).sort({favCount:-1})

分页查询

skip用于指定跳过记录数,limit则用于限定返回结果数量。可以在执行find命令的同时指定 skip、limit 参数,以此实现分页的功能。比如,假定每页大小为8条,查询第3页的book文档:

db.books.find().skip(8).limit(4)

正则表达式匹配查询

MongoDB 使用 $regex 操作符来设置匹配字符串的正则表达式

//使用正则表达式查找type包含 so 字符串的book
db.books.find({type: {$regex: "so"}})
//或者
db.books.find({type: /so/})

更新文档

可以用update命令对指定的数据进行更新,命令的格式如下:

db.collection.update(query, update, options);
  • query:描述更新的查询条件;
  • update:描述更新的动作及新的内容;
  • options:描述更新的选项
    • upsert: 可选,如果不存在update的记录,是否插入新的记录。默认false,不插入
    • multi: 可选,是否按条件查询出的多条记录全部更新。 默认false,只更新找到的第一条记录
    • writeConcern :可选,决定一个写操作落到多少个节点上才算成功。

更新操作符

操作符格式描述
$set{$set: {field: value}}指定一个键并更新值,若键不存在则创建
$unset{$unset: {field: 1}}删除一个键
$inc{$inc: {field: value}}对数值类型进行增减
$rename{$rename: {old_field_name: new_field_name}}修改字段名称
$push{ $push: {field: value}}将数值追加到数组中,若数组不存在则会进行初始化
$pushAll{$pushAll: {field: value_array}}追加多个值到一个数组字段内
$pull{$pull: {field: _value}}从数组中删除指定的元素
$addToSet{$addToSet: {field: value}}添加元素到数组中,具有排重功能
$pop{$pop: {field: 1 }}删除数组的第一个或最后一个元素

更新单个文档

某个book文档被收藏了,则需要将该文档的favCount字段自增

db.books.update({_id: ObjectId('658d031c24b29552c8e7befd')}, {$inc :{favCount: 1}});

更新多个文档

默认情况下,update命令只在更新第一个文档之后返回,如果需要更新多个文档,则可以使用multi选项。

将分类为“novel”的文档的增加发布时间(publishedDate)

db.books.update({type: "novel"}, {$set: {publishedDate: new Date()}}, {"multi": true});

multi : 可选,mongodb 默认是false,只更新找到的第一条记录,如果这个参数为true,就把按条件查出来多条记录全部更新

在这里插入图片描述

update命令的选项配置较多,为了简化使用还可以使用一些快捷命令:

  • updateOne:更新单个文档。
  • updateMany:更新多个文档。
  • replaceOne:替换单个文档。

使用upsert命令

upsert是一种特殊的更新,其表现为如果目标文档不存在,则执行插入命令。

db.books.update(
    {title: "my book"},
    {$set: {tags: ["nosql", "mongodb"], type: "none", author: "fox"}},
    {upsert: true}
)

nMatched、nModified都为0,表示没有文档被匹配及更新,nUpserted=1提示执行了upsert动作

在这里插入图片描述

findAndModify命令

findAndModify兼容了查询和修改指定文档的功能,findAndModify只能更新单个文档

//将某个book文档的收藏数(favCount)加1
db.books.findAndModify({
	query: {_id: ObjectId('658d031c24b29552c8e7bf10')},
	update: {$inc: {favCount: 1}}
});

该操作会返回符合查询条件的文档数据,并完成对文档的修改。

在这里插入图片描述

默认情况下,findAndModify会返回修改前的“旧”数据。如果希望返回修改后的数据,则可以指定new选项

//将某个book文档的收藏数(favCount)加1
db.books.findAndModify({
	query: {_id: ObjectId('658d031c24b29552c8e7bf10')},
	update: {$inc: {favCount: 1}},
	new: true
});

与findAndModify语义相近的命令如下:

  • findOneAndUpdate:更新单个文档并返回更新前(或更新后)的文档。
  • findOneAndReplace:替换单个文档并返回替换前(或替换后)的文档。

删除文档

使用 remove 删除文档

  • remove 命令需要配合查询条件使用;
  • 匹配查询条件的文档会被删除;
  • 指定一个空文档条件会删除所有文档;

示例:

db.books.remove({_id: ObjectId('658d031c24b29552c8e7bf10')});  // 删除特定id 的数据
db.books.remove({favCount: {$lt: 20}});  // 删除 favCount 小于 25 的所有数据
db.books.remove({});  //删除整个集合中的所有的文档
db.books.remove();  // 报错

remove命令会删除匹配条件的全部文档,如果希望明确限定只删除一个文档,则需要指定justOne参数,命令格式如下:

db.collection.remove(query, justOne)

例如:删除满足type:novel条件的首条记录

db.books.remove({type: "novel"}, true)

使用 delete 删除文档

官方推荐使用 deleteOne() 和 deleteMany() 方法删除文档,语法格式如下:

db.books.deleteMany({}) //删除集合下全部文档
db.books.deleteMany({type: "novel"}) //删除 type等于 novel 的全部文档
db.books.deleteOne({type: "novel"}) //删除 type等于novel 的一个文档

注意: remove、deleteMany等命令需要对查询范围内的文档逐个删除,如果希望删除整个集合,则使用drop命令会更加高效

返回被删除文档

remove、deleteOne等命令在删除文档后只会返回确认性的信息,如果希望获得被删除的文档,则可以使用findOneAndDelete命令

db.books.findOneAndDelete({type:"novel"})

除了在结果中返回删除文档,findOneAndDelete命令还允许定义“删除的顺序”,即按照指定顺序删除找到的第一个文档

db.books.findOneAndDelete({type:"novel"},{sort:{favCount:1}})

remove、deleteOne等命令只能按默认顺序删除,利用这个特性,findOneAndDelete可以实现队列的先进先出。

聚合操作


聚合操作处理数据记录并返回计算结果(诸如统计平均值,求和等)。聚合操作组值来自多个文档,可以对分组数据执行各种操作以返回单个结果。聚合操作包含三类:单一作用聚合、聚合管道、MapReduce。

  • 单一作用聚合:提供了对常见聚合过程的简单访问,操作都从单个集合聚合文档。
  • 聚合管道是一个数据聚合的框架,模型基于数据处理流水线的概念。文档进入多级管道,将文档转换为聚合结果。
  • MapReduce操作具有两个阶段:处理每个文档并向每个输入文档发射一个或多个对象的map阶段,以及reduce组合map操作的输出阶段。

单一作用聚合

MongoDB提供 db.collection.estimatedDocumentCount(), db.collection.count(), db.collection.distinct() 这类单一作用的聚合函数。 所有这些操作都聚合来自单个集合的文档。虽然这
些操作提供了对公共聚合过程的简单访问,但它们缺乏聚合管道和map-Reduce的灵活性和功能。

函数描述
db.collection.estimatedDocumentCount()忽略查询条件,返回集合或视图中所有文档的计数
db.collection.count()返回与find()集合或视图的查询匹配的文档计数 。等同于 db.collection.find(query).count()构造
db.collection.distinct()在单个集合或视图中查找指定字段的不同值,并在数组中返回结果。
#检索books集合中所有文档的计数
db.books.estimatedDocumentCount()
#计算与查询匹配的所有文档
db.books.count({favCount: {$gt: 50}})
#返回不同type的数组
db.books.distinct("type")
#返回收藏数大于90的文档不同type的数组
db.books.distinct("type", {favCount: {$gt: 90}})

注意:在分片群集上,如果存在孤立文档或正在进行块迁移,则db.collection.count()没有查询谓词可能导致计数不准确。要避免这些情况,请在分片群集上使用 db.collection.aggregate()方法。

聚合管道

什么是 MongoDB 聚合框架

MongoDB 聚合框架(Aggregation Framework)是一个计算框架,它可以:

  • 作用在一个或几个集合上;
  • 对集合中的数据进行的一系列运算;
  • 将这些数据转化为期望的形式;

从效果而言,聚合框架相当于 SQL 查询中的GROUP BY、 LEFT OUTER JOIN 、 AS等。

管道(Pipeline)和阶段(Stage)

整个聚合运算过程称为管道(Pipeline),它是由多个阶段(Stage)组成的, 每个管道:

  • 接受一系列文档(原始数据);
  • 每个阶段对这些文档进行一系列运算;
  • 结果文档输出给下一个阶段;

在这里插入图片描述

聚合管道操作语法

pipeline = [$stage1, $stage2, ...$stageN];
db.collection.aggregate(pipeline, {options})
  • pipelines 一组数据聚合阶段。除 $out$Merge$geonear 阶段之外,每个阶段都可以在管道中出现多次。
  • options 可选,聚合操作的其他参数。包含:查询计划、是否使用临时文件、 游标、最大操作时间、读写策略、强制索引等等

在这里插入图片描述

常用的管道聚合阶段

聚合管道包含非常丰富的聚合阶段,下面是最常用的聚合阶段

阶段描述SQL等价运算符
$match筛选条件WHERE
$project投影AS
$lookup左外连接LEFT OUTER JOIN
$sort排序ORDER BY
$group分组GROUP BY
$skip/$limit分页
$unwind展开数组
$graphLookup图搜索
$facet/$bucket分面搜索

数据准备

准备数据集,执行脚本

var tags = ["nosql", "mongodb", "document", "developer", "popular"];
var types = ["technology", "sociality", "travel", "novel", "literature"];
var books = [];

for(var i=0;i<50;i++){
	var typeIdx = Math.floor(Math.random() * types.length);
	var tagIdx = Math.floor(Math.random() * tags.length);
	var tagIdx2 = Math.floor(Math.random() * tags.length);
	var favCount = Math.floor(Math.random() * 100);
	var username = "xx00" + Math.floor(Math.random() * 10);
	var age = 20 + Math.floor(Math.random() * 15);
	var book = {
		title: "book-"+i,
		type: types[typeIdx],
		tag: [tags[tagIdx],tags[tagIdx2]],
		favCount: favCount,
		author: {name: username, age: age}
	};
	books.push(book)
}
db.books.insertMany(books);

$project

投影操作, 将原始字段投影成指定名称, 如将集合中的 title 投影成 name

db.books.aggregate([{$project: {name: "$title"}}]);

$project 可以灵活控制输出文档的格式,也可以剔除不需要的字段

db.books.aggregate([{$project: {name: "$title", _id: 0, type: 1, auth: 1}}]);

从嵌套文档中排除字段

db.books.aggregate([
    {$project: {name: "$title", _id: 0, type: 1, "author.name": 1}}
])
或者
db.books.aggregate([
    {$project: {name: "$title", _id: 0, type: 1, author: {name: 1}}}
])

$match

$match用于对文档进行筛选,之后可以在得到的文档子集上做聚合,$match可以使用除了地理空间之外的所有常规查询操作符,在实际应用中尽可能将$match放在管道的前面位置。这样有两个好处:一是可以快速将不需要的文档过滤掉,以减少管道的工作量;二是如果再投射和分组之前执行$match,查询可以使用索引。

db.books.aggregate([{$match: {type: "technology"}}])

筛选管道操作和其他管道操作配合时候时,尽量放到开始阶段,这样可以减少后续管道操作符要操作的文档数,提升效率.

$count

计数并返回与查询匹配的结果数

db.books.aggregate([{$match: {type: "technology"}}, {$count: "type_count"}]);

$match阶段筛选出type匹配technology的文档,并传到下一阶段;

$count阶段返回聚合管道中剩余文档的计数,并将该值分配给type_count

$group

按指定的表达式对文档进行分组,并将每个不同分组的文档输出到下一个阶段。输出文档包含一个_id字段,该字段按键包含不同的组。

输出文档还可以包含计算字段,该字段保存由$group_id字段分组的一些accumulator表达式的值。$group不会输出具体的文档而只是统计信息。

{$group: {_id: <expression>, <field1>: {<accumulator1>: <expression1>}, ...
}}
  • _id字段是必填的;但是,可以指定id值为null来为整个输入文档计算累计值。
  • 剩余的计算字段是可选的,并使用运算符进行计算。
  • _id和表达式可以接受任何有效的表达式。

accumulator操作符

名称描述类比sql
$avg计算均值avg
$first返回每组第一个文档,如果有排序,按照排序,如果没有按照默认的存储的顺序的第一个文档。limit 0,1
$last返回每组最后一个文档,如果有排序,按照排序,如果没有按照默认的存储的顺序的最后个文档。-
$max根据分组,获取集合中所有文档对应值得最大值。max
$min根据分组,获取集合中所有文档对应值得最小值。min
$push将指定的表达式的值添加到一个数组中。-
$addToSet将表达式的值添加到一个集合中(无重复值,无序)。-
$sum计算总和sum
$stdDevPop返回输入值的总体标准偏差(population standard deviation)-
$stdDevSamp返回输入值的样本标准偏差(the sample standard deviation)-

$group阶段的内存限制为100M。默认情况下,如果stage超过此限制,$group将产生错误。但是,要允许处理大型数据集,请将allowDiskUse选项设置为true以启用$group操作以写入临时文件。

book的数量,收藏总数和平均值

db.books.aggregate([
	{$group: {_id: null, count: {$sum: 1}, pop: {$sum: "$favCount"}, avg: {$avg: "$favCount"}}}
]);

注意:这里的 _id 不是每个文档的 _id,而是指定分组字段。

上述语义:没有分组字段,也就是说,数据组为全体数据。统计数据条数,输出字段为 count,统计收藏总和数量,输出字段为 pop,统计每本书的平均收藏数,输出字段为 avg。

统计每个作者的book收藏总数

db.books.aggregate([
	{$group: {_id: "$author.name", pop: {$sum: "$favCount"}}}
]);

这里 _id 指定了分组字段为作者姓名,统计每个姓名下的收藏总数。

统计每个作者的每本book的收藏数

db.books.aggregate([
	{$group: {_id: {name: "$author.name", title: "$title"}, pop: {$sum: "$favCount"}}}
])

这里对数据集合按照作者+书本进行分组,统计每个组的收藏数

每个作者的book的type合集

db.books.aggregate([
	{$group: {_id: "$author.name", types: {$addToSet: "$type"}}}
])

这里按照作者姓名进行分组,收集每个作者的书本分类[不重复]

$unwind

可以将数组拆分为单独的文档

v3.2+支持如下语法:

{
	$unwind: {
		#要指定字段路径,在字段名称前加上$符并用引号括起来。
		path: <field path>,
		#可选,一个新字段的名称用于存放元素的数组索引。该名称不能以$开头。
		includeArrayIndex: <string>,
		#可选,default :false,若为true,如果路径为空,缺少或为空数组,则$unwind输出文档
		preserveNullAndEmptyArrays: <boolean>
	}
}

姓名为xx006的作者的book的tag数组拆分为多个文档

db.books.aggregate([
	{$match:{"author.name":"xx006"}},
	{$unwind:"$tag"}
])

每个作者的book的tag合集

db.books.aggregate([
	{$unwind: "$tag"},
	{$group: {_id: "$author.name", types: {$addToSet: "$tag"}}}
])

案例

示例数据

db.books.insertMany([
    {
        "title" : "book-51",
        "type" : "technology",
        "favCount" : 11,
        "tag":[],
        "author" : {
            "name" : "fox",
            "age" : 28
        }
    },{
        "title" : "book-52",
        "type" : "technology",
        "favCount" : 15,
        "author" : {
            "name" : "fox",
            "age" : 28
    }
    },{
        "title" : "book-53",
        "type" : "technology",
        "tag" : [
            "nosql",
            "document"
         ],
        "favCount" : 20,
        "author" : {
            "name" : "fox",
            "age" : 28
         }
    }
])

测试

# 使用includeArrayIndex选项来输出数组元素的数组索引
db.books.aggregate([
	{$match:{"author.name":"fox"}},
	{$unwind:{path:"$tag", includeArrayIndex: "arrayIndex"}}
])

# 使用preserveNullAndEmptyArrays选项在输出中包含缺少size字段,null或空数组的文档
db.books.aggregate([
{$match:{"author.name":"fox"}},
{$unwind:{path:"$tag", preserveNullAndEmptyArrays: true}}
])

$limit

限制传递到管道中下一阶段的文档数

db.books.aggregate([
	{$limit: 5 }
])

此操作仅返回管道传递给它的前5个文档。 $limit对其传递的文档内容没有影响。

注意:当$sort在管道中的$limit之前立即出现时,$sort操作只会在过程中维持前n个结果,其中n是指定的限制,而MongoDB只需要将n个项存储在内存中。

$skip

跳过进入stage的指定数量的文档,并将其余文档传递到管道中的下一个阶段

db.books.aggregate([
	{$skip: 5 }
])

此操作将跳过管道传递给它的前5个文档。 $skip对沿着管道传递的文档的内容没有影响。

$sort

对所有输入文档进行排序,并按排序顺序将它们返回到管道。

语法:

{ $sort: { <field1>: <sort order>, <field2>: <sort order> ... } }

要对字段进行排序,请将排序顺序设置为1或-1,以分别指定升序或降序排序,如下例所示:

db.books.aggregate([
	{$sort: {favCount: -1, title: 1}}
])

$lookup

Mongodb 3.2版本新增,主要用来实现多表关联查询, 相当关系型数据库中多表关联查询。每个输入待处理的文档,经过$lookup 阶段的处理,输出的新文档中会包含一个新生成的数组(可根据需要命名新key )。数组列存放的数据是来自被Join集合的适配文档,如果没有,集合为空(即 为[ ])

语法:

db.collection.aggregate([{
	$lookup: {
		from: "<collection to join>",
		localField: "<field from the input documents>",
		foreignField: "<field from the documents of the from collection>",
		as: "<output array field>"
	}
})
属性作用
from同一个数据库下等待被Join的集合。
localField源集合中的match值,如果输入的集合中,某文档没有 localField这个Key(Field),在处理的过程中,会默认为此文档含有 localField:null的键值对。
foreignField待Join的集合的match值,如果待Join的集合中,文档没有foreignField值,在处理的过程中,会默认为此文档含有 foreignField:null的键值对。
as为输出文档的新增值命名。如果输入的集合中已存在该值,则会覆盖掉

注意:null = null 此为真

其语法功能类似于下面的伪SQL语句:

SELECT *, <output array field>
FROM collection
WHERE <output array field> IN (
	SELECT *
	FROM <collection to join>
	WHERE <foreignField>= <collection.localField>
);

案例

数据准备

db.customer.insert({customerCode:1,name:"customer1",phone:"13112345678",address:"test1"})
db.customer.insert({customerCode:2,name:"customer2",phone:"13112345679",address:"test2"})
db.order.insert({orderId:1,orderCode:"order001",customerCode:1,price:200})
db.order.insert({orderId:2,orderCode:"order002",customerCode:2,price:400})
db.orderItem.insert({itemId:1,productName:"apples",qutity:2,orderId:1})
db.orderItem.insert({itemId:2,productName:"oranges",qutity:2,orderId:1})
db.orderItem.insert({itemId:3,productName:"mangoes",qutity:2,orderId:1})
db.orderItem.insert({itemId:4,productName:"apples",qutity:2,orderId:2})
db.orderItem.insert({itemId:5,productName:"oranges",qutity:2,orderId:2})
db.orderItem.insert({itemId:6,productName:"mangoes",qutity:2,orderId:2})

关联查询

db.customer.aggregate([{
	$lookup: {
		from: "order",
		localField: "customerCode",
		foreignField: "customerCode",
		as: "customerOrder"
	}
}])

db.order.aggregate([
	{
		$lookup: {
			from: "customer",
			localField: "customerCode",
			foreignField: "customerCode",
			as: "curstomer"
		}
	}, {
		$lookup: {
			from: "orderItem",
			localField: "orderId",
			foreignField: "orderId",
			as: "orderItem"
		}
	}
])
;