文章目录
MongoDB相关概念
业务应用场景
传统的关系型数据库(如MySQL),在数据操作的“三高”需求以及应对Web2.0的网站需求面前,显得力不从心。
解释:“三高”需求:
- High performance - 对数据库高并发读写的需求。
- Huge Storage - 对海量数据的高效率存储和访问的需求。
- High Scalability && High Availability- 对数据库的高可扩展性和高可用性的需求。
而MongoDB可应对“三高”需求。
具体的应用场景如:
- 社交场景,使用 MongoDB 存储存储用户信息,以及用户发表的朋友圈信息,通过地理位置索引实现附近的人、地点等功能。
- 游戏场景,使用 MongoDB 存储游戏用户信息,用户的装备、积分等直接以内嵌文档的形式存储,方便查询、高效率存储和访问。
- 物流场景,使用 MongoDB 存储订单信息,订单状态在运送过程中会不断更新,以 MongoDB 内嵌数组的形式来存储,一次查询就能将订单所有的变更读取出来。
- 物联网场景,使用 MongoDB 存储所有接入的智能设备信息,以及设备汇报的日志信息,并对这些信息进行多维度的分析。
- 视频直播,使用 MongoDB 存储用户信息、点赞互动信息等。
这些应用场景中,数据操作方面的共同特点是:
- 数据量大
- 写入操作频繁(读写都很频繁)
- 价值较低的数据,对事务性要求不高
对于这样的数据,我们更适合使用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术语/概念 | 解释/说明 |
---|---|---|
database | database | 数据库 |
table | collection | 数据库表/集合 |
row | document | 数据记录行/文档 |
column | field | 数据字段/域 |
index | index | 索引 |
table joins | 表连接,MongoDB不支持 | |
嵌入文档 | MongoDB通过嵌入式文档来替代多表连接 | |
primary key | primary 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,按照下列步骤操作
-
拉取最新镜像:
docker pull mongo
-
启动 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 默认是不需要账号密码校验的,这里开启校验
-
通过 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 数据库。
可以选择进入任何一个数据库,哪怕是不存在的数据库,只有向数据库中写入数据,才会真正的创建数据库。比如我们用命令显示的创建一个集合(表)
假如我们删除了这个表,这个数据库就会消失。
数据库删除
- 数据库中的数据都没了,数据库也就没了,包括这个数据库的用户权限角色什么的,这些后面再说。
- 显示的数据库删除命令。
集合
集合的命名规范:
- 集合名不能是空字符串""。
- 集合名不能含有\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}})
查询条件对照表
SQL | MSQL |
---|---|
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}} |
查询逻辑对照表
SQL | MSQL |
---|---|
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"
}
}
])