文档数据库MongoDB的基本使用姿势教程,手把手教你学会MongoDB的CURD
MongoDB系列教程
- 1: 零:环境安装与初始化
- 2: 一:基本概念
- 3: 二:连接
- 4: 三:基本工具介绍
- 5: 四:数据库 Database
- 6: 五:集合 Collection
- 7: 六:文档 Document 插入姿势
- 8: 七:文档 Document 删除姿势
- 9: 八:文档 Document 更新姿势
- 10: 九:文档 Document 查询基础篇
- 11: 十:文档 Document 查询高级篇
- 12: 十一:文档 Document 查询非典型篇
- 13: 十二:文档更新删除之非典型篇
- 14: 十三:索引
1 - 零:环境安装与初始化
MongoDB 是一个基于分布式文件存储的数据库。由 C++ 语言编写。旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。
MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。
本篇为mongodb系列教程第一篇,环境安装与连接设置
1. docker安装
首先介绍最简单的安装方式,docker安装,请先保证docker环境存在(没有安装的推荐查看: Centos安装docker与使用说明)
安装命令如下:
# 下载镜像
docker pull mongo
# 加载并运行镜像
docker run --name mongo -p 27017:27017 -d mongo --auth
# 进入容器
docker exec -it mongo /bin/bash
2. centos安装
直接借助yum进行安装,命令如下
# 查看支持的mongo库
yum list | grep mongo
yum install -y mongodb.x86_64 mongodb-server.x86_64
3. 用户配置
直接通过mongodb提供的终端命令进行设置,
# 为mongo创建登录用户和密码
mongo
use admin
db.createUser({user:"root",pwd:"root",roles:[{role:'root',db:'admin'}]})
exit
4. 终端控制台
mongodb集成了终端控制台,通过mongo
进入;
但是当我们设置了登录认证时,有下面两种使用姿势
case1
# 直接指定用户名密码,注意--authenticationDatabase admin 必须得有
mongo -u root -p root --authenticationDatabase admin
case2
mongo
# 下一行不可少
use admin
db.auth('root', 'root')
5. 可视化操作工具
终端虽好,使用起来终究不太顺手,可视化工具推荐使用ROBO 3T
操作mongodb,官网下载地址: https://robomongo.org/
然后配置mongodb连接信息(支持ssh验证方式哦),下面是一个简单的配置
然后就可以通过它来操作mongodb了
2 - 一:基本概念
mongodb和我们通常使用的关系型数据库如mysql,在一些基本概念上有相同之处,但也有一些区别,在进行mongodb的语言介绍之前,有必要先了解一些基础概念
本文将对比sql对一些基础概念进行解释说明
I. 基本概念
在sql中,会区分database, table, row, column, index, primaryId;在mongodb中也有对应的概念
sql | mongodb | 说明 |
---|---|---|
database | db | 数据库 |
table | collection | 表/集合 |
row | document | 行/文档 |
column | field | 字段 |
index | index | 索引 |
primaryId | _id | 主键 |
lock | lock | 锁 |
下面对以上基本概念进行简单说明,详情的后续博文会补上
1. 数据库
数据库可以理解为collection的聚集体,每个mongodb实例可以有多个database,每个database可以有多个collection
常见的几个命令如下:
# 显示所有db
show dbs
# 选中某个db
use db_name
# 显示当前选中的db
db
# 删除
db.dropDatabase()
2. 集合
document的集合,与table最大的区别是它的结构不是固定的,不需要事先定义字段、类型
首次新增document时,集合被创建;
3. document
文档,也就是具体的数据;bson结构,kv方式
最大的特点是不要求所有的document的结构一致,相同的field的数据类型可以不一致
4. index
索引,同样是用来提高查询效率,避免全盘扫描
5. lock
支持读写锁,document加读锁时,其他读操作ok,写操作禁止;加写锁时,其他读写操作禁止
6. 事务
版本>= 4.0
,支持事务,支持多文档ACID,后续详细说明
3 - 二:连接
后续的所有文章的基础,都是需要先连上mongodb,然后才能执行各种命令操作;
本文将介绍一下如何连接一个已经启动的mongodb服务器
1. 连接语法
标准URI连接语法:
mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]
mongodb://
固定前缀username:password@
: 如果开启了用户登录验证,需要指定用户名密码host1:port1
: mongodb服务器的ip/域名 + 端口(不填时,默认为27017)database
: 如果指定username:password@
,连接并验证登陆指定数据库。若不指定,默认打开 test 数据库?options
: 是连接选项。如果不使用/database,则前面需要加上
2. 实例
直接连接方式如下,注意这种方式会保留用户名和密码,会有一定的安全风险
连接目标服务器
# 连接本地mongodb
mongo mongodb://root:root@127.0.0.1:27017/admin
连接多台服务器
mongo mongodb://root:root@127.0.0.1:27017,127.0.0.1:27018/admin
连接 replica set 三台服务器, 写入操作应用在主服务器 并且分布查询到从服务器
mongo mongodb://host1,host2,host3/?slaveOk=true
4 - 三:基本工具介绍
mongodb服务器安装完毕之后,提供了一些配套的操作工具,接下来我们有必要认识一下它们,并了解基本用法
0. mongod
启动mongodb实例的主要命令,常见的使用姿势如下
mongod --dbpath=/data/mongodb/data --logpath=/data/mongodb/logs --logappend --auth --port=27017 --fork
1. mongo 命令行使用
mongodb安装完毕之后,会自带一个终端命令行工具,通过它可以连接mongodb,并执行相关命令
a. 连接
介绍三种连接mongodb的姿势
case1
mongo --host 目标主机 --port 端口号 -u 用户名 -p 密码 --authenticationDatabase admin
case2
mongo mongodb://root:root@127.0.0.1:27017/admin
case3
上面两种姿势虽然简单,但是用户名密码有暴露的风险,推荐使用下面这种方式
mongo --host 目标主机 --port 端口号
use admin
db.auth('用户名', '密码')
b. 操作
连接上mongodb服务器之后,就可以执行mongo命令,查看数据库,管理文档,比如下面给几个常见的操作
# 查看所有database
show dbs
# 选择数据库(不存在时,创建)
use basic
# 显示所有集合
show collections
# 查看文档
db.demo.findOne({})
2. mongoimport/mongoexport
用于导入导出数据,如
将库database
中的集合collection
导出到json文件out.json
bin/mongoexport -h localhost:27107 -u user -p pwd -d database -c collection -o out.json
从json文件导入到目标集合new_collection
bin/mongoimport -h localhost:27107 -u user -p pwd -d database -c new_collection ./out.json
3. mongodump/mongorestore
使用mongodump命令来备份MongoDB数据, 将数据库basic
的所有集合备份到目录 /tmp/outDir
下
mongodump -d basic -u root -p root --authenticationDatabase admin -o /tmp/outDir
使用mongorestore恢复,如下
# --drop 表示先删除当前数据,然后再恢复,可以不指定
mongorestore -u root -p root --authenticationDatabase admin --drop /tmp/outDir
4. mongostate
mongostat是mongodb自带的状态检测工具,在命令行下使用。它会间隔固定时间获取mongodb的当前运行状态,并输出。如果你发现数据库突然变慢或者有其他问题的话,你第一手的操作就考虑采用mongostat来查看mongo的状态。
mongostat -u root -p root --authenticationDatabase admin
5. mongotop
mongotop提供每个集合的水平的统计数据,默认每s输出一次
5 - 四:数据库 Database
我们通常把mongodb叫文档型数据库,mysql叫关系型数据库,influxdb叫时序数据库,如果熟悉这三个的话,会发现他们都有一个database
,它是collection/table/measurement
的上一级,可以简单的把它理解为更高层级的集合,方便统一管理/权限划分/业务拆分
下面简单介绍一下database的基础操作
1. 创建数据库
当数据库不存在时,通过use + 数据库
命令可以用来创建数据库;当数据库存在时,表示选中
use dbname
2. 查看数据库
通过 db
查看当前的数据库
通过 show dbs
查看当前的数据库列表
请注意,新创建一个数据库时,直接使用show dbs
命令,并不会显示出来,如下
为了显示这个数据库,需要插入一个文档
db.dbname.insert({"name": "一灰灰blog", "age": 18})
3. 删除数据库
对于数据库而言,任何删除命令都需要慎重处理,一不小心就得跑路了。。。
命令如下: db.dropDatabase()
实例说明:
一般来说我们需要删除时,两步走
# 选中db
use dbname
# 执行删除命令
db.dropDatabase()
4. 潜规则
需要注意,有三个数据库属于预留的,有特殊的作用,不能新建同名的数据
- admin: 将一个用户添加到这个数据库,这个用户自动继承所有数据库的权限; 一些特定的服务器端命令也只能从这个数据库运行,比如列出所有的数据库或者关闭服务器。
- local: 这个数据永远不会被复制,可以用来存储限于本地单台服务器的任意集合
- config: 当Mongo用于分片设置时,config数据库在内部使用,用于保存分片的相关信息。
命名规则:
- 不能是空字符串
- 不能含有特殊字符(如
.
,$
,\
,/
,\0
- 小写
- 最多64字节
6 - 五:集合 Collection
集合,相当于关系型数据库中的table,在mongodb中,集合的概念非常贴切,属于文档(Document)的集合
其最大的特点是:
- 没有固定的结构
1. 创建集合
创建命令如: db.createCollection(name, options)
重点看一下参数options
的可选项
- capped: true,表示创建固定大小的集合,需要指定size;超过数量之后,覆盖最早的文档
- size: 固定集合时配套使用,KB为单位
- autoIndexId: 自动为
_id
添加索引,默认true - max: 固定集合时,文档的最大数量
一个简单的实例
# 创建一个名为 to.insert 的集合
db.createCollection('to.insert')
除此之外,新插入一个文档时,集合若不存在,也会创建对应的集合,如
# 不推荐在集合名中包含点号,如果没有点号时,可以通过 db.test_collection.insert({'a': 1})来插入数据,更简单
db.getCollection('to.insert2').insert({'a': 123, 'b': 456})
2. 查看集合
通过 show collections
查看数据库下的集合列表
3. 删除集合
通过命令 db.col.drop()
来删除
4. 命名规则
- 不能全是空白字符
- 不应包含特殊字符
- 不要以
system.
开头
7 - 六:文档 Document 插入姿势
文档相当于关系数据库中数据行,也是我们最关心的数据本身;以BSON格式存储(和json区别不大)
我们通常所说业务开发者的CURD四大技能,在mongodb中,就是针对Document而言,接下来我们先看一下文档的新增使用姿势
1. 基本语法
插入语法: db.collection.insert()
因为集合不要求定义数据结构,所以插入的文档格式理论上可以完全不一样,可以拥有完全不同的数据结构,相同的字段拥有不同的数据类型
2. 实例演示
下面给出几个实例进行说明
基本数据类型插入
# 插入两个数据,注意age的数据类型不一样哦
db.doc_demo.insert({'name': 'yihui', 'age': 18})
db.doc_demo.insert({'address': 'China', 'age': 18.8})
数组类型插入
db.doc_demo.insert({'name': 'yihui', 'skill': ['java', 'python', 'php', 'js']})
Object类型插入
db.doc_demo.insert({'name': 'yihui', 'site': {'blog':'https://blog.hhui.top', 'spring': 'https://spring.hhui.top'}})
3. 数据类型
mongodb支持的基本数据类型,除了我们常见的string,int,float,boolean之外,还有一些其他的;
数据类型 | 说明 |
---|---|
String | 字符串, UTF8编码 |
Integer | 整型,32/64位 |
Boolean | 布尔 |
Double | 浮点 |
Min/Max keys | 将一个值与 BSON(二进制的 JSON)元素的最低值和最高值相对比 |
Array | 数组 |
Timestamp | 时间戳,记录文档修改或添加的具体时间 |
Object | 内嵌文档 |
Null | 创建空值 |
Symbol | 符号。该数据类型基本上等同于字符串类型,但不同的是,它一般用于采用特殊符号类型的语言。 |
Date | 日期,用 UNIX 时间格式来存储当前日期或时间。你可以指定自己的日期时间:创建 Date 对象,传入年月日信息。 |
ObjectID | 对象ID |
Binary Data | 二进制 |
code | 代码类型。用于在文档中存储 JavaScript 代码。 |
Regular expression | 正则表达式类型。用于存储正则表达式。 |
ObjectId 类似唯一主键,可以很快的去生成和排序,包含 12 bytes,含义是:
- 前 4 个字节表示创建 unix 时间戳,格林尼治时间 UTC 时间,比北京时间晚了 8 个小时
- 接下来的 3 个字节是机器标识码
- 紧接的两个字节由进程 id 组成 PID
- 最后三个字节是随机数
8 - 七:文档 Document 删除姿势
前面一篇介绍了插入文档的使用姿势,这一篇则主要介绍删除的使用case
1. 基本语法
db.collection.remove(
<query>,
{
justOne: <boolean>,
writeConcern: <document>
}
)
第一个为需要删除的匹配条件;第二个表示是否只删除一个,默认是false,删除所有满足条件的文档
注意
- 当query为空时,表示删除所有文档,高危操作,谨慎执行
2. 实例演示
借用给我们上一篇插入的文档来进行演示,当前存在的文档为
> db.doc_demo.find({})
{ "_id" : ObjectId("5e786582b0d677183afba746"), "name" : "yihui", "age" : 18 }
{ "_id" : ObjectId("5e78659ab0d677183afba747"), "address" : "China", "age" : 18.8 }
{ "_id" : ObjectId("5e786622b0d677183afba748"), "name" : "yihui", "skill" : [ "java", "python", "php", "js" ] }
{ "_id" : ObjectId("5e786680b0d677183afba749"), "name" : "yihui", "site" : { "blog" : "https://blog.hhui.top", "spring" : "https://spring.hhui.top" } }
根据id进行删除
db.doc_demo.remove({"_id": ObjectId("5e786582b0d677183afba746")})
根据name删除第一个满足条件的记录
db.doc_demo.remove({"name":"yihui"}, {justOne: true})
再次查看剩下的内容如下:
9 - 八:文档 Document 更新姿势
本篇介绍update/save两种方法提供的更新姿势
1. update
用于更新已经存在的文档,语法如下
db.collection.update(
<query>,
<update>,
{
upsert: <boolean>,
multi: <boolean>,
writeConcern: <document>
}
)
- query: 查询条件
- update: 更新语句
- upsert: (可选)true, 不存在update的记录时插入;默认是false,不插入
- multi: (可选) true,表示更新所有满足条件的记录;默认false,只更新第一条
- writeConcern: (可选),抛出异常的级别
插入两条用于测试的数据
db.doc_demo.insert({'name': '一灰灰', 'age': 19, 'skill': ['java', 'python', 'sql']})
db.doc_demo.insert({'name': '一灰灰blog', 'age': 20, 'skill': ['web', 'shell', 'js']})
下面给出几个更新的实例
更新age
# 将name为"一灰灰"的文档age + 1
db.doc_demo.update({'name':'一灰灰'}, {$inc: {'age': 1}})
# 修改name
db.doc_demo.update({'name':'一灰灰'}, {$set: {'name': '一灰灰Blog'}})
更新所有age为20的文档,新增一个tag成员
db.doc_demo.update({'age': 20}, {$set: {'tag': 1}}, {multi:true})
更新一个不存在的文档
db.doc_demo.update({'name': '一灰灰'}, {$set: {'age': 18, 'sex': 'man'}}, {upsert: true})
2. save
save最大的特点是覆盖,用新的文档完全覆盖旧的文档;而update,则是更新指定的field
语法如下:
db.collection.save(
<document>,
{
writeConcern: <document>
}
)
举例如下
db.doc_demo.save({'name': '一灰灰', 'age': 22, 'hobby': ['reading', 'walking']})
那么问题来了,怎样判定是新增一条记录,还是覆盖已经存在的记录呢?
- 有唯一键来判定
- 即:如果save的文档中,某个field有唯一性要求,那么当数据库中存在这个field文档文档时,执行覆盖操作;否则执行插入
举例如下, 指定ObjectId
db.doc_demo.save({ "_id" : ObjectId("5e7b5c2e0172dc950171c48a"), "name" : "一灰灰New", "age" : 18, "hobby" : [ "play game" ] })
10 - 九:文档 Document 查询基础篇
MongoDb文档查询,主要借助find方法来完成,在实际的业务开发中,为了满足各种复杂的业务场景,查询的姿势也是各种各样,本篇则主要介绍基本的使用姿势,不涉及到聚合、排序、分页相关内容
1. 查询语法
查询语法定义比较简单,复杂的是查询条件的组合;语法定义如下
db.collection.find(query, projection)
- query: 查询条件,如果不填,则表示查询所有文档
- projection: 查询需要返回的field,如果不填则返回所有的数据
此外为了mongo-cli的返回结果更加友好,可以在最后添加.pretty()
,使输出更友好
2. 查询所有
db.doc_demo.find()
3. 根据条件精准查询
db.doc_demo.find({'name': '一灰灰'})
4. 数字比较查询
对于数字类型的field,可以借助符号$gt
(>), $get
(>=), $lt
(<), $lte
(<=), $ne
(!=) 来表示具体的操作
#查询age>18的文档
db.doc_demo.find({'age': {$gt: 18}})
# 查询age<20的文档
db.doc_demo.find({'age': {$lt: 20}})
5. 模糊查询
在mysql中有一个like用于模糊查询,在mongodb中,同样支持基于正则的模糊查询
# 查询name以灰灰结尾的文档
db.doc_demo.find({'name': /灰灰$/})
# 查询name中包含 lo 字符的文档
db.doc_demo.find({'name': /lo/})
# 查询name中包含l, g字符的文档
db.doc_demo.find({'name': /l.g/})
# 查询name以一灰灰开头的文档
db.doc_demo.find({'name': /^一灰灰/})
6. and条件
多个查询条件需要满足时,并不需要什么特殊的操作,只需要在查询bson中,加上多个条件即可
# 查询age > 18, 且name为 一灰灰blog的文档
db.doc_demo.find({'age': {$gt: 18}, 'name':'一灰灰blog'})
7. or条件
和and不需要额外的操作不同,or条件需要借助 $or
来实现,语法如下
db.collection.find({$or: [{queyr1, query2}]})
实例如下:
# 查询age > 18, 且name为 一灰灰blog的文档 或 age < 20 且name为一灰灰的文档
db.doc_demo.find({$or: [{'age': {$gt: 18}, 'name':'一灰灰blog'}, {'age': {$lt: 20}, 'name': '一灰灰'}]})
8. 限制返回成员
有些时候我们只需要获取文档中的部分成员,可以在第二个参数中进行指定,规则如下
成员名: 1
: 表示这个成员需要返回成员名: 0
: 表示这个成员不返回
# 表示返回的结果中,除了_id之外,其他的正常返回
db.doc_demo.find({}, {'_id': 0})
# 表示返回的结果中,除了_id之外,就只要name和age
db.doc_demo.find({}, {'name': 1, 'age': 1})
请注意,一般在使用了 成员名: 1
来指定返回field时,会自动返回_id
,如果不需要,请显示加上 _id: 0
9. field类型查询
根据field的成员类型来作为查询条件,一般有两种方式,这里只介绍更优雅的,语法如下
{field: {$type: '类型'}}
举例说明
db.doc_demo.find({'skill': {$type: 'array'}})
10. 存在查询
mongodb的一个特点就是集合的结构不固定,所以某个成员可能存在也可能不存在,所以当我们的查询条件中需要加一个是否存在的判断时,可以如下
# 查询tag存在的文档
db.doc_demo.find({'tag': {$exists:true}})
# 查询tag不存在的文档
db.doc_demo.find({'tag': null})
11 - 十:文档 Document 查询高级篇
上一篇的mongodb查询,主要介绍的是一些基本操作,当然有基本就高阶操作;
本文将带来更多的查询姿势
- 排序
- 分页
- 聚合
1. 排序
在mongodb中,使用sort方法进行排序,语法如下
db.collection.find().sort({key: 1})
请注意,sort内部是一个对象,key为field,value为1或者-1,其中1表示升序,-1表示降序
实例说明,根据age进行排序
db.doc_demo.find().sort({'age': 1})
输出如下:
上面的演示属于常规的操作,但是针对mongodb的特点,自然会有一些疑问
q1: 如果某个文档没有包含这个field,排序是怎样的?
db.doc_demo.find().sort({'tag': 1})
从输出来看,升序时,不包含这个field的文档,在最前面;降序时,不包含这个field的文档,在最后面
q2: 支持多个field排序吗?
原则上一般不建议多个field的排序(比较影响性能),但对于数据库而言,你得支持吧
# 在开始之前,先改一下tag,让文档不完全一致
db.doc_demo.update({"_id": ObjectId("5e7b5ac10172dc950171c488")}, {$set: {'tag': 2}})
db.doc_demo.update({"_id": ObjectId("5e7b5bb085a742842d2e23fc")}, {$set: {'tag': 2}})
# 先根据age进行升序排,当age相同的,根据tag降序排
db.doc_demo.find().sort({'age': 1, 'tag': -1})
# 先根据tag进行升序排,tag相同的,根据age升序排
db.doc_demo.find().sort({'tag': 1, 'age': 1})
请注意上的输出,在涉及到多个field排序时,优先根据第一个进行排序,当文档的field相同时,再根据后面的进行排序
2. 分页
当文档很多时,我们不可能把所有的文档一次返回,所以就有了常见的分页,在sql中我们一般使用limit
offset
来实现分页,在mongodb中也差不多
limit()
限制返回的文档数
db.doc_demo.find().limit(2)
skip()
使用limit进行返回条数限制,使用skip进行分页,表示跳过前面的n条数据
# 跳过第一条数据,返回两条; 相当于返回第2、3条数据
db.doc_demo.find().limit(2).skip(1)
3. 聚合
使用aggregate()
来实现聚合,用于处理求和、平均值,最大值,分组等
数据准备:
{ "_id" : ObjectId("5e7b5ac10172dc950171c488"), "name" : "一灰灰blog", "age" : "19", "skill" : [ "java", "python", "sql" ], "tag" : 2 }
{ "_id" : ObjectId("5e7b5ac40172dc950171c489"), "name" : "一灰灰blog", "age" : 20, "skill" : [ "web", "shell", "js" ], "tag" : 1 }
{ "_id" : ObjectId("5e7b5bb085a742842d2e23fc"), "name" : "一灰灰", "age" : 18, "sex" : "man", "tag" : 2 }
{ "_id" : ObjectId("5e7b5c2e0172dc950171c48a"), "name" : "一灰灰", "age" : 18, "hobby" : [ "play game" ] }
分组查询
根据name进行分组统计
# 根据name进行分组,统计文档数量
# 相当于sql中的 select name, count(1) from doc_demo group by name
db.doc_demo.aggregate([{$group: {_id: "$name", size: {$sum: 1}}}])
请注意,分组的条件中
_id
: 表示根据哪个字段进行分组size: {}
: 表示聚合条件指定,将结果输出到名为size的field中filed
名前加$
进行指定
当前mongodb支持的聚合表达式包括:
表达式 | 说明 | 举例说明 |
---|---|---|
sum | 求和 | db.doc_demo.aggregate([{$group: {_id: "$name", size: {$sum: '$age'}}}]) |
avg | 平均值 | db.doc_demo.aggregate([{$group: {_id: "$name", size: {$avg: '$age'}}}]) |
min | 取最小 | db.doc_demo.aggregate([{$group: {_id: "$name", age: {$min: '$age'}}}]) |
max | 取最大 | db.doc_demo.aggregate([{$group: {_id: "$name", age: {$max: '$age'}}}]) |
push | 结果插入到一个数组中 | db.doc_demo.aggregate([{$group: {_id: "$name", age: {$push: '$age'}}}]) |
addToSet | 结果插入集合,过滤重复 | db.doc_demo.aggregate([{$group: {_id: "$name", age: {$addToSet: '$age'}}}]) |
first | 第一个 | db.doc_demo.aggregate([{$group: {_id: "$name", age: {$first: '$age'}}}]) |
last | 最后一个 | db.doc_demo.aggregate([{$group: {_id: "$name", age: {$last: '$age'}}}]) |
上面虽然介绍了分组支持的一些表达式,但是没有查询条件,难道只能针对所有的文档进行分组统计么?
分组过滤
借助$match
来实现过滤统计,如下
db.doc_demo.aggregate([
{$match: {'tag': {$gt: 1}}},
{$group: {_id: '$name', age: {$sum: 1}}}
])
请注意,$match的语法规则和find的查询条件一样,会将满足条件的数据传递给后面的分组计算
这种方式和liux中的管道特别相似,aggregate方法的参数数组中,前面的执行完毕之后,将结果传递给后面的继续执行,除了$match
和$group
之外,还有一些其他的操作
操作 | 说明 |
---|---|
$project | 修改输入文档的结构。可以用来重命名、增加或删除域,也可以用于创建计算结果以及嵌套文档。 |
$match | 用于过滤数据,只输出符合条件的文档。$match使用MongoDB的标准查询操作。 |
$limit | 用来限制MongoDB聚合管道返回的文档数。 |
$skip | 在聚合管道中跳过指定数量的文档,并返回余下的文档。 |
$unwind | 将文档中的某一个数组类型字段拆分成多条,每条包含数组中的一个值。 |
$group | 将集合中的文档分组,可用于统计结果。 |
$sort | 将输入文档排序后输出。 |
$geoNear | 输出接近某一地理位置的有序文档。 |
12 - 十一:文档 Document 查询非典型篇
前面介绍的查询可以说是常见的典型case,但是mongodb中有两个比价特殊的数据类型,数组 + 对象,自然的也会有一些非典型的查询case,下面主要针对这两种数据类型的查询姿势,给出实例讲解
1. 数组
首先准备一些供数组操作的文档如下
{ "_id" : ObjectId("5e7b5ac10172dc950171c488"), "name" : "一灰灰blog", "age" : 19, "skill" : [ "java", "python", "sql" ], "tag" : 2 }
{ "_id" : ObjectId("5e7b5ac40172dc950171c489"), "name" : "一灰灰blog", "age" : 20, "skill" : [ "web", "shell", "js" ], "tag" : 1 }
{ "_id" : ObjectId("5e7b5bb085a742842d2e23fc"), "name" : "一灰灰", "age" : 18, "sex" : "man", "tag" : 2 }
{ "_id" : ObjectId("5e7b5c2e0172dc950171c48a"), "name" : "一灰灰", "age" : 18, "hobby" : [ "play game" ] }
{ "_id" : ObjectId("5e7c5627f020f58f5323e52d"), "name" : "一灰灰2", "age" : 22, "skill" : [ "android", "ios" ] }
长度查询
根据数组长度进行查询,借助$size
来统计数组长度
# 查询数组长度为3的文档
db.doc_demo.find({'skill': {$size: 3}})
长度范围查询
请注意,不支持长度的比较查询,如下,会报语法错误
db.doc_demo.find({'skill:{$size: {$gt: 2}}})
要实现范围查询,可以借助$where
来实现($where
比较强大,后面单独说明)
# 请注意判空需要有
db.doc_demo.find({$where:'this.skill !=null && this.skill.length>2'})
数组内容查询
根据数组内容进行查询,常见的有两种方式,一个是直接根据数组定位比较如
# 查询skill数组中,第一个元素为java的文档
db.doc_demo.find({'skill.0': 'java'})
上面这种实用性可能并不大,另外一个常见的case就是查询数组中包含某个元素的文档,这时可以借助$elemMatch
来实现
# 查询skill数组中包含 java 元素的文档
db.doc_demo.find({'skill': {$elemMatch: {$eq: 'java'}}})
说明,当数组的元素是Object类型时,还可以用右边这种姿势:db.doc_demo.find({'skill': {$elemMatch: {'subField': 'xxx'}}})
2. Object
因为mongodb支持内嵌文档,所以根据内嵌文档进行查询的场景也是不少的
首先准备三个用于后续查询测试的文档
{ "_id" : ObjectId("5e7c5a61f020f58f5323e52e"), "name" : "一灰灰", "doc" : { "title" : "简单的标题", "content" : "简单的内容", "tag" : [ "java", "后端" ] } }
{ "_id" : ObjectId("5e7c5a8af020f58f5323e52f"), "name" : "一灰灰", "doc" : { "title" : "哈哈", "content" : "嘻嘻哈哈", "tag" : [ "随笔" ], "draft" : true } }
{ "_id" : ObjectId("5e7c5ae7f020f58f5323e530"), "name" : "一灰灰", "doc" : { "title" : "22", "content" : "3333", "tag" : [ "随笔" ], "draft" : false, "visit" : 10 } }
根据内嵌文档字段查询
查询姿势和field查询相似,只是需要注意一下key的语法为: field.subField
, 实例如下
db.doc_demo.find({'doc.title': '22'})
存在性查询
查询嵌入文档包含某个field的case,和普通的查询姿势也一样
db.doc_demo.find({'doc.visit': {$exists: true}})
排序
根据Object的成员进行排序,操作姿势也基本一样
db.doc_demo.find({'doc': {$exists: true}}).sort({'doc.visit': -1})
13 - 十二:文档更新删除之非典型篇
前面介绍document的新增、删除、更新都处于相对常见和基础的说明,但是考虑到mongodb非结构化的特点,它的一些特性是我们的mysql不会遇到的,本文将针对这些特殊场景给出示例说明
- 在现有文档中,增加一个field
- 删除文档中的某个field
- 重命名文档的field
- 在文档的数组orObject中,添加/删除/更新数据
1. 增加field
我们知道修改文档的命令格式如下
db.collection.update(
<query>,
<update>,
{
upsert: <boolean>,
multi: <boolean>,
writeConcern: <document>
}
)
当我们更新一个文档中,不存在的field,会怎样
# 插入一条数据,然后设置一个不存在的field
db.doc_demo.insert({ "author" : "一灰灰blog", "title" : "测试"})
db.doc_demo.update({'author': '一灰灰blog'}, {$set: {'skill': ['java', 'db']}})
2. 重命名field
同样是借助update方法,但是我们用到的关键字为 $rename
db.doc_demo.update({'author': '一灰灰blog'}, {$rename: {'skill': 'like'}})
请注意,当文档中不存在这个field,则不会有任何影响
3. 删除field
既然$set
可以新增一个不存在的field,那么是不是就可以用$unset
来删除一个已存在的field呢
db.doc_demo.update({'author': '一灰灰blog'}, {$unset: {'title': 1}})
4. 数组元素修改
数组元素的修改删除增加,可以参考官方教程: MongoDB update-array Method
如果我们希望直接修改数组中的某个元素,可以借助之前查询的case
# 修改数组中第0个元素
db.doc_demo.update({'author': '一灰灰blog'}, {$set: {'like.0': 'spring'}})
# 如果查询条件中,包含了数组内容的过滤,则可以用`$`来代替具体的数组下标,如
db.doc_demo.update({'author': '一灰灰blog', 'like': {$eq: 'db'}}, {$set: {'like.$': 'mysql'}})
请注意,使用$
占位符的前途是,前面的查询条件可以限定数组元素
5. 数组元素新增
元素添加支持两种方式,一是addToSet
,一是push
$addToSet
- 确保没有重复的项添加到数组集合,对于已经存在的重复元素不受影响;
- 不能保证添加时元素的顺序
- 如果值是数组,则作为一个元素添加进去
- 可以通过
$each
实现添加多个元素到数组中
# 不存在时,则添加,存在则忽略
db.doc_demo.update({'author': '一灰灰blog'}, {$addToSet: {'like': 'redis'}})
# 借助 $each 实现批量添加
db.doc_demo.update({'author': '一灰灰blog'}, {$addToSet: {'like': {$each: ['mongodb', 'es']}}})
$push
- 如果被更新的文档该数组不存在,那么$push将添加数组字段和值
- 如果字段不是数组,失败
- 如果值是数组,那么整个数组作为一个单个元素添加到数组
# 不存在时,创建一个数组
db.doc_demo.update({'author': '一灰灰blog'}, {$push: {'skill': 'a'}})
# 存在时,添加到数组
db.doc_demo.update({'author': '一灰灰blog'}, {$push: {'skill': 'a'}})
# 批量添加
db.doc_demo.update({'author': '一灰灰blog'}, {$push: {'skill': {$each: ['b', 'c']}}})
6. 数组元素删除
$pop
删除第一个or最后一个
# 删除最后一个
db.doc_demo.update({'author': '一灰灰blog'}, {$pop: {'skill': 1}})
# 删除第一个
db.doc_demo.update({'author': '一灰灰blog'}, {$pop: {'skill': -1}})
$pull
删除满足条件的数组元素
# 将数组中添加几个元素
db.doc_demo.update({'author': '一灰灰blog'}, {$push: {'skill': {$each: ['a', 'b', 'c']}}})
# 删除指定的元素
db.doc_demo.update({'author': '一灰灰blog'}, {$pull: {'skill': 'b'}})
# 删除多个指定的元素
db.doc_demo.update({'author': '一灰灰blog'}, {$pull: {'skill': {$in: ['a', 'c']}}})
注意,$pull
后面跟上的可以理解为限定条件,查询教程篇的一些操作也是支持的(如比较查询等)
7. 内嵌文档操作
对于内嵌文档的操作,实际上普通的field的操作姿势没有什么区别,只是对于key加了一个xx.xx
的限定而已
# 删除测试数据
db.doc_demo.remove({})
# 初始话一条演示文档
db.doc_demo.insert({'author': '一灰灰blog',})
# 不存在内嵌文档,则新增
db.doc_demo.update({}, {$set: {'t': {'a': 1, 'b': 2}}})
# 修改子field
db.doc_demo.update({}, {$set: {'t.a': 10}})
# 新增子field
db.doc_demo.update({}, {$set: {'t.c': 'c'}})
# 删除子field
db.doc_demo.update({}, {$unset: {'t.c': 1}})
# 重命名
db.doc_demo.update({}, {$rename: {'t.b': 't.dd'}})
14 - 十三:索引
索引一般用来提高查询效率,避免全集合搜索,那么在mongodb中,支持索引么?如果支持,如何定义索引,如何使用索引,如何确定一个sql是否走索引?
1. 创建索引
语法定义:
db.collection.createIndex(keys, options)
请注意,在3.0之前的版本中,也可以使用ensureIndex
来创建索引
参数说明:
- keys:kv结构,key为fieldName, value为1 表示升序创建索引;-1 表示降序创建索引;支持多字段索引
- options:可选参数
常见参数说明如下表:
参数名 | 说明 |
---|---|
background |
true,则后台方式创建索引,不阻塞其他操作;默认为false |
unique |
true,则表示唯一约束索引,比如_id 就有唯一约束;默认为false |
name |
索引名,不指定时,根据field + 方向生成索引名 |
sparse |
true, 则不包含这个字段的不创建索引,且索引查询时查不到不包含这个字段的文档;默认false |
expireAfterSeconds |
设置文档在集合的生存时间,s为单位 |
v |
版本号 |
weight |
索引权重值,数值在 1 到 99,999 之间,表示该索引相对于其他索引字段的得分权重 |
default_language |
对于文本索引,该参数决定了停用词及词干和词器的规则的列表。 默认为英语 |
language_override |
对于文本索引,该参数指定了包含在文档中的字段名,语言覆盖默认的language,默认值为 language |
实例如下:
db.doc_demo.createIndex({'name': 1}, {'background': true})
2. 索引查询
查看一个集合定义了哪些索引,借助getIndexes()
方法即可,如
db.doc_demo.getIndexes()
3. 索引分析
虽然我们创建了索引,但是我们的查询语句却并不一定会走索引,在mysql中我们知道有一个explain
语句来分析索引情况,在mongodb中也存在类似的方法
集合数据如下
{ "_id" : ObjectId("5e7b5ac10172dc950171c488"), "name" : "一灰灰blog", "age" : 19, "skill" : [ "java", "python", "sql" ], "tag" : 2 }
{ "_id" : ObjectId("5e7b5ac40172dc950171c489"), "name" : "一灰灰blog", "age" : 20, "skill" : [ "web", "shell", "js" ], "tag" : 1 }
{ "_id" : ObjectId("5e7b5bb085a742842d2e23fc"), "name" : "一灰灰", "age" : 18, "sex" : "man", "tag" : 2 }
{ "_id" : ObjectId("5e7b5c2e0172dc950171c48a"), "name" : "一灰灰", "age" : 18, "hobby" : [ "play game" ] }
{ "_id" : ObjectId("5e7c5627f020f58f5323e52d"), "name" : "一灰灰2", "age" : 22, "skill" : [ "android", "ios" ] }
{ "_id" : ObjectId("5e7c5a61f020f58f5323e52e"), "name" : "一灰灰", "doc" : { "title" : "简单的标题", "content" : "简单的内容", "tag" : [ "java", "后端" ] } }
{ "_id" : ObjectId("5e7c5a8af020f58f5323e52f"), "name" : "一灰灰", "doc" : { "title" : "哈哈", "content" : "嘻嘻哈哈", "tag" : [ "随笔" ], "draft" : true } }
{ "_id" : ObjectId("5e7c5ae7f020f58f5323e530"), "name" : "一灰灰", "doc" : { "title" : "22", "content" : "3333", "tag" : [ "随笔" ], "draft" : false, "visit" : 10 } }
当前集合上除了默认的_id
索引之外,针对name
也创建了升序索引
如需要判断一个查询语句的情况,可以在后面加上explain()
方法,如下
db.doc_demo.find({'name': '一灰灰'}).explain()
输出如下
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "basic.doc_demo",
"indexFilterSet" : false,
"parsedQuery" : {
"name" : {
"$eq" : "一灰灰"
}
},
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"name" : 1
},
"indexName" : "name_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"name" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"name" : [
"[\"一灰灰\", \"一灰灰\"]"
]
}
}
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "0f51c424211c",
"port" : 27017,
"version" : "4.0.4",
"gitVersion" : "f288a3bdf201007f3693c58e140056adf8b04839"
},
"ok" : 1
}
关于是否走索引,主要看stage,通常会有以下几种状态
stage | 描述 |
---|---|
COLLSCAN | 全表扫描 |
IXSCAN | 扫描索引 |
FETCH | 根据索引去检索指定document |
SHARD_MERGE | 将各个分片返回数据进行merge |
SORT | 表明在内存中进行了排序 |
LIMIT | 使用limit限制返回数 |
SKIP | 使用skip进行跳过 |
IDHACK | 针对_id进行查询 |
SHARDING_FILTER | 通过mongos对分片数据进行查询 |
COUNT | 利用db.coll.explain().count()之类进行count运算 |
COUNTSCAN | count不使用Index进行count时的stage返回 |
COUNT_SCAN | count使用了Index进行count时的stage返回 |
SUBPLA | 未使用到索引的$or查询的stage返回 |
TEXT | 使用全文索引进行查询时候的stage返回 |
PROJECTION | 限定返回字段时候stage的返回 |
上面的具体查询,对应的stage组合是Fetch+ixscan
,也就是说会根据索引查询
虽然mongodb会根据查询来选择索引,但并不能保证都能选到最优的索引;这种时候我们可以通过hint
来强制指定索引,举例如下
db.doc_demo.find({'age': 18, 'name':'一灰灰'}).hint({'name': 1}).explain()
4. 删除索引
一般有下面两种删除方式,全量删除和指定索引删除
# 全量删除
db.collection.dropIndexes()
# 指定删除
db.collection.dropIndex(索引名)
请注意,指定索引名删除时,如果不确定索引名是啥,可以通过getIndexes()
来查看
5. 文档自动删除
在创建索引的时候,其中有一个参数比较有意思,有必要单独拿出来说明一下,expireAfterSeconds
设置文档的生存时间
使用它有几个潜规则:
- 索引字段为Date类型
- 单字段索引,不支持混合索引
- 非立即执行
# 插入一条文档,请注意这个时间,因为时区原因相对于北京时间,少8小时
db.doc_demo.insert({'name': 'yihui', 'log': '操作了啥啥啥', 'createDate': new Date('Mar27, 2020 2:54:00')})
# 创建索引
db.doc_demo.createIndex({'createDate': 1}, {expireAfterSeconds: 60})
然后过一段时间(并不一定10:55分的时候会删除)再去查询,会发现插入的文档被删除了
利用这种特性,在mongodb中存一些需要定时删除的数据,相比较我们常用的mysql而言,还是有很大优势的
6. 覆盖索引
覆盖索引的概念有些类似mysql中的不回表查询的case,直接查询索引,就可以返回所需要的字段了
比如在前面的case中,我只查询name字段,可以走覆盖索引;但是返回除了name,还有_id
,那么就不能了
# 覆盖索引
db.doc_demo.find({'name': '一灰灰'}, {'name': 1, '_id': 0})
# 非覆盖索引
db.doc_demo.find({'name': '一灰灰'}, {'name': 1})
注意:所有索引字段是一个数组时,不能使用覆盖索引