这是本节的多页打印视图。 点击此处打印.

返回本页常规视图.

一灰灰学MongoDB

时序数据库InfluxDb的基本使用姿势教程

InfluxDB是什么?

MongoDB 是一个基于分布式文件存储的数据库。由 C++ 语言编写。旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。

MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。

系列教程讲什么,可以带来什么收获?

《一灰灰学MongoDB》 记录了一灰灰当初学习并实际使MongoDB的全过程,其中包含两部分

基础系列教程

通过基础系列教程,学会文档数据库MongoDB的CURD,直接上手mongodb的业务开发

实战小技巧

这里将记录一些一灰灰个人在实际使用过程的可以用到的小技巧,主要是为了让我们更好的来使用它

1 - MongoDB系列教程

文档数据库MongoDB的基本使用姿势教程,手把手教你学会MongoDB的CURD

1.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了

1.2 - 一:基本概念

mongodb和我们通常使用的关系型数据库如mysql,在一些基本概念上有相同之处,但也有一些区别,在进行mongodb的语言介绍之前,有必要先了解一些基础概念

本文将对比sql对一些基础概念进行解释说明

I. 基本概念

MongoDB 概念解析

在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. 事务

MongoDB 4.0 事务实现解析

版本>= 4.0,支持事务,支持多文档ACID,后续详细说明

1.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

1.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输出一次

1.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字节

1.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.开头

1.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
  • 最后三个字节是随机数

1.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})

再次查看剩下的内容如下:

1.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" ] })

1.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})

1.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 输出接近某一地理位置的有序文档。

1.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})

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'}})

1.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})

注意:所有索引字段是一个数组时,不能使用覆盖索引

2 - MongoDB小技巧

文档数据库MongoDB的一些使用小技巧收录

2.1 - MongoDB之Collection导入导出

mongodb中集合的导入导出,在robot3t工具中没有找到对应的方法,记录下控制台的操作流程

主要利用: mongoexportmongoimport

1. 集合导出

直接使用 mognoexport 即可,通过mongoexport --help查看对应的使用说明

将库database中的集合collection导出到json文件out.json

bin/mongoexport -h localhost:27107 -u user -p pwd -d database -c collection -o out.json

2. 集合导入

使用 mongoimport 实现导入,同样可以输入--help查看使用说明

bin/mongoimport -h localhost:27107 -u user -p pwd -d database -c new_collection ./out.json