mongodb学习笔记

目录

    • 1、MongoDb简介
    • 2、Ubuntu安装mongodb
    • 3、Docker安装mongodb
    • 4、MongoDb概念解析
      • 4.1 数据库
      • 4.2 文档
      • 4.3 集合
      • 4.4 mongodb数据类型
      • 4.5 连接mongodb
    • 5、MongoDb基本操作
    • 6、CRUD操作
      • 6.1 插入文档
        • 插入文档Golang操作:
      • 6.2 查找文档
        • 6.2.1 基本操作
        • 6.2.2 mongoDb与RDBMS的where条件查询对比:
        • 6.2.3 mongodb AND条件
        • 6.2.4 mongodb OR条件
        • 6.2.5 分页查询
        • 6.2.7 排序
        • 6.2.8 嵌套文档查询
        • 6.2.9 查询数组
        • 6.2.10 查询集合中文档数量
        • 6.2.11 查询文档Golang操作
      • 6.3 更新文档
        • 更新文档Golang操作
      • 6.4 删除文档
        • 删除文档Golang操作
    • 7、索引
      • 7.1 创建索引
        • 指定索引名字
      • 7.2 索引类型
        • 7.2.1 单字段索引
        • 7.2.2 复合索引
      • 7.3 多键索引
      • 7.4 全文索引
        • 7.4.1 创建全文索引
        • 7.4.2 为全文索引指定语言
      • 7.5 索引属性
        • 7.5.1 TTL index
        • 7.5.2 唯一索引(unique index)
        • 7.5.3 隐藏索引
      • 7.6 删除索引
    • 8、聚合
      • 8.1 聚合管道(Aggregation Pipeline)
      • 8.2 聚合管道阶段
        • 8.2.1 $addFields
        • 8.2.2 $count
        • 8.2.3 $group
        • 8.2.4 $limit 和 $skip
        • 8.2.5 $match
        • 8.2.6 $out
        • 8.2.7 $project
        • 8.2.8 $set
        • 8.2.9 $sort
      • 8.3 聚合管道优化
        • 8.3.1 投影优化
        • 8.3.2 管道顺序优化
    • 9、副本集
      • 9.1 mongodb中的副本集
        • 9.1.1 副本集的概念
        • 9.1.2 自动故障切换
        • 9.1.3 读操作
        • 9.1.4 使用docker搭建副本集

1、MongoDb简介

MongoDB是一个基于分布式文件存储的数据库。由C++语言编写。

MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。他支持的数据结构非常松散,是类似json的bson格式,因此可以存储比较复杂的数据类型。

Mongo最大的特点是他支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。

mongodb学习笔记_第1张图片

官方文档:https://www.mongodb.com/docs/manual/introduction/

2、Ubuntu安装mongodb

(1)直接安装

sudo apt-get install mongodb

(2)安装完成后,进程会自动启动,查看进程

ps -ef | grep mongodb

在这里插入图片描述

(3)直接在控制台输入mongo来连接mongodb

mongo

mongodb学习笔记_第2张图片

默认情况下 MongoDB 启动后会初始化以下两个目录:

  • 数据存储目录:/var/lib/mongodb
  • 日志文件目录:/var/log/mongodb

mongodb数据库的可执行文件放在了/usr/bin目录下:

  • mongodb数据库服务端:/usr/bin/mongod
  • mongodb数据库客户端:/usr/bin/mongo
  • mongodb配置文件:/etc/mongodb.conf

(4)启动mongodb,先进入/usr/bin目录下

./mongod [--dbpath dbpath]  [--logpath logpath] --logappend &

--dbpath:指定mongo的数据库文件在哪个文件夹
--logpath:指定mongo的log日志是哪个,这里log一定要指定到具体的文件名
--logappend:表示log的写入是采用附加的方式,默认的是覆盖之前的文件
&:表示程序在后台运行

(5)停止/启动mongodb

service stop mongodb
service start mongodb

3、Docker安装mongodb

(1)搜索mongo镜像

docker search mongo

(2)拉取mongo镜像

docker pull mongo

(3)查看本地镜像

docker images

mongodb学习笔记_第3张图片

(4)运行容器

docker run -id --name mongo -p 27017:27017 mongo --auth

参数:
-p 27017:27017 :映射容器服务的 27017 端口到宿主机的 27017 端口。外部可以直接通过宿主机ip:27017访问到mongo 的服务。
--auth:需要密码才能访问容器服务。

(5)查看运行中的容器

docker ps

在这里插入图片描述

接着使用以下命令添加用户和设置密码,并且尝试连接。

# 进入容器连接mongo
$ docker exec -it mongo mongo admin

# 创建一个名为 admin,密码为 123456 的用户。
>  db.createUser({ user:'admin',pwd:'123456',roles:[ { role:'userAdminAnyDatabase', db: 'admin'},"readWriteAnyDatabase"]});

# 尝试使用上面创建的用户信息进行连接。
> db.auth('admin', '123456')

4、MongoDb概念解析

在mongo中的基本概念是数据库、集合、文档。

SQL中的概念 mongo中的概念 说明
database database 数据库
table collection 数据库表/集合
row document 数据行/文档
column field 数据字段/域
index index 索引
primary key primary key 主键,MongoDB自动将_id字段设置为主键

通过下面实例,可以更直观地了解mongo中地一些概念:

mongodb学习笔记_第4张图片

4.1 数据库

Mongo中可以创建多个数据库,MongoDB的单个实例可以容纳多个独立的数据库,每一个都有自己的集合和权限,不同的数据库也放置在不同的文件中。

  • 列出所有数据库:
show dbs

# 示例
> show dbs
admin   0.000GB
config  0.000GB
local   0.000GB
  • 查看当前使用的数据库:
db

# 示例
> db
test
  • 切换数据库:
use dbname

# 示例
> use local
switched to db local

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

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

 

4.2 文档

文档是一组键值(key-value)对(即 BSON),类似于mysql的一个表格中的一条数据。MongoDB 的文档不需要设置相同的字段,并且相同的字段不需要相同的数据类型,这与关系型数据库有很大的区别,也是 MongoDB 非常突出的特点。

例如:

{"name":"zhangsan", "age": 20, "gender":"female"}

需要注意的是:

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

 

4.3 集合

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

查看数据库中的集合:

show collections
myrs:PRIMARY> show collections
inventory
messages
user

 

4.4 mongodb数据类型

下表为mongodb中常用的几种数据类型:

数据类型 描述
String 字符串。存储数据常用的数据类型。在 MongoDB 中,UTF-8 编码的字符串才是合法的。
Integer 整型数值。用于存储数值。根据你所采用的服务器,可分为 32 位或 64 位。
Boolean 布尔值。用于存储布尔值(真/假)。
Double 双精度浮点值。用于存储浮点值。
Min/Max keys 将一个值与 BSON(二进制的 JSON)元素的最低值和最高值相对比。
Array 用于将数组或列表或多个值存储为一个键。
Timestamp 时间戳。记录文档修改或添加的具体时间。
Object 用于内嵌文档。
Null 用于创建空值。
Symbol 符号。该数据类型基本上等同于字符串类型,但不同的是,它一般用于采用特殊符号类型的语言。
Date 日期时间。用 UNIX 时间格式来存储当前日期或时间。你可以指定自己的日期时间:创建 Date 对象,传入年月日信息。
Object ID 对象 ID。用于创建文档的 ID。
Binary Data 二进制数据。用于存储二进制数据。
Code 代码类型。用于在文档中存储 JavaScript 代码。
Regular expression 正则表达式类型。用于存储正则表达式。

ObjectId

ObjectId类似唯一主键,可以很快地生成和排序,包含12bytes:

  • 前4个字节表示unix时间戳,格林尼治UTC时间,比北京时间晚8小时。
  • 接下来的3个字节是机器标识码。
  • 紧接着的2个字节由进程id组成PID。
  • 最后三个字节是随机数。

mongodb学习笔记_第5张图片

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

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

> var newObj = ObjectId()

> newObj.getTimestamp()
ISODate("2021-11-12T04:56:05Z")

> newObj.str
618df3e542a3268f4fa025b9

字符串

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

时间戳

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

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

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

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

日期

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

> var date1 = new Date()
> date1
ISODate("2021-11-12T05:01:03.998Z")
> typeof date1
object

> var date2 = ISODate()
> date2
ISODate("2021-11-12T05:01:41.918Z")

 

4.5 连接mongodb

可以使用mongo命令来连接mongodb,mongodb默认启动在27017端口

mongo uri

uri的格式如下:

mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]
  • mongodb:// 这是固定的格式,必须要指定。
  • username:password@ 可选项,如果设置,在连接数据库服务器之后,驱动都会尝试登录这个数据库
  • host1 必须的指定至少一个host, host1 是这个URI唯一要填写的。它指定了要连接服务器的地址。如果要连接复制集,请指定多个主机地址。
  • portX 可选的指定端口,如果不填,默认为27017
  • /database 如果指定username:password@,连接并验证登录指定数据库。若不指定,默认打开 test 数据库。
  • ?options 是连接选项。如果不使用/database,则前面需要加上/。所有连接选项都是键值对name=value,键值对之间通过&或;(分号)隔开

options选项如下:

选项 描述
replicaSet=name 验证replica set的名称。 Impliesconnect=replicaSet.
slaveOk=true|false true:在connect=direct模式下,驱动会连接第一台机器,即使这台服务器不是主。在connect=replicaSet模式下,驱动会发送所有的写请求到主并且把读取操作分布在其他从服务器。false: 在 connect=direct模式下,驱动会自动找寻主服务器. 在connect=replicaSet 模式下,驱动仅仅连接主服务器,并且所有的读写命令都连接到主服务器。
safe=true|false true: 在执行更新操作之后,驱动都会发送getLastError命令来确保更新成功。(还要参考 wtimeoutMS).false: 在每次更新之后,驱动不会发送getLastError来确保更新成功。

 

例如,连接本地的mongo,使用test数据库:

mongo mongodb://localhost:27017/test
或
mongo
use test

连接三台repilca set

replica set是mongo的高可用机制,它是一种复制机制。

连接三台docker启动的mongo,端口映射为27017、27018、27019,写入操作应用在主服务器 并且分布查询到从服务器。

mongo mongodb://localhost:27017,localhost:27018,localhost:27019/?slaveOk=true

 

mongodb学习笔记_第6张图片

 

Golang连接mongodb

Golang mongodb的数据库驱动为go.mongodb.org/mongo-driver/mongo,代码如下:

type MongoConf struct {
	Uri         string
	DB          string
	MaxPoolSize uint32
	MinPoolSize uint32
}

var mc = &MongoConf{
	Uri:         "mongodb://192.168.44.100:27017",
	DB:          "test",
	MaxPoolSize: 20,
	MinPoolSize: 10,
}

var mgo *mongo.Database

func initMongoDB() {
    // 设置连接mongo的options,设置uri、连接池的最大和最小数量
	clientOptions := options.Client().ApplyURI(mc.Uri).
		SetMaxPoolSize(uint64(mc.MaxPoolSize)).SetMinPoolSize(uint64(mc.MinPoolSize))
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
	defer cancel()
    // 连接mongo
	conn, err := mongo.Connect(ctx, clientOptions)
	if err != nil {
		panic(fmt.Sprintf("Connect to mongodb error:%v", err))
	}
	
    // ping 测试是否连通
	if err = conn.Ping(context.Background(), nil); err != nil {
		panic(fmt.Sprintf("Ping error:%v", err))
	} else {
		fmt.Println("Connect to mongodb success")
	}
	mgo = conn.Database(mc.DB)
}

 

5、MongoDb基本操作

(1)创建数据库:

use DB_NAME

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

(2)查看所有数据库

show dbs

(3)删除当前数据库

db.dropDatabase()

(4)创建集合

db.createCollection(name, options)

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

options可选以下参数:

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

(5)查看所有集合

show collections 或
show tables

示例1:在mytest数据库中创建users集合并查看集合

> db
mytest

> db.createCollection("users")
{ "ok" : 1 }

> show collections
users

> show tables
users

示例2:创建固定集合 mycol,整个集合空间大小 6142800 B, 文档最大个数为 10000 个。

> db.createCollection("mycoll", {capped: true, size: 6142800, max: 10000})
{ "ok" : 1 }

> show tables
mycoll
users

(6)删除集合

db.COLLECTION_NAME.drop()

# 例如
> db.users.drop()
true
> show tables
mycoll

(7)也可以先不创建集合,在插入数据时集合会自动创建

> show tables
mycoll
> db.users.insert({"name":"zhangsan", "age": 25, "gender":"female"})
WriteResult({ "nInserted" : 1 })
> show tables
mycoll
users

 

6、CRUD操作

6.1 插入文档

使用insert或save向集合中插入文档:

db.COLLECTION_NAME.insert()
或
db.COLLECTION_NAME.save()
  • save():如果 _id 主键存在则更新数据,如果不存在就插入数据。该方法新版本中已废弃,可以使用 db.collection.insertOne()db.collection.replaceOne() 来代替。
  • insert(): 若插入的数据主键已经存在,则会抛 org.springframework.dao.DuplicateKeyException 异常,提示主键重复,不保存当前数据。

示例:

myrs:PRIMARY> db.user.insert({username:"zhangsan", password:"root123"})
WriteResult({ "nInserted" : 1 })

myrs:PRIMARY> db.user.find()
{ "_id" : ObjectId("63553cbbec927a5603e2b31c"), "username" : "zhangsan", "password" : "root123" }

由于mongo的命令行是一个js解析器,因此也可以将要插入的数据定义为一个变量,然后再插入。

myrs:PRIMARY> lisi = {username: "lisi", password: "lisi666"}
{ "username" : "lisi", "password" : "lisi666" }

myrs:PRIMARY> lisi._id = ObjectId()
ObjectId("63553d65ec927a5603e2b31d")

myrs:PRIMARY> db.user.insert(lisi)
WriteResult({ "nInserted" : 1 })

myrs:PRIMARY> db.user.find()
{ "_id" : ObjectId("63553cbbec927a5603e2b31c"), "username" : "zhangsan", "password" : "root123" }
{ "_id" : ObjectId("63553d65ec927a5603e2b31d"), "username" : "lisi", "password" : "lisi666" }
插入文档Golang操作:
type ChatMessage struct {
	UserId     uint32 `bson:"userid"`         // 发送消息的用户ID
	PeerId     uint32 `bson:"peerid"`         // 对方ID
	Message    string `bson:"message"`
	CreateTime int64  `bson:"createtime"`
	Status     int    `bson:"status"`         // 消息的状态 0 未被接收, 1 已被接收
}

// 插入一条记录
func InsertOne() {
    // 获取集合
    collection := mgo.Collection("messages")
	cm := ChatMessage{
		UserId:     1,
		PeerId:     2,
		Message:    "hello, world",
		CreateTime: time.Now().Unix(),
		Status:     0,
	}
	
    // 使用insertOne来插入一条记录
	result, err := collection.InsertOne(context.TODO(), &cm)
	if err != nil {
		fmt.Println("Insert failed, err:", err)
	} else {
		fmt.Println("Insert ok, ID:", result.InsertedID)
	}

}

// 插入多条记录
func InserMany(collection *mongo.Collection) {
	ms := []interface{}{
		&ChatMessage{
			UserId:     2,
			PeerId:     3,
			Message:    "How are you?",
			CreateTime: time.Now().Unix(),
			Status:     1,
		},
		&ChatMessage{
			UserId:     3,
			PeerId:     2,
			Message:    "I am fine, and you?",
			CreateTime: time.Now().Unix(),
			Status:     1,
		},
		&ChatMessage{
			UserId:     2,
			PeerId:     3,
			Message:    "I am ok, bye",
			CreateTime: time.Now().Unix(),
			Status:     1,
		},
		&ChatMessage{
			UserId:     3,
			PeerId:     2,
			Message:    "bye bye",
			CreateTime: time.Now().Unix(),
			Status:     0,
		},
	}

	result, err := collection.InsertMany(context.TODO(), ms)
	if err != nil {
		fmt.Println("Insert failed, err:", err)
	} else {
		fmt.Println("Insert ok, ID:", result.InsertedIDs)
	}

}

 

6.2 查找文档

6.2.1 基本操作

find()用于查找文档,语法如下:

db.collection.find(<filter>, <fields>)

filter:查询的过滤条件,类似mysql中的where字句

fields:为要查询的字段,如果不指定,则查询所有字段。{field: 0 or 1} ,1表示查询该字段,0表示不查询该字段

示例:查询所有记录

myrs:PRIMARY> db.user.find()
{ "_id" : ObjectId("63553cbbec927a5603e2b31c"), "username" : "zhangsan", "password" : "root123" }
{ "_id" : ObjectId("63553d65ec927a5603e2b31d"), "username" : "lisi", "password" : "lisi888" }
{ "_id" : ObjectId("6355411c5035c8ac0c11e0f2"), "username" : "laowang", "password" : "aabb" }
{ "_id" : ObjectId("6355412a5035c8ac0c11e0f3"), "username" : "laoliu", "password" : "ccdd" }

如果需要以易读的方式查看数据,可以使用pretty()方法

使用prety以易读方式展示

myrs:PRIMARY> db.user.find().pretty()
{
        "_id" : ObjectId("63553cbbec927a5603e2b31c"),
        "username" : "zhangsan",
        "password" : "root123"
}
{
        "_id" : ObjectId("63553d65ec927a5603e2b31d"),
        "username" : "lisi",
        "password" : "lisi888"
}
{
        "_id" : ObjectId("6355411c5035c8ac0c11e0f2"),
        "username" : "laowang",
        "password" : "aabb"
}
{
        "_id" : ObjectId("6355412a5035c8ac0c11e0f3"),
        "username" : "laoliu",
        "password" : "ccdd"
}

查询username为laowang的用户:

myrs:PRIMARY> db.user.find({username: "laowang"})
{ "_id" : ObjectId("6355411c5035c8ac0c11e0f2"), "username" : "laowang", "password" : "aabb" }

db.user.find({username: “laowang”})相当于sql语句的:

select * from user where username = "laowang"

如果想指定查询结果中的字段,就需要在第二个参数指定

示例:查询username为laowang的用户的密码:

myrs:PRIMARY> db.user.find({username: "laowang"}, {password: 1, _id: 0})
{ "password" : "aabb" }

myrs:PRIMARY> db.user.find({username: "laowang"}, {username: 0, _id: 0})
{ "password" : "aabb" }

上面的语句相当于:

select password from user where username = "laowang"

如果不指定_id,_id默认是会被查询出来的

 

除了find()方法,还有一个findOne()方法来查询一条记录。

示例:

myrs:PRIMARY> db.user.findOne()
{
        "_id" : ObjectId("63553cbbec927a5603e2b31c"),
        "username" : "zhangsan",
        "password" : "root123"
}
6.2.2 mongoDb与RDBMS的where条件查询对比:
操作 mongo格式 mongo范例 mysql示例
等于 {:} db.user.find({username: "zhangsan"}) where username = "zhangsan"
小于 {:{$lt:}} db.user.find({id: {$lt: 3}}) where id < 3
小于或等于 {:{$lte:}} db.user.find({id: {$lte: 3}}) where id <= 3
大于 {:{$gt:}} db.user.find({id: {$gt: 20}}) where id > 3
大于或等于 {:{$gte:}} db.user.find({id: {$gte: 20}}) where id >= 3
不等于 {:{$ne:}} db.user.find({id: {$ne: 5}}) where id != 5

lt: less than

lte: less than equal

gt: greater than

gte: greater than equal

ne: not equal

6.2.3 mongodb AND条件

mongo的and条件就是在filter中添加多个键值对

示例:查询用户名密码

myrs:PRIMARY> db.user.find({username: "zhangsan", password: "root123"})
{ "_id" : ObjectId("63553cbbec927a5603e2b31c"), "username" : "zhangsan", "password" : "root123" }
6.2.4 mongodb OR条件

mongodb OR条件要用到$or指令,语法如下:

db.collection.find(
	{
        $or: [{key1: val1}, {key2: val2}]
    }
)

示例:查找用户名为zhangsan或lisi的文档

myrs:PRIMARY> db.user.find({$or: [{username:"zhangsan"},{username:"lisi"}]})
{ "_id" : ObjectId("63553cbbec927a5603e2b31c"), "username" : "zhangsan", "password" : "root123" }
{ "_id" : ObjectId("63553d65ec927a5603e2b31d"), "username" : "lisi", "password" : "lisi888" }
6.2.5 分页查询

如果需要对数据进行分页,需要使用limit()和skip方法。

limit():接收一个数字参数,该参数指定从mongodb中读取的记录条数

skip(): 接收一个数字参数,该参数指定跳过的记录条数

示例:查询两条记录

myrs:PRIMARY> db.user.find().limit(2)
{ "_id" : ObjectId("63553d65ec927a5603e2b31d"), "username" : "lisi", "password" : "lisi888" }
{ "_id" : ObjectId("6355411c5035c8ac0c11e0f2"), "username" : "laowang", "password" : "aabb" }

跳过前两条记录并查找两条记录(查找第二页,pageNum = 2, pageSize = 2)

myrs:PRIMARY> db.user.find().skip(2).limit(2)
{ "_id" : ObjectId("6355412a5035c8ac0c11e0f3"), "username" : "laoliu", "password" : "ccdd" }
{ "_id" : ObjectId("63553cbbec927a5603e2b31c"), "username" : "zhangsan", "password" : "root123" }
6.2.7 排序

在mongodb中使用sort方法来对查询的数据进行排序,sort()方法可以通过参数指定根据排序的字段和排序规则:1为升序,-1为降序

sort语法如下:

db.collection.find().sort({KEY:1 or -1})

示例:根据username进行升序排序

myrs:PRIMARY> db.user.find().sort({username: 1})
{ "_id" : ObjectId("6355412a5035c8ac0c11e0f3"), "username" : "laoliu", "password" : "ccdd" }
{ "_id" : ObjectId("6355411c5035c8ac0c11e0f2"), "username" : "laowang", "password" : "aabb" }
{ "_id" : ObjectId("63553d65ec927a5603e2b31d"), "username" : "lisi", "password" : "lisi888" }
{ "_id" : ObjectId("63553cbbec927a5603e2b31c"), "username" : "zhangsan", "password" : "root123" }
6.2.8 嵌套文档查询

对于下面的集合,文档中包含了一个Object对象size

db.inventory.insertMany( [
   { item: "journal", qty: 25, size: { h: 14, w: 21, uom: "cm" }, status: "A" },
   { item: "notebook", qty: 50, size: { h: 8.5, w: 11, uom: "in" }, status: "A" },
   { item: "paper", qty: 100, size: { h: 8.5, w: 11, uom: "in" }, status: "D" },
   { item: "planner", qty: 75, size: { h: 22.85, w: 30, uom: "cm" }, status: "D" },
   { item: "postcard", qty: 45, size: { h: 10, w: 15.25, uom: "cm" }, status: "A" }
]);

如果要根据文档中的某个对象中的一个字段进行查询,需要使用 field.nestedField作为key

示例:

myrs:PRIMARY> db.inventory.find({"size.h": {$gt: 10}})
{ "_id" : ObjectId("6357528f07cd932759afb1e0"), "item" : "journal", "qty" : 25, "size" : { "h" : 14, "w" : 21, "uom" : "cm" }, "status" : "A" }
{ "_id" : ObjectId("6357528f07cd932759afb1e3"), "item" : "planner", "qty" : 75, "size" : { "h" : 22.85, "w" : 30, "uom" : "cm" }, "status" : "D" }
6.2.9 查询数组

元数据:

db.inventory.insertMany([
   { item: "journal", qty: 25, tags: ["blank", "red"], dim_cm: [ 14, 21 ] },
   { item: "notebook", qty: 50, tags: ["red", "blank"], dim_cm: [ 14, 21 ] },
   { item: "paper", qty: 100, tags: ["red", "blank", "plain"], dim_cm: [ 14, 21 ] },
   { item: "planner", qty: 75, tags: ["blank", "red"], dim_cm: [ 22.85, 30 ] },
   { item: "postcard", qty: 45, tags: ["blue"], dim_cm: [ 10, 15.25 ] }
]);

匹配一个数组

匹配整个数组可以使用key: array的方式来查询

myrs:PRIMARY> db.inventory.find({tags: ["red", "blank"]})
{ "_id" : ObjectId("6357539407cd932759afb1e6"), "item" : "notebook", "qty" : 50, "tags" : [ "red", "blank" ], "dim_cm" : [ 14, 21 ] }

查找包含数组中任意元素的文档,不考虑数组中的顺序

如果要查找包含数组中任意元素的文档,而不考虑数组中的顺序,需要使用$all运算符

myrs:PRIMARY> db.inventory.find({tags: {$all:["red", "blank"]}})
{ "_id" : ObjectId("6357539407cd932759afb1e5"), "item" : "journal", "qty" : 25, "tags" : [ "blank", "red" ], "dim_cm" : [ 14, 21 ] }
{ "_id" : ObjectId("6357539407cd932759afb1e6"), "item" : "notebook", "qty" : 50, "tags" : [ "red", "blank" ], "dim_cm" : [ 14, 21 ] }
{ "_id" : ObjectId("6357539407cd932759afb1e7"), "item" : "paper", "qty" : 100, "tags" : [ "red", "blank", "plain" ], "dim_cm" : [ 14, 21 ] }
{ "_id" : ObjectId("6357539407cd932759afb1e8"), "item" : "planner", "qty" : 75, "tags" : [ "blank", "red" ], "dim_cm" : [ 22.85, 30 ] }

查询数组中的元素

查询数组字段中是否至少包含一个具有指定值的元素:

示例:查找tags中包含red的文档

myrs:PRIMARY> db.inventory.find({tags: "red"})
{ "_id" : ObjectId("6357539407cd932759afb1e5"), "item" : "journal", "qty" : 25, "tags" : [ "blank", "red" ], "dim_cm" : [ 14, 21 ] }
{ "_id" : ObjectId("6357539407cd932759afb1e6"), "item" : "notebook", "qty" : 50, "tags" : [ "red", "blank" ], "dim_cm" : [ 14, 21 ] }
{ "_id" : ObjectId("6357539407cd932759afb1e7"), "item" : "paper", "qty" : 100, "tags" : [ "red", "blank", "plain" ], "dim_cm" : [ 14, 21 ] }
{ "_id" : ObjectId("6357539407cd932759afb1e8"), "item" : "planner", "qty" : 75, "tags" : [ "blank", "red" ], "dim_cm" : [ 22.85, 30 ] }

要指定数组字段中元素的条件,需要在查询筛选器文档中使用查询运算符:

示例:

myrs:PRIMARY> db.inventory.find({dim_cm:{$gt: 21}})
{ "_id" : ObjectId("6357539407cd932759afb1e8"), "item" : "planner", "qty" : 75, "tags" : [ "blank", "red" ], "dim_cm" : [ 22.85, 30 ] }

为数组元素指定多个条件

使用数组元素上的复合过滤条件查询数组

下面的示例查询文档,其中dim_cm数组包含在某种组合中满足查询条件的元素; 例如,一个元素可以满足大于15的条件,另一个元素能够满足小于20的条件,或者单个元素可以满足这两个条件:

myrs:PRIMARY> db.inventory.find({dim_cm:{$gt: 15, $lt: 20}})
{ "_id" : ObjectId("6357539407cd932759afb1e5"), "item" : "journal", "qty" : 25, "tags" : [ "blank", "red" ], "dim_cm" : [ 14, 21 ] }
{ "_id" : ObjectId("6357539407cd932759afb1e6"), "item" : "notebook", "qty" : 50, "tags" : [ "red", "blank" ], "dim_cm" : [ 14, 21 ] }
{ "_id" : ObjectId("6357539407cd932759afb1e7"), "item" : "paper", "qty" : 100, "tags" : [ "red", "blank", "plain" ], "dim_cm" : [ 14, 21 ] }
{ "_id" : ObjectId("6357539407cd932759afb1e9"), "item" : "postcard", "qty" : 45, "tags" : [ "blue" ], "dim_cm" : [ 10, 15.25 ] }

查询满足多个条件的数组元素

使用$elemMatch运算符在数组元素上指定多个条件,以便至少有一个数组元素满足所有指定的条件。

以下示例查询dim_cm数组包含至少一个大于( g t ) 22 且小于( gt)22且小于( gt22且小于(lt)30的元素的文档:

myrs:PRIMARY> db.inventory.find({dim_cm: {$elemMatch: {$gt:22, $lt: 30}}})
{ "_id" : ObjectId("6357539407cd932759afb1e8"), "item" : "planner", "qty" : 75, "tags" : [ "blank", "red" ], "dim_cm" : [ 22.85, 30 ] }

通过数组下标来查询元素

通过数组下标来查询元素可以使用.下标的方式

示例:查询dim_cm数组中第二个元素大于25的文档

myrs:PRIMARY> db.inventory.find({"dim_cm.1": {$gt: 25}})
{ "_id" : ObjectId("6357539407cd932759afb1e8"), "item" : "planner", "qty" : 75, "tags" : [ "blank", "red" ], "dim_cm" : [ 22.85, 30 ] }

通过数组长度查询

通过数组长度查询需要使用$size操作符

示例:查询tags数组大小为3的文档

myrs:PRIMARY> db.inventory.find({tags: {$size: 3}})
{ "_id" : ObjectId("6357539407cd932759afb1e7"), "item" : "paper", "qty" : 100, "tags" : [ "red", "blank", "plain" ], "dim_cm" : [ 14, 21 ] }
6.2.10 查询集合中文档数量

查询集合中文档的数量使用count()方法,语法如下:

db.collection.count(<filter>)

示例:

myrs:PRIMARY> db.user.count()
7
myrs:PRIMARY> db.user.count({"username": "zhangsan"})
2
6.2.11 查询文档Golang操作
// 查询一个文档
func FindOne(collection *mongo.Collection) {
	if result := collection.FindOne(context.TODO(), bson.D{{"userid", 1}} /*, opts*/); result.Err() != nil {
		fmt.Println("find err:", result.Err())
	} else {
		var m ChatMessage
		result.Decode(&m)
		fmtPrint(&m)
	}

}

// 查询多个文档
func FindMany(collection *mongo.Collection) {
    // 设置排序规则,根据userid升序排序,1:升序 -1:降序
	opts := options.Find().SetSort(bson.D{{"userid", 1}})
	cursor, err := collection.Find(context.TODO(), bson.D{}, opts)
	if err != nil {
		fmt.Println("Find error", err)
		return
	}
	var ms []ChatMessage
	if err := cursor.All(context.TODO(), &ms); err != nil {
		fmt.Println("decode error:", err)
		return
	}
	for i := 0; i < len(ms); i++ {
		fmtPrint(&ms[i])
	}
}

// 也可以使用SetProjection来设置要查询的字段
opts := options.FindOne().SetProjection(bson.D{{"message", 1}})
collection.FindOne(context.TODO(), bson.D{{"userid", 1}}, opts)

// 分页
func Pagination(collection *mongo.Collection, pageNum, pageSize int) {
	if pageNum < 1 {
		pageNum = 1
	}
	if pageSize < 1 {
		pageSize = 5
	}
	skip := (pageNum - 1) * pageSize
	opts := options.Find().SetLimit(int64(pageSize)).SetSkip(int64(skip))
	cursor, err := collection.Find(context.TODO(), bson.D{}, opts)
	if err != nil {
		fmt.Println("Find error", err)
		return
	}
	var ms []ChatMessage
	if err := cursor.All(context.TODO(), &ms); err != nil {
		fmt.Println("decode error:", err)
		return
	}
	for i := 0; i < len(ms); i++ {
		fmtPrint(&ms[i])
	}
}

 

6.3 更新文档

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

db.collection.update(
	<filter>,
    <update>,
    {
    	upsert: <boolean>
    	multi: <boolean>,
    	writeConcern: <document>
    }
)

参数如下:

  • filter:更新的过滤条件,也就是更新哪些文档,类似于mysql的where字句
  • update:update的对象和一些更新的操作符(如 , , ,inc等),也可以理解为mysql的set字句
  • upsert:这个参数的意思是,如果不存在update的记录,是否插入新的记录
  • multi:是否更新多条记录,默认只更新一条
  • writeConcern:抛出异常的级别

示例:更新李四的密码

myrs:PRIMARY> db.user.update({username: "lisi"},{$set: {password: "lisi888"}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

myrs:PRIMARY> db.user.find()
{ "_id" : ObjectId("63553cbbec927a5603e2b31c"), "username" : "zhangsan", "password" : "root123" }
{ "_id" : ObjectId("63553d65ec927a5603e2b31d"), "username" : "lisi", "password" : "lisi888" }

如果不使用$set指令的话,就会更新整个文档为指定的内容:

myrs:PRIMARY> db.user.update({username: "lisi"},{password: "lisi888"})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

myrs:PRIMARY> db.user.find()
{ "_id" : ObjectId("63553cbbec927a5603e2b31c"), "username" : "zhangsan", "password" : "root123" }
{ "_id" : ObjectId("63553d65ec927a5603e2b31d"), "password" : "lisi888" }
更新文档Golang操作
func UpdateOne(collection *mongo.Collection) {
	result, err := collection.UpdateOne(context.TODO(), bson.D{{"userid", 1}},
		bson.D{{"$set", bson.D{{"message", "test update"}}}})
	if err != nil {
		fmt.Println("update error:", err)
		return
	}
	fmt.Println(result.ModifiedCount)
}

 

6.4 删除文档

remove()方法用于删除文档,语法如下:

db.collection.remove(
   <filter>,
   {
     justOne: <boolean>,
     writeConcern: <document>
   }
)

参数如下:

  • filter :(可选)删除的文档的过滤条件。
  • justOne : (可选)如果设为 true 或 1,则只删除一个文档。
  • writeConcern :(可选)抛出异常的级别。

示例:删除所有文档

myrs:PRIMARY> db.user.remove({})
WriteResult({ "nRemoved" : 4 })

myrs:PRIMARY> db.user.find()
myrs:PRIMARY>

删除用户名为zhangsan的文档:

myrs:PRIMARY> db.user.remove({username: "zhangsan"})
WriteResult({ "nRemoved" : 1 })
myrs:PRIMARY> db.user.find()
{ "_id" : ObjectId("63553d65ec927a5603e2b31d"), "username" : "lisi", "password" : "lisi888" }
{ "_id" : ObjectId("6355411c5035c8ac0c11e0f2"), "username" : "laowang", "password" : "aabb" }
{ "_id" : ObjectId("6355412a5035c8ac0c11e0f3"), "username" : "laoliu", "password" : "ccdd" }
删除文档Golang操作
// 删除所有文档
func RemoveAll(collection *mongo.Collection) {
	if result, err := collection.DeleteMany(context.TODO(), bson.D{}); err != nil {
		fmt.Println("delete all error", err)
	} else {
		fmt.Println(result.DeletedCount)
	}
}

// 删除指定文档
func Remove(collection *mongo.Collection) {
	if result, err := collection.DeleteOne(context.TODO(), bson.D{{"userid", 1}}); err != nil {
		fmt.Println("delete error:", err)
	} else {
		fmt.Println(result.DeletedCount)
	}
}

 

7、索引

创建索引能够支持高效率的查询,如果没有索引,那么mongodb就需要进行全表扫描,扫描集合中的所有文档来选择匹配查询条件的文档,效率非常底下。在创建了索引后,如果一个查询可以用的到索引,那么mongodb就可以使用索引来进行查询了。

索引是一种特殊的数据结构,它以易于遍历的形式存储集合数据的一小部分。索引存储特定字段或字段集的值,按字段值排序。索引项的排序支持有效的相等匹配和基于范围的查询操作。此外,mongodb可以通过使用索引中的排序结果返回排序结果。

下图是使用索引选择和排序匹配文档的查询:在对score字段建立了索引后,在使用score进行查询和排序时就可以使用到索引了。

mongodb学习笔记_第7张图片

mongodb中每个collection都有一个_id字段,如果自己没有指定,那么系统默认会创建,并且这个_id默认是会创建索引的。

使用下面命令来查询collection的索引:

db.collection.getIndexes()

示例:查看user集合的索引

myrs:PRIMARY> db.user.getIndexes()
[
        {
                "v" : 2,
                "key" : {
                        "_id" : 1
                },
                "name" : "_id_"
        },
        {
                "v" : 2,
                "key" : {
                        "username" : 1
                },
                "name" : "username_1"
        }
]

7.1 创建索引

使用createIndex()方法来创建一个索引,语法如下:

db.collection.createIndex(<key and index type specification>, <options>)

示例:创建一个升序索引

myrs:PRIMARY> db.user.createIndex({username: 1})
{
        "numIndexesBefore" : 1,
        "numIndexesAfter" : 2,
        "createdCollectionAutomatically" : false,
        "commitQuorum" : "votingMembers",
        "ok" : 1,
        "$clusterTime" : {
                "clusterTime" : Timestamp(1666579024, 3),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        },
        "operationTime" : Timestamp(1666579024, 3)
}

创建索引时,val的值为1或-1, 1为创建升序索引, -1为创建降序索引

指定索引名字

如果不指定索引名字,默认的索引名为字段名_排序规则, 例如 username_1 或 username_-1

可以在optinos选项中来指定索引名字。

myrs:PRIMARY> db.user.createIndex({password: -1}, {name: "passwd_index"})
{
        "numIndexesBefore" : 2,
        "numIndexesAfter" : 3,
        "createdCollectionAutomatically" : false,
        "commitQuorum" : "votingMembers",
        "ok" : 1,
        "$clusterTime" : {
                "clusterTime" : Timestamp(1666580358, 7),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        },
        "operationTime" : Timestamp(1666580358, 7)
}

 

7.2 索引类型

mongodb提供了多种不同类型的索引来支持特殊类型的数据查询。

7.2.1 单字段索引

除了mongodb定义的_id索引外,mongodb支持在文档的单个字段上创建用户自定义的升序/降序索引。在7.1节中创建的索引都为单字段索引。

mongodb学习笔记_第8张图片

对于单个字段的索引和排序操作,索引键的排序规则(即升序或降序)并不重要,因为mongodb可以在任意方向遍历索引。

 

7.2.2 复合索引

除了单字段索引,mongodb也支持在多个字段上创建复合索引。

示例:创建username和password的联合索引

myrs:PRIMARY> db.user.createIndex({username: 1, password: 1})
{
        "numIndexesBefore" : 1,
        "numIndexesAfter" : 2,
        "createdCollectionAutomatically" : false,
        "commitQuorum" : "votingMembers",
        "ok" : 1,
        "$clusterTime" : {
                "clusterTime" : Timestamp(1666580833, 7),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        },
        "operationTime" : Timestamp(1666580833, 7)
}
myrs:PRIMARY> db.user.getIndexes()
[
        {
                "v" : 2,
                "key" : {
                        "_id" : 1
                },
                "name" : "_id_"
        },
        {
                "v" : 2,
                "key" : {
                        "username" : 1,
                        "password" : 1
                },
                "name" : "username_1_password_1"
        }
]

符合索引中列出的字段顺序是具有重要意义的。例如一个符合索引由{userid: 1, score: 1}组成,则该索引会首先按userid来进行排序,然后在每个userid值内按score排序。

mongodb学习笔记_第9张图片

 

复合索引除了支持所有索引字段的查询外,还可以支持匹配索引字段的前缀查询。

索引前缀

索引前缀是索引字段的开始子集。例如下面的复合索引:

{ item: 1, location: 1, stock: 1 }

这个索引有下面的索引前缀:

  • {item: 1}
  • {item: 1, location: 1}

对于一个复合索引,mongodb支持全索引字段查询和索引前缀查询

对于{item: 1, stock: 1}来说,由于item字段对应于前缀,因此也可以使用到索引,但是只能使用到item索引,索引的效率就不会那么高。

 

比如在user集合中给username和password建立了复合索引,那么在查询时使用全索引字段的查询:{username: “lisi”, passowrd: “aabb”} 或前缀索引: {username: “lisi”} 时都可以使用到索引。但是{password: “aabb”}这样的查询就不能使用索引。

使用explain()可以查看查询时使用到的索引:

示例1:使用全索引字段查询

myrs:PRIMARY> db.user.find({username:"laoliu", password: "ccdd"}).explain()
{
        ...
                "winningPlan" : {
                        "stage" : "FETCH",
                        "inputStage" : {
                                "stage" : "IXSCAN",       # INDEX SCAN使用到了index
                                "keyPattern" : {
                                        "username" : 1,
                                        "password" : 1
                                },
                                "indexName" : "username_1_password_1",
       ...
}

示例2:前缀索引

myrs:PRIMARY> db.user.find({username:"laoliu"}).explain()
{
        ...
                "winningPlan" : {
                        "stage" : "FETCH",
                        "inputStage" : {
                                "stage" : "IXSCAN",    # 使用到了index
                                "keyPattern" : {
                                        "username" : 1,
                                        "password" : 1
                                },
                                "indexName" : "username_1_password_1",
       ...
}

示例3:不使用全索引和前缀索引

myrs:PRIMARY> db.user.find({password: "ccdd"}).explain()
{
	...
                "winningPlan" : {
                        "stage" : "COLLSCAN",     # COLLECTION SCAN 没有用到索引,全集合扫描
                        "filter" : {
                                "password" : {
                                        "$eq" : "ccdd"
                                }
                        },
                        "direction" : "forward"
                },
                "rejectedPlans" : [ ]
        },
       ...
}

 

对于单字段索引来说,建的排序顺序并不重要,因为mongodb可以在任意方向遍历索引,但是,对于复合索引来说,排序顺序在确定索引是否支持排序操作时可能很重要

例如:对于一个集合,其中包含用户名和日期字段的文档。对于下面的查询

db.user.find().sort({username: 1, date: -1})
或
db.user.find().sort({username: -1, date: 1})

下面的索引是有效的:

db.user.createIndex({username: 1, date: -1})

然而,上面的索引不支持按照username升序排序以及date升序排序

db.user.find().sort({username: 1, date: 1})

 

7.3 多键索引

mongodb是一个文档型数据库,在文档中是可以包含数组的。因此为了索引包含数组的字段,mongodb为数组中的每个元素创建一个索引键。这些多键索引支持对数组字段的高效查询。可以在包含标量值(例如字符串、数字)和嵌套文档的数组上构造多键索引。
mongodb学习笔记_第10张图片

创建多键索引的语法如下:

db.collection.createIndex({ <field> : <1 or -1>})

mongodb会自动创建多键索引如果一个索引字段是一个数组,因此不需要特别指明多键的类型。

复合多键索引

mongodb在创建复合多键索引时最多只能包含一个多键索引,也就是说复合索引中的字段最多只能有一个是数组。

比如,下面的文档中,不能同时对a和b创建复合索引。

{ _id: 1, a: [ 1, 2 ], b: [ 1, 2 ], category: "AB - both arrays" }

 

7.4 全文索引

mongodb的全文索引可以包含其值为字符串或字符串元素数组的任何字段。一个集合只能有一个全文索引,但该索引可以覆盖多个字段。

7.4.1 创建全文索引

创建全文索引的语法如下:

db.collection.createIndex({<field>: "text"})

示例:创建全文索引并查看

myrs:PRIMARY> db.user.find()
{ "_id" : ObjectId("63553d65ec927a5603e2b31d"), "username" : "lisi", "password" : "lisi888", "hobbies" : [ "sing", "jump", "rap", "basketball" ] }
{ "_id" : ObjectId("6355411c5035c8ac0c11e0f2"), "username" : "laowang", "password" : "aabb", "hobbies" : [ "sing", "jump", "rap", "basketball" ] }
{ "_id" : ObjectId("6355412a5035c8ac0c11e0f3"), "username" : "laoliu", "password" : "ccdd", "hobbies" : [ "sing", "jump", "rap", "basketball" ] }
{ "_id" : ObjectId("63553cbbec927a5603e2b31c"), "username" : "zhangsan", "password" : "root123", "hobbies" : [ "sing", "jump", "rap", "basketball" ] }
{ "_id" : ObjectId("6356444707cd932759afb1d5"), "username" : "zhangsan", "password" : "rroot123", "hobbies" : [ "sing", "jump", "rap", "basketball" ] }
{ "_id" : ObjectId("6356445a07cd932759afb1d6"), "username" : "zhangsan", "password" : "root123", "hobbies" : [ "sing", "jump", "rap", "basketball" ] }

myrs:PRIMARY> db.user.createIndex({hobbies:"text"})
{
        "numIndexesBefore" : 2,
        "numIndexesAfter" : 3,
        "createdCollectionAutomatically" : false,
        "commitQuorum" : "votingMembers",
        "ok" : 1,
        "$clusterTime" : {
                "clusterTime" : Timestamp(1666598708, 7),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        },
        "operationTime" : Timestamp(1666598708, 7)
}

myrs:PRIMARY> db.user.getIndexes()
[
        {
                "v" : 2,
                "key" : {
                        "_fts" : "text",
                        "_ftsx" : 1
                },
                "name" : "hobbies_text",
                "weights" : {
                        "hobbies" : 1
                },
                "default_language" : "english",
                "language_override" : "language",
                "textIndexVersion" : 3
        }
]

也可以在多个字段上创建全文索引,例如:

db.reviews.createIndex(
   {
     subject: "text",
     comments: "text"
   }
 )
7.4.2 为全文索引指定语言

与索引数据相关联的默认语言确定了解析词根(即词干)和忽略停止词的规则。索引数据的默认语言为英语。要指定其他语言,在创建文本索引时可以使用default_language选项。

示例:创建全文索引并指定语言

db.quotes.createIndex(
   { content : "text" },
   { default_language: "spanish" }
)

好像并不支持中文…

 

7.5 索引属性

7.5.1 TTL index

(1) 创建TTL 索引

TTL index是特殊的单字段索引,mingodb可以使用它在一定时间或特定时钟时间后自动从集合中删除文档,数据过期对于某些类型的信息很有用,例如机器生成的事件数据、日志和会话信息,这些信息只需要在数据库中持续有限的时间。

创建TTL index语法:

db.collection.createIndex({<field>: <1 or -1>}, {expireAfterSecondes: <duration>})

(2) 将非TTL索引字段改为TTL索引

从5.1版本之后可以将expireAfterSeconds选项添加到现有的单字段索引中。

语法如下:

db.runCommand({
    "collMod": <collName>,
    "index": {
    	"keyPattern": <keyPattern>,
    	"expireAfterSeconds": <number>
	}
})

例如:

db.runCommand({
  "collMod": "tickets",
  "index": {
    "keyPattern": { "lastModifiedDate": 1 },
    "expireAfterSeconds": 100
  }
})

除了在没有TTL索引的字段上修改,也可以通过该方式来修改过期时间。

(3)数据过期的行为

TTL索引在自索引字段值起经过指定秒数后过期文档;到期阈值是索引字段值加上指定的秒数。

  • 如果字段是一个数组,并且索引中有多个日期值,MongoDB将使用数组中的最低(即最早)日期值来计算到期阈值。
  • 如果文档中的索引字段不是日期或包含一个或多个日期值的数组,则文档不会过期。
  • 如果文档不包含索引字段,则该文档不会过期。

数据删除操作

mongodb中的后台线程会读取索引中的值,并从集合中删除过期的文档。

一旦索引在主数据库上创建成功,mongodb就开始删除过期的文档。TTL索引不保证过期数据在过期后会被立即删除,文档到期时间与mongodb从数据库中删除文档的时间存在延迟。因为负责删除文档的后台线程每60s运行一次。由于删除操作的持续时间取决于mongod实例的工作负载,因此过期数据可能会在后台任务运行之间的60秒时间段之外存在一段时间。

注意:

  • TTL索引是单字段索引,复合索引不支持TTL索引。
  • _id字段不支持TTL索引
  • 不能再固定大小的集合上创建TTL索引
  • 不能使用createIndex更改已有的expireAfterSeconds值,可以使用collMod数据库命令
  • 如果一个字段已经存在TTL索引,则无法在同一字段上再创建TTL索引
7.5.2 唯一索引(unique index)

唯一索引确保索引字段不会存储重复值;强制索引字段的唯一性。默认情况下,MongoDB在创建集合期间在_id字段上创建唯一索引。

创建唯一索引语法:

db.collection.createIndex( <key and index type specification>, { unique: true } )

唯一索引又分为单字段唯一索引和复合唯一索引。

单字段唯一索引的字段中不能有重复的值。

(1) 单字段唯一约束

示例:创建单字段唯一索引

# 给userid字段创建唯一索引
myrs:PRIMARY> db.user.createIndex({userid:1},{unique:true})
{
        "numIndexesBefore" : 3,
        "numIndexesAfter" : 4,
        "createdCollectionAutomatically" : false,
        "commitQuorum" : "votingMembers",
        "ok" : 1,
        "$clusterTime" : {
                "clusterTime" : Timestamp(1666601824, 7),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        },
        "operationTime" : Timestamp(1666601824, 7)
}

myrs:PRIMARY> db.user.find()
{ "_id" : ObjectId("63553d65ec927a5603e2b31d"), "username" : "lisi", "password" : "lisi888", "hobbies" : [ "sing", "jump", "rap", "basketball" ], "createtime" : "Mon Oct 24 2022 08:42:24 GMT+0000 (UTC)", "userid" : 1 }
{ "_id" : ObjectId("6355411c5035c8ac0c11e0f2"), "username" : "laowang", "password" : "aabb", "hobbies" : [ "sing", "jump", "rap", "basketball" ], "createtime" : "Mon Oct 24 2022 08:42:24 GMT+0000 (UTC)", "userid" : 2 }
{ "_id" : ObjectId("6355412a5035c8ac0c11e0f3"), "username" : "laoliu", "password" : "ccdd", "hobbies" : [ "sing", "jump", "rap", "basketball" ], "createtime" : "Mon Oct 24 2022 08:42:24 GMT+0000 (UTC)", "userid" : 3 }
{ "_id" : ObjectId("63553cbbec927a5603e2b31c"), "username" : "zhangsan", "password" : "root123", "hobbies" : [ "sing", "jump", "rap", "basketball" ], "createtime" : "Mon Oct 24 2022 08:42:24 GMT+0000 (UTC)", "userid" : 4 }
{ "_id" : ObjectId("6356444707cd932759afb1d5"), "username" : "zhangsan", "password" : "rroot123", "hobbies" : [ "sing", "jump", "rap", "basketball" ], "createtime" : "Mon Oct 24 2022 08:42:24 GMT+0000 (UTC)", "userid" : 5 }
{ "_id" : ObjectId("6356445a07cd932759afb1d6"), "username" : "zhangsan", "password" : "root123", "hobbies" : [ "sing", "jump", "rap", "basketball" ], "createtime" : "Mon Oct 24 2022 08:42:24 GMT+0000 (UTC)", "userid" : 6 }
# 查看索引
myrs:PRIMARY> db.user.getIndexes()
[
        {
                "v" : 2,
                "key" : {
                        "userid" : 1
                },
                "name" : "userid_1",
                "unique" : true
        }
]
# 插入userid相同的记录,插入失败
myrs:PRIMARY> db.user.insert({username:"zhangsan", userid: 5})
WriteResult({
        "nInserted" : 0,
        "writeError" : {
                "code" : 11000,
                "errmsg" : "E11000 duplicate key error collection: test.user index: userid_1 dup key: { userid: 5.0 }"
        }
})

(2) 复合字段唯一约束

也可以为复合索引创建唯一约束,如果对复合索引使用唯一约束,那么mongodb将对索引键值的组合强制唯一性

示例:给username和password创建复合索引,并使用唯一约束

# 创建索引
myrs:PRIMARY> db.user.createIndex({username: 1, password: 1}, {unique: true})
{
        "numIndexesBefore" : 3,
        "numIndexesAfter" : 4,
        "createdCollectionAutomatically" : false,
        "commitQuorum" : "votingMembers",
        "ok" : 1,
        "$clusterTime" : {
                "clusterTime" : Timestamp(1666602549, 7),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        },
        "operationTime" : Timestamp(1666602549, 7)
}

myrs:PRIMARY> db.user.find()
{ "_id" : ObjectId("63553d65ec927a5603e2b31d"), "username" : "lisi", "password" : "lisi888", "hobbies" : [ "sing", "jump", "rap", "basketball" ], "createtime" : "Mon Oct 24 2022 08:42:24 GMT+0000 (UTC)", "userid" : 1 }
{ "_id" : ObjectId("6355411c5035c8ac0c11e0f2"), "username" : "laowang", "password" : "aabb", "hobbies" : [ "sing", "jump", "rap", "basketball" ], "createtime" : "Mon Oct 24 2022 08:42:24 GMT+0000 (UTC)", "userid" : 2 }
{ "_id" : ObjectId("6355412a5035c8ac0c11e0f3"), "username" : "laoliu", "password" : "ccdd", "hobbies" : [ "sing", "jump", "rap", "basketball" ], "createtime" : "Mon Oct 24 2022 08:42:24 GMT+0000 (UTC)", "userid" : 3 }
{ "_id" : ObjectId("63553cbbec927a5603e2b31c"), "username" : "zhangsan", "password" : "root123", "hobbies" : [ "sing", "jump", "rap", "basketball" ], "createtime" : "Mon Oct 24 2022 08:42:24 GMT+0000 (UTC)", "userid" : 4 }
{ "_id" : ObjectId("6356444707cd932759afb1d5"), "username" : "zhangsan", "password" : "rroot123", "hobbies" : [ "sing", "jump", "rap", "basketball" ], "createtime" : "Mon Oct 24 2022 08:42:24 GMT+0000 (UTC)", "userid" : 5 }
{ "_id" : ObjectId("635653c707cd932759afb1d8"), "username" : "zhangsan, userid: 5" }

myrs:PRIMARY> db.user.insert({username: "zhangsan", password: "root123", userid: 7})
WriteResult({
        "nInserted" : 0,
        "writeError" : {
                "code" : 11000,
                "errmsg" : "E11000 duplicate key error collection: test.user index: username_1_password_1 dup key: { username: \"zhangsan\", password: \"root123\" }"
        }
})
7.5.3 隐藏索引

隐藏索引对查询规划器不可见,不能用于支持查询。

通过向计划器隐藏索引,用户可以评估删除索引的潜在影响,而无需实际删除索引。如果影响是负面的,用户可以取消隐藏索引,而不必重新创建删除的索引。

注意:

  • 如果隐藏索引是唯一索引,则索引仍将其唯一约束应用于文档
  • 如果隐藏索引是TTL索引,则该索引仍会使文档过期
  • 隐藏索引包含在listIndex和db.collection.getIndexes()的结果中
  • 隐藏索引在对集合进行写入操作时更新,并继续消耗磁盘空间和内存。因此,它们包含在各种统计操作中,例如db.collection.stats()和$indexStats

创建隐藏索引

语法如下:

db.user.createIndex(
   { userid: 1 },
   { hidden: true }
);

可以通过db.collection.getIndexes()来查看隐藏索引

myrs:PRIMARY> db.user.getIndexes()
[
	...
        {
                "v" : 2,
                "key" : {
                        "userid" : 1
                },
                "name" : "userid_1",
                "unique" : true,
                "hidden" : true
        },
        
]

隐藏一个已存在的索引

隐藏一个已存在的索引可以使用hideIndex()方法:

db.collection.hideIndex({userid: 1})
or
db.collection.hideIndex("userid_1")

示例:

myrs:PRIMARY> db.user.hideIndex("userid_1")
{
        "hidden_old" : false,
        "hidden_new" : true,
        "ok" : 1,
        "$clusterTime" : {
                "clusterTime" : Timestamp(1666666183, 1),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        },
        "operationTime" : Timestamp(1666666183, 1)
}
myrs:PRIMARY> db.user.getIndexes()
[
	...
        {
                "v" : 2,
                "key" : {
                        "userid" : 1
                },
                "name" : "userid_1",
                "unique" : true,
                "hidden" : true
        },
]

取消隐藏现有索引

取消隐藏现有索引可以使用unhideIndex()方法:

db.collcetion.unhideIndex({userid: 1})
or 
db.collection.unhideIndex("userid_1")

 

7.6 删除索引

删除集合所有索引:

db.collection.dropIndexes()

删除集合某个索引:

db.collection.dropIndex("indexName")

 

8、聚合

聚合操作处理多个文档并返回计算结果,使用聚合操作可以实现如下操作:

  • 将多个文档中的值分组在一起
  • 对分组数据执行操作并返回单个结果
  • 分析数据随时间的变化

要使用聚合操作,有下面两个方式:

  • 聚合管道(Aggregation Pipeline),这是执行聚合的首选方式。
  • 单一用途聚合方法,比如count(),它们很简单,但是缺乏聚合管道的能力。

 

8.1 聚合管道(Aggregation Pipeline)

聚合管道就像是linux的管道一样,前一个命令的输出可以通过 |作为后一个命令的输入,最终输出多个命令处理后的结果。

集合管道由一个或多个处理文档的阶段组成

  1. 每个阶段对输入文档执行操作。例如:过滤文档、对文档进行分组和计算值。
  2. 从一个阶段输出的文档将传递到下一阶段。
  3. 聚合管道可以返回文档组的结果。例如:返回总值、平均值、最大值和最小值。

聚合管道使用aggregation()方法,语法如下:

db.collection.aggregate([stage1, stage2, ..., stagen])

聚合管道示例:

# 创建数据
db.orders.insertMany( [
   { _id: 0, name: "Pepperoni", size: "small", price: 19,
     quantity: 10, date: ISODate( "2021-03-13T08:14:30Z" ) },
   { _id: 1, name: "Pepperoni", size: "medium", price: 20,
     quantity: 20, date : ISODate( "2021-03-13T09:13:24Z" ) },
   { _id: 2, name: "Pepperoni", size: "large", price: 21,
     quantity: 30, date : ISODate( "2021-03-17T09:22:12Z" ) },
   { _id: 3, name: "Cheese", size: "small", price: 12,
     quantity: 15, date : ISODate( "2021-03-13T11:21:39.736Z" ) },
   { _id: 4, name: "Cheese", size: "medium", price: 13,
     quantity:50, date : ISODate( "2022-01-12T21:23:13.331Z" ) },
   { _id: 5, name: "Cheese", size: "large", price: 14,
     quantity: 10, date : ISODate( "2022-01-12T05:08:13Z" ) },
   { _id: 6, name: "Vegan", size: "small", price: 17,
     quantity: 10, date : ISODate( "2021-01-13T05:08:13Z" ) },
   { _id: 7, name: "Vegan", size: "medium", price: 18,
     quantity: 10, date : ISODate( "2021-01-13T05:10:13Z" ) }
] )

# 执行聚合操作
db.orders.aggregate( [

   // Stage 1: 过滤披萨订单通过size匹配
   {
      $match: { size: "medium" }
   },

   // Stage 2:通过披萨name为stage1的文档进行分组并计算总数量 
   {
      $group: { _id: "$name", totalQuantity: { $sum: "$quantity" } }
   }

] )

# 输出如下
[
   { _id: 'Cheese', totalQuantity: 50 },
   { _id: 'Vegan', totalQuantity: 10 },
   { _id: 'Pepperoni', totalQuantity: 20 }
]

在上面的聚合操作中有两个阶段:

阶段1($match阶段):

  • 将披萨订单按照size筛选为中等(medium)大小的披萨
  • 将剩余文档传递到$group阶段

阶段2($group阶段):

  • 通过披萨name对剩余文档进行分组
  • 使用$sum来为每个不同name的披萨订单计算总的订单数量。总计存储在聚合管道返回的totalQuantity字段中。

示例:计算总订单价值和平均订单数量

db.orders.aggregate( [

   // Stage 1: 通过日期范围过滤披萨订单文档
   {
      $match:
      {
         "date": { $gte: new ISODate( "2020-01-30" ), $lt: new ISODate( "2022-01-30" ) }
      }
   },

   // Stage 2: 通过日期对剩余文档分组并计算结果 
   {
      $group:
      {
         _id: { $dateToString: { format: "%Y-%m-%d", date: "$date" } },
         totalOrderValue: { $sum: { $multiply: [ "$price", "$quantity" ] } },
         averageOrderQuantity: { $avg: "$quantity" }
      }
   },

   // Stage 3: 通过totalOrderValue以递减顺序排序
   {
      $sort: { totalOrderValue: -1 }
   }

 ] )
 
 # 输出结果
 [
   { _id: '2022-01-12', totalOrderValue: 790, averageOrderQuantity: 30 },
   { _id: '2021-03-13', totalOrderValue: 770, averageOrderQuantity: 15 },
   { _id: '2021-03-17', totalOrderValue: 630, averageOrderQuantity: 30 },
   { _id: '2021-01-13', totalOrderValue: 350, averageOrderQuantity: 10 }
]

在上面的聚合操作中有三个阶段:

$match阶段:

  • 将披萨订单文档筛选为使用$gte和$lt指定的日期范围内的文档

  • 将剩余文档传递到$group阶段。

$group阶段:

  • 使用$dateToString按日期对文档进行分组

  • 对于每个组,计算:

    • 使用$sum和$multiply的总订单值

    • 使用$avg的平均订单数量

  • 将分组的文档传递到$sort阶段。

$sort阶段:

  • 按每个组的总顺序值按降序对文档进行排序(-1)。

  • 返回已排序的文档。

 

8.2 聚合管道阶段

Stage Description
$addFields 向文档添加新字段。与$project类似,$addFields会重塑流中的每个文档;具体来说,通过向包含输入文档中的现有字段和新添加字段的输出文档添加新字段。
$set是$addFields的别名
$count 返回聚合管道此阶段的文档数计数。与$count聚合累加器不同。
$group 按指定的标识符表达式对输入文档进行分组,并将累加器表达式(如果指定)应用于每个组。使用所有输入文档,并为每个不同的组输出一个文档。输出文档仅包含标识符字段和累积字段(如果指定)。
$limit 将未修改的前n个文档传递到管道,其中n是指定的限制。对于每个输入文档,输出一个文档(对于前n个文档)或零个文档(在前n个文件之后)。
$match 过滤文档流以仅允许匹配的文档未经修改地传递到下一个管道阶段$match使用标准MongoDB查询。对于每个输入文档,输出一个文档(匹配)或零个文档(不匹配)。
$merge 将聚合管道的结果文档写入集合。该阶段可以将结果合并到输出集合中(插入新文档、合并文档、替换文档、保留现有文档、操作失败、使用自定义更新管道处理文档)。要使用$merge阶段,它必须是管道中的最后一个阶段。
$out 将聚合管道的结果文档写入集合。要使用$out阶段,它必须是管道中的最后一个阶段。
$project 重新塑造流中的每个文档,例如添加新字段或删除现有字段。对于每个输入文档,输出一个文档。
$set 向文档添加新字段。与$project类似,$set会重塑流中的每个文档;具体来说,通过向包含输入文档中的现有字段和新添加字段的输出文档添加新字段。
$skip 跳过前n个文档,其中n是指定的跳过编号,并将其余未修改的文档传递到管道。对于每个输入文档,输出零个文档(对于前n个文档)或一个文档(如果在前n个文件之后)。
$sort 按指定的排序键对文档流重新排序。只有顺序更改;文档保持不变。对于每个输入文档,输出一个文档。

 

8.2.1 $addFields
  • $addFields向文档中添加新字段,输出包含来自输入的文档和新添加字段的所有现有字段。
  • $addFields向现有文档添加新的字段,在聚合操作中可以包含多个$addFields阶段。

1 示例:

# 添加数据
db.scores.insertMany([
{
  _id: 1,
  student: "Maya",
  homework: [ 10, 5, 10 ],
  quiz: [ 10, 8 ],
  extraCredit: 0
},{
  _id: 2,
  student: "Ryan",
  homework: [ 5, 6, 5 ],
  quiz: [ 8, 8 ],
  extraCredit: 8
}])

# 执行聚合操作
db.scores.aggregate([
	{
		$addFields: {
			totalHomework: {$sum: "$homework"},
			totalQuiz: {$sum: "$quiz"}
		}
	},
	{
		$addFields: {
			totalScore: {$add: ["$totalHomework", "$totalQuiz", "$extraCredit"]}
		}
	}
])

# 输出结果
{ "_id" : 1, "student" : "Maya", "homework" : [ 10, 5, 10 ], "quiz" : [ 10, 8 ], "extraCredit" : 0, "totalHomework" : 25, "totalQuiz" : 18, "totalScore" : 43 }
{ "_id" : 2, "student" : "Ryan", "homework" : [ 5, 6, 5 ], "quiz" : [ 8, 8 ], "extraCredit" : 8, "totalHomework" : 16, "totalQuiz" : 16, "totalScore" : 40 }

2 重写现有字段:

# 插入数据
db.animals.insert({ _id: 1, dogs: 10, cats: 15 })

# 聚合操作,重写现有字段
db.animals.aggregate([
	{
		$addFields: {cats: 20}
	}
])

# 输出如下
{ "_id" : 1, "dogs" : 10, "cats" : 20 }

# 示例2
# 插入数据
db.fruit.insertMany([
{ "_id" : 1, "item" : "tangerine", "type" : "citrus" },
{ "_id" : 2, "item" : "lemon", "type" : "citrus" },
{ "_id" : 3, "item" : "grapefruit", "type" : "citrus" }
])

# 聚合操作
db.fruit.aggregate([
	{
		$addFields: {
			_id: "$item",
			item: "fruit"
		}
	}
])

# 输出如下
{ "_id" : "tangerine", "item" : "fruit", "type" : "citrus" }
{ "_id" : "lemon", "item" : "fruit", "type" : "citrus" }
{ "_id" : "grapefruit", "item" : "fruit", "type" : "citrus" }

3 向数组中添加字段

# 插入数据
db.scores.insertMany([
   { _id: 1, student: "Maya", homework: [ 10, 5, 10 ], quiz: [ 10, 8 ], extraCredit: 0 },
   { _id: 2, student: "Ryan", homework: [ 5, 6, 5 ], quiz: [ 8, 8 ], extraCredit: 8 }
])

# 聚合操作
db.scores.aggregate([
   { $match: { _id: 1 } },
   { $addFields: { homework: { $concatArrays: [ "$homework", [ 7 ] ] } } }
])

# 输出如下
{ "_id" : 1, "student" : "Maya", "homework" : [ 10, 5, 10, 7 ], "quiz" : [ 10, 8 ], "extraCredit" : 0 }
8.2.2 $count

$count将文档传递到下一个阶段,该阶段包含输入到该阶段的文档数计数。

示例:

# 插入数据
db.scores.insertMany([
{ "_id" : 1, "subject" : "History", "score" : 88 },
{ "_id" : 2, "subject" : "History", "score" : 92 },
{ "_id" : 3, "subject" : "History", "score" : 97 },
{ "_id" : 4, "subject" : "History", "score" : 71 },
{ "_id" : 5, "subject" : "History", "score" : 79 },
{ "_id" : 6, "subject" : "History", "score" : 83 }
])

# 聚合数据,统计分数大于80的文档数量
db.scores.aggregate([
	{
		$match: {score: {$gt: 80}}
	},
	{
		$count: "pass_count"
	}
])

# 输出结果
{ "pass_count" : 4 }
8.2.3 $group

$group阶段根据"组键"将文档分成多个组,然后输出这些组组成的文档。

组键通常是一个字段或一组字段,组键也可以是表达式的结果。使用$group管道阶段中的_id字段设置组键。在输出中,_id字段被设置为该文档的组键。

语法如下:

{
  $group:
    {
      _id: <expression>, // Group key
      <field1>: { <accumulator1> : <expression1> },
      ...
    }
 }

_id:必须的。_id表达式指定组键。如果将_id值指定为null或任何其他常量值,$group阶段将返回一个文档,该文档将聚合所有输入文档的值。

field:可选的

累加器运算符(accmulator operator)

Name Description
$addToSet 返回每个组的唯一表达式值数组。数组元素的顺序未定义。
$avg 求平均值
$bottom 根据指定的排序顺序返回组中的底部元素。
$bottomN 根据指定的排序顺序返回组中最后n个字段的聚合。
$top 根据指定的排序顺序返回组中的顶部元素。
$topN 根据指定的排序顺序返回组中前n个字段的聚合
$count 返回组中的文档数。
$first 返回每个组的第一个文档中的值。仅当文档已排序时才定义顺序
$firstN 返回组中前n个元素的聚合。仅当文档处于定义的顺序时才有意义。
$last 返回每个组的最后一个文档中的值。仅当文档已排序时才定义顺序。
$max 求最大值
$min 求最小值
$sum 求和
$multiply 求积
$push 返回每个组对应字段组成的数组

默认情况下,$group阶段的内存限制为100M。默认情况下如果超过这个限制,返回错误。若要为阶段处理留出更多空间,可以使用allowDiskUse选项启用聚合管道阶段以将数据写入临时文件。

示例:统计文档条数

# 插入数据
db.sales.insertMany([
  { "_id" : 1, "item" : "abc", "price" : NumberDecimal("10"), "quantity" : NumberInt("2"), "date" : ISODate("2014-03-01T08:00:00Z") },
  { "_id" : 2, "item" : "jkl", "price" : NumberDecimal("20"), "quantity" : NumberInt("1"), "date" : ISODate("2014-03-01T09:00:00Z") },
  { "_id" : 3, "item" : "xyz", "price" : NumberDecimal("5"), "quantity" : NumberInt( "10"), "date" : ISODate("2014-03-15T09:00:00Z") },
  { "_id" : 4, "item" : "xyz", "price" : NumberDecimal("5"), "quantity" :  NumberInt("20") , "date" : ISODate("2014-04-04T11:21:39.736Z") },
  { "_id" : 5, "item" : "abc", "price" : NumberDecimal("10"), "quantity" : NumberInt("10") , "date" : ISODate("2014-04-04T21:23:13.331Z") },
  { "_id" : 6, "item" : "def", "price" : NumberDecimal("7.5"), "quantity": NumberInt("5" ) , "date" : ISODate("2015-06-04T05:08:13Z") },
  { "_id" : 7, "item" : "def", "price" : NumberDecimal("7.5"), "quantity": NumberInt("10") , "date" : ISODate("2015-09-10T08:43:00Z") },
  { "_id" : 8, "item" : "abc", "price" : NumberDecimal("10"), "quantity" : NumberInt("5" ) , "date" : ISODate("2016-02-06T20:20:13Z") },
])

# 统计文档条数
db.sales.aggregate([
	{
		$group: {
			_id: null,
			count: {$count: {}}
		}
	}
])

# 相当于sql语句:
SELCET COUNT(*) AS count FROM sales

# 输出结果:
{ "_id" : null, "count" : 8 }
# 上面的count跟count阶段的count是不一样的

示例:根据item分组并统计总价格

# 根据item统计总价格
db.sales.aggregate([
	{
		$group: {
			_id: "$item",
			total_prices: {$sum: {$multiply: ["$price", "$quantity"]}}
		}
	}
])
# 相当于sql语句:
SELECT item, sum(price * quantity) AS total_price 
FROM sales 
GROUP BY item

# 结果如下:
{ "_id" : "abc", "total_prices" : NumberDecimal("170") }
{ "_id" : "xyz", "total_prices" : NumberDecimal("150") }
{ "_id" : "jkl", "total_prices" : NumberDecimal("20") }
{ "_id" : "def", "total_prices" : NumberDecimal("112.5") }

示例:根据日期分组并统计

# 根据日期分组
db.sales.aggregate([
  // First Stage
  {
    $match : { "date": { $gte: new ISODate("2014-01-01"), $lt: new ISODate("2015-01-01") } }
  },
  // Second Stage
  {
    $group : {
       _id : { $dateToString: { format: "%Y-%m-%d", date: "$date" } },
       totalSaleAmount: { $sum: { $multiply: [ "$price", "$quantity" ] } },
       averageQuantity: { $avg: "$quantity" },
       count: { $sum: 1 }
    }
  },
  // Third Stage
  {
    $sort : { totalSaleAmount: -1 }
  }
 ])
 
 # 相当于sql语句
SELECT date,
       Sum(( price * quantity )) AS totalSaleAmount,
       Avg(quantity)             AS averageQuantity,
       Count(*)                  AS Count
FROM   sales
GROUP  BY Date(date)
ORDER  BY totalSaleAmount DESC
 
 # 结果如下:
{ "_id" : "2014-04-04", "totalSaleAmount" : NumberDecimal("200"), "averageQuantity" : 15, "count" : 2 }
{ "_id" : "2014-03-15", "totalSaleAmount" : NumberDecimal("50"), "averageQuantity" : 10, "count" : 1 }
{ "_id" : "2014-03-01", "totalSaleAmount" : NumberDecimal("40"), "averageQuantity" : 1.5, "count" : 2 }

示例:通过null分组

# 通过null分组
db.sales.aggregate([
  {
    $group : {
       _id : null,
       totalSaleAmount: { $sum: { $multiply: [ "$price", "$quantity" ] } },
       averageQuantity: { $avg: "$quantity" },
       count: { $sum: 1 }
    }
  }
 ])
 
# sql
SELECT Sum(price * quantity) AS totalSaleAmount,
       Avg(quantity)         AS averageQuantity,
       Count(*)              AS Count
FROM   sales
 
# 结果如下:
{ "_id" : null, "totalSaleAmount" : NumberDecimal("452.5"), "averageQuantity" : 7.875, "count" : 8 }

可以看到,通过null分组,将所有文档分为了一个组。

示例:使用$push

# 插入数据
db.books.insertMany([
  { "_id" : 8751, "title" : "The Banquet", "author" : "Dante", "copies" : 2 },
  { "_id" : 8752, "title" : "Divine Comedy", "author" : "Dante", "copies" : 1 },
  { "_id" : 8645, "title" : "Eclogues", "author" : "Dante", "copies" : 2 },
  { "_id" : 7000, "title" : "The Odyssey", "author" : "Homer", "copies" : 10 },
  { "_id" : 7020, "title" : "Iliad", "author" : "Homer", "copies" : 10 }
])

# 根据作者名统计它的书有哪些
db.books.aggregate([
	{
		$group: {
			_id: "$author",
			books: {$push: "$title"}
		}
	}
])

# 结果如下:
{ "_id" : "Homer", "books" : [ "The Odyssey", "Iliad" ] }
{ "_id" : "Dante", "books" : [ "The Banquet", "Divine Comedy", "Eclogues" ] }

示例:通过author对文档分组

db.books.aggregate([
	{
		$group: {
			_id: "$author",
			books: {$push: "$$ROOT"}
		}
	},
	{
		$addFields: {
			totalCopies: {$sum: "$books.copies"}
		}
	}
])

# 结果如下:
{
  "_id" : "Homer",
  "books" :
     [
       { "_id" : 7000, "title" : "The Odyssey", "author" : "Homer", "copies" : 10 },
       { "_id" : 7020, "title" : "Iliad", "author" : "Homer", "copies" : 10 }
     ],
   "totalCopies" : 20
}

{
  "_id" : "Dante",
  "books" :
     [
       { "_id" : 8751, "title" : "The Banquet", "author" : "Dante", "copies" : 2 },
       { "_id" : 8752, "title" : "Divine Comedy", "author" : "Dante", "copies" : 1 },
       { "_id" : 8645, "title" : "Eclogues", "author" : "Dante", "copies" : 2 }
     ],
   "totalCopies" : 5
}

$group阶段使用了$$ROOT系统变量按作者对整个文档进行分组。

8.2.4 $limit 和 $skip

$limit限制通过pipeline到达下一阶段的文档数量,语法如下:

{ $limit: <positive 64-bit integer> }

例如:

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

$skip 跳过传递到该阶段的指定数量的文档,并将其余文档传递到管道中的下一阶段,语法如下:

{ $skip: <positive 64-bit integer> }

例如:

db.article.aggregate([
    { $skip : 5 }
]);
8.2.5 $match

$match筛选文档以仅将符合指定条件的文档传递到下一个管道阶段。

语法如下:

{ $match: { <query> } }

示例:

# 插入数据
db.articles.insertMany([
{ "_id" : ObjectId("512bc95fe835e68f199c8686"), "author" : "dave", "score" : 80, "views" : 100 },
{ "_id" : ObjectId("512bc962e835e68f199c8687"), "author" : "dave", "score" : 85, "views" : 521 },
{ "_id" : ObjectId("55f5a192d4bede9ac365b257"), "author" : "ahn", "score" : 60, "views" : 1000 },
{ "_id" : ObjectId("55f5a192d4bede9ac365b258"), "author" : "li", "score" : 55, "views" : 5000 },
{ "_id" : ObjectId("55f5a1d3d4bede9ac365b259"), "author" : "annT", "score" : 60, "views" : 50 },
{ "_id" : ObjectId("55f5a1d3d4bede9ac365b25a"), "author" : "li", "score" : 94, "views" : 999 },
{ "_id" : ObjectId("55f5a1d3d4bede9ac365b25b"), "author" : "ty", "score" : 95, "views" : 1000 }
])

# 匹配作者为dave的文档
db.articles.aggregate([
	{
		$match: {
			author: "dave"
		}
	}
])

# 结果如下:
{ "_id" : ObjectId("512bc95fe835e68f199c8686"), "author" : "dave", "score" : 80, "views" : 100 }
{ "_id" : ObjectId("512bc962e835e68f199c8687"), "author" : "dave", "score" : 85, "views" : 521 }

# 查询分数在70-90之间或者view大于等于1000的记录条数
db.articles.aggregate([
	{
		$match: {
			$or: [{score: {$gt: 70, $lt: 90}}, {views: {$gte: 1000}}]
		}
	},
	{
		$group: {
			_id: null,
			count: {$sum: 1}
		}
	}
])

# 结果如下:
{ "_id" : null, "count" : 5 }
8.2.6 $out

$out获取聚合管道返回的文档并将其写入指定的集合。从MongoDB 4.4开始,可以指定输出数据库。

$out阶段必须是管道中的最后一个阶段。$out运算符允许聚合框架返回任何大小的结果集。

语法:

{ $out: { db: "", coll: "" } }
8.2.7 $project

$project将带有请求字段的文档传递到管道中的下一阶段。指定的字段可以是输入文档中的现有字段或新计算的字段。

语法如下:

{ $project: { <specification(s)> } }

使用$project操作符可以指定查询包含的字段或者排除字段,也可以增加新的字段,$projcet有以下形式:

Form Description
: <1 or true> 指定包含字段。非零整数也被视为真
_id: <0 or false> 指定禁止显示_id字段。
: 添加新字段或重置现有字段的值。如果表达式的计算结果为$$REMOVE,则该字段将被排除在输出中
:<0 or false> 指定排除字段。要有条件地排除字段,请改用REMOVE变量。

示例:指定包含的字段在输出文档中

# 插入数据
db.books.insertMany([
{
  "_id" : 1,
  title: "abc123",
  isbn: "0001122223334",
  author: { last: "zzz", first: "aaa" },
  copies: 5,
  lastModified: "2016-07-28"
},
{
  "_id" : 2,
  title: "Baked Goods",
  isbn: "9999999999999",
  author: { last: "xyz", first: "abc", middle: "" },
  copies: 2,
  lastModified: "2017-07-21"
},
{
  "_id" : 3,
  title: "Ice Cream Cakes",
  isbn: "8888888888888",
  author: { last: "xyz", first: "abc", middle: "mmm" },
  copies: 5,
  lastModified: "2017-07-22"
}
])

# 查询title和author
db.books.aggregate([
	{
		$project: {title: 1, author: 1}
	}
])

# 结果如下:
{ "_id" : 1, "title" : "abc123", "author" : { "last" : "zzz", "first" : "aaa" } }
{ "_id" : 2, "title" : "Baked Goods", "author" : { "last" : "xyz", "first" : "abc", "middle" : "" } }
{ "_id" : 3, "title" : "Ice Cream Cakes", "author" : { "last" : "xyz", "first" : "abc", "middle" : "mmm" } }
# 如果不指定_id字段,默认会包含_id字段

# 在输出时不输出_id字段:
db.books.aggregate([
	{
		$project: {_id: 0, title: 1, author: 1}
	}
])

# 输出结果如下:
{ "title" : "abc123", "author" : { "last" : "zzz", "first" : "aaa" } }
{ "title" : "Baked Goods", "author" : { "last" : "xyz", "first" : "abc", "middle" : "" } }
{ "title" : "Ice Cream Cakes", "author" : { "last" : "xyz", "first" : "abc", "middle" : "mmm" } }

示例:指定不包含的字段在输出文档中

# 查询时不包含author.first和lastModified
db.books.aggregate([
	{
		$project: {"author.first": 0, lastModified: 0}
	}
])

# 输出结果如下:
{ "_id" : 1, "title" : "abc123", "isbn" : "0001122223334", "author" : { "last" : "zzz" }, "copies" : 5 }
{ "_id" : 2, "title" : "Baked Goods", "isbn" : "9999999999999", "author" : { "last" : "xyz", "middle" : "" }, "copies" : 2 }
{ "_id" : 3, "title" : "Ice Cream Cakes", "isbn" : "8888888888888", "author" : { "last" : "xyz", "middle" : "mmm" }, "copies" : 5 }

在一个$project阶段中,除了_id字段,不能同时选择包含的字段以及选择不包含的字段,也就是不能有value同时为0和1

示例:

db.books.aggregate([
	{
		$project: {title: 1, isbn: 0}
	}
])

# 运行结果
... {
... $project: {title: 1, isbn: 0}
... }
... ])
uncaught exception: Error: command failed: {
        "ok" : 0,
        "errmsg" : "Invalid $project :: caused by :: Cannot do exclusion on field isbn in inclusion projection",
        "code" : 31254,
        ...
# 可以看到直接报错了

条件排除字段

可以在聚合表达式中使用变量REMOVE来有条件地抑制字段。

示例:

# 如果author.middle为空字符串,就排除,否则就包含
db.books.aggregate([
	{
		$project: {
			title: 1,
			"author.first": 1,
			"author.last": 1,
			"author.middle": {
				$cond: {
					if: {$eq: ["", "$author.middle"]},
					then: "$$REMOVE",
					else: "$author.middle"
				}
			}
		}
	}
])

# 结果如下:
{ "_id" : 1, "title" : "abc123", "author" : { "last" : "zzz", "first" : "aaa" } }
{ "_id" : 2, "title" : "Baked Goods", "author" : { "last" : "xyz", "first" : "abc" } }
{ "_id" : 3, "title" : "Ice Cream Cakes", "author" : { "last" : "xyz", "first" : "abc", "middle" : "mmm" } }

包含计算的字段:

db.books.aggregate([
	{
		$project: {
			title: 1,
			isbn: {
				prefix: {$substr: ["$isbn", 0, 3]},   # 取子串,下标从0开始,3个字符
				group: {$substr: ["$isbn", 3, 2]},    # 下标从3开始, 2个字符
				publisher: {$substr: ["$isbn", 5, 4]},
				title: {$substr: ["$isbn", 9, 3]},
				checkDigit: {$substr: ["$isbn", 12, 1]}
			},
			lastName: "$author.last",
			copiesSold: "$copies"
		}
	}
])

# 运行结果如下:
{ "_id" : 1, "title" : "abc123", "isbn" : { "prefix" : "000", "group" : "11", "publisher" : "2222", "title" : "333", "checkDigit" : "4" }, "lastName" : "zzz", "copiesSold" : 5 }
{ "_id" : 2, "title" : "Baked Goods", "isbn" : { "prefix" : "999", "group" : "99", "publisher" : "9999", "title" : "999", "checkDigit" : "9" }, "lastName" : "xyz", "copiesSold" : 2 }
{ "_id" : 3, "title" : "Ice Cream Cakes", "isbn" : { "prefix" : "888", "group" : "88", "publisher" : "8888", "title" : "888", "checkDigit" : "8" }, "lastName" : "xyz", "copiesSold" : 5 }

投影新数组字段

# 如果一个集合有如下文档:
{ "_id" : ObjectId("55ad167f320c6be244eb3b95"), "x" : 1, "y" : 1 }

# 使用下面的聚合操作,可以生成新的一个数组
db.collection.aggregate( [ { $project: { myArray: [ "$x", "$y" ] } } ] )

# 运行结果:
{ "_id" : ObjectId("55ad167f320c6be244eb3b95"), "myArray" : [ 1, 1 ] }

# 如果使用的字段不存在,则对应的位置为null
db.collection.aggregate( [ { $project: { myArray: [ "$x", "$y", "$someField" ] } } ] )
# 结果如下
{ "_id" : ObjectId("55ad167f320c6be244eb3b95"), "myArray" : [ 1, 1, null ] }
8.2.8 $set
  • $set向文档添加新字段。
  • $set包含输入文档和新添加字段中所有现有字段的输出文档。
  • $set是$addFields的别名,这两个阶段相当于$project阶段,它显示指定输入文档中的所有现有字段并添加新字段。

语法如下:

{ $set: { <newField>: <expression>, ... } }
  • $set将新字段附加到现有文档。可以在聚合操作中包含一个或多个$set阶段。

  • 要向嵌入文档(包括数组中的文档)添加一个或多个字段,要使用点.

  • 要使用$set将元素添加到现有数组字段,和$concatArray一起使用。

示例:使用两个阶段的$set

# 插入数据
db.scores.insertMany([
   { _id: 1, student: "Maya", homework: [ 10, 5, 10 ], quiz: [ 10, 8 ], extraCredit: 0 },
   { _id: 2, student: "Ryan", homework: [ 5, 6, 5 ], quiz: [ 8, 8 ], extraCredit: 8 }
])

# 聚合操作,统计总分数
db.scores.aggregate([
	{
		$set: {
			totalHomework: {$sum: "$homework"},
			totalQuiz: {$sum: "$quiz"}
		}
	},
	{
		$set: {
			totalScore: {$add: ["$totalHomework", "$totalQuiz", "$extraCredit"]}
		}
	}
])

# 运行结果如下:
{ "_id" : 1, "student" : "Maya", "homework" : [ 10, 5, 10 ], "quiz" : [ 10, 8 ], "extraCredit" : 0, "totalHomework" : 25, "totalQuiz" : 18, "totalScore" : 43 }
{ "_id" : 2, "student" : "Ryan", "homework" : [ 5, 6, 5 ], "quiz" : [ 8, 8 ], "extraCredit" : 8, "totalHomework" : 16, "totalQuiz" : 16, "totalScore" : 40 }

向嵌入文档中添加新字段

# 插入数据
db.vehicles.insertMany([
   { _id: 1, type: "car", specs: { doors: 4, wheels: 4 } },
   { _id: 2, type: "motorcycle", specs: { doors: 0, wheels: 2 } },
   { _id: 3, type: "jet ski" }
])

# 聚合操作
db.vehicles.aggregate([
	{
		$set: {"specs.fuel_type": "unleaded"}
	}
])

# 查询结果
{ "_id" : 1, "type" : "car", "specs" : { "doors" : 4, "wheels" : 4, "fuel_type" : "unleaded" } }
{ "_id" : 2, "type" : "motorcycle", "specs" : { "doors" : 0, "wheels" : 2, "fuel_type" : "unleaded" } }
{ "_id" : 3, "type" : "jet ski", "specs" : { "fuel_type" : "unleaded" } }

重写一个已存在字段

# 插入数据
db.animals.insertOne( { _id: 1, dogs: 10, cats: 15 } )

# 重新cats字段
db.animals.aggregate([
	{
		$set: {cats: 20}
	}
])

# 查询结果
{ "_id" : 1, "dogs" : 10, "cats" : 20 }

向数组中添加新元素

# 添加数据
db.scores.insertMany([
   { _id: 1, student: "Maya", homework: [ 10, 5, 10 ], quiz: [ 10, 8 ], extraCredit: 0 },
   { _id: 2, student: "Ryan", homework: [ 5, 6, 5 ], quiz: [ 8, 8 ], extraCredit: 8 }
])

# 聚合操作
db.scores.aggregate([
	{
		$match: {_id: 1},
	},
	{
		$set: {homework: {$concatArrays: ["$homework", [7] ]}}
	}
])

# 结果:
{ "_id" : 1, "student" : "Maya", "homework" : [ 10, 5, 10, 7 ], "quiz" : [ 10, 8 ], "extraCredit" : 0 

创建一个新的字段

# 增加quize的平均值字段
db.scores.aggregate([
	{
		$set: {quizAverage: {$avg: "$quiz"}}
	}
])

# 结果如下:
{ "_id" : 1, "student" : "Maya", "homework" : [ 10, 5, 10 ], "quiz" : [ 10, 8 ], "extraCredit" : 0, "quizAverage" : 9 }
{ "_id" : 2, "student" : "Ryan", "homework" : [ 5, 6, 5 ], "quiz" : [ 8, 8 ], "extraCredit" : 8, "quizAverage" : 8 }
8.2.9 $sort

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

语法如下:

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

示例:

# 插入数据
db.restaurants.insertMany( [
   { "_id" : 1, "name" : "Central Park Cafe", "borough" : "Manhattan"},
   { "_id" : 2, "name" : "Rock A Feller Bar and Grill", "borough" : "Queens"},
   { "_id" : 3, "name" : "Empire State Pub", "borough" : "Brooklyn"},
   { "_id" : 4, "name" : "Stan's Pizzaria", "borough" : "Manhattan"},
   { "_id" : 5, "name" : "Jane's Deli", "borough" : "Brooklyn"},
] );

# 根据borough升序排序
db.restaurants.aggregate([
	{
		$sort: {borough: 1}
	}
])

# 结果如下:
{ "_id" : 3, "name" : "Empire State Pub", "borough" : "Brooklyn" }
{ "_id" : 5, "name" : "Jane's Deli", "borough" : "Brooklyn" }
{ "_id" : 1, "name" : "Central Park Cafe", "borough" : "Manhattan" }
{ "_id" : 4, "name" : "Stan's Pizzaria", "borough" : "Manhattan" }
{ "_id" : 2, "name" : "Rock A Feller Bar and Grill", "borough" : "Queens" }

 

8.3 聚合管道优化

聚合管道操作在mongodb中有一个优化阶段,该阶段试图重塑管道以提高性能。要查看优化器如何转换特定的聚合管道,在db.collection.aggregate()方法后使用explian()。

8.3.1 投影优化

投影优化主要是对查询的字段数量进行优化。聚合管道可以确定它是否只需要文档中字段的子集来获得结果。如果是这样,管道将只使用那些必填字段,从而减少通过管道的数据量。

8.3.2 管道顺序优化

对于包含投影阶段($project、$unset、$addFields或$set)和$match阶段的聚合管道,mongodb会将$match中不需要在投影阶段中计算值的任何筛选器移动到投影之前的新$match阶段中。

如果聚合管道包含多个投影或$match阶段,mongodb会对每个$match阶段执行此优化,将每个$match筛选器移动到筛选器不依赖的所有投影阶段之前。

对于下面的例子:

{ $addFields: {
    maxTime: { $max: "$times" },
    minTime: { $min: "$times" }
} },
{ $project: {
    _id: 1, name: 1, times: 1, maxTime: 1, minTime: 1,
    avgTime: { $avg: ["$maxTime", "$minTime"] }
} },
{ $match: {
    name: "Joe Schmoe",
    maxTime: { $lt: 20 },
    minTime: { $gt: 5 },
    avgTime: { $gt: 7 }
} }

优化器将$match阶段分解为四个单独的过滤器,每个过滤器用于$match查询文档中的每个键。然后,优化器将每个过滤器移动到尽可能多的投影阶段之前,根据需要创建新的$match阶段。在此示例中,优化器生成以下优化的管道:

{ $match: { name: "Joe Schmoe" } },
{ $addFields: {
    maxTime: { $max: "$times" },
    minTime: { $min: "$times" }
} },
{ $match: { maxTime: { $lt: 20 }, minTime: { $gt: 5 } } },
{ $project: {
    _id: 1, name: 1, times: 1, maxTime: 1, minTime: 1,
    avgTime: { $avg: ["$maxTime", "$minTime"] }
} },
{ $match: { avgTime: { $gt: 7 } } }

可以看到,如果$match:{name: “Joe Schmoe”}放在后面执行,那么在前面可能查询出很多不匹配"Joe Schmoe"的文档,但是这些文档在$match阶段将被筛选掉,因此前面做了很多无用功。因此,在聚合管道优化阶段,mongodb会将$match提前,将会大大提高效率以及内存占用。

  • $match过滤器{name:“Joe Schmoe”}不使用在$project或$addFields阶段中计算的任何值,因此它在两个投影阶段之前被移动到了新的$match阶段。

  • $match筛选器{avgTime:{$gt:7}}依赖于$project阶段来计算avgTime字段。$project阶段是此管道中的最后一个投影阶段,因此无法移动avgTime上的$match筛选器。

  • maxTime和minTime字段在$addFields阶段中计算,但与$project阶段无关。优化器为这些字段上的过滤器创建了一个新的$match阶段,并将其放置在$project阶段之前。

$sort + $match 序列优化

当在$sort阶段后有$match时,$match将被移动到$sort之前来最小化排序的文档数量:

{ $sort: { age : -1 } },
{ $match: { status: 'A' } }

# 优化后
{ $match: { status: 'A' } },
{ $sort: { age : -1 } }

聚合操作的相关阶段和运算符:https://www.mongodb.com/docs/manual/meta/aggregation-quick-reference/

 

9、副本集

MongoDB中的副本集是一组维护相同数据集的mongod进程。副本集提供冗余和高可用性,是所有生产部署的基础。本节介绍MongoDB中的复制以及副本集的组件和体系结构。本节还提供了与副本集相关的常见任务的教程。

冗余和数据可用性

  • 复制提供了冗余并提高了数据可用性。由于在不同的数据库服务器上有多个数据副本,复制提供了针对单个数据库服务器丢失的容错级别。

  • 在某些情况下,复制可以提供更大的读取容量,因为客户端可以将读取操作发送到不同的服务器。在不同的数据中心维护数据副本可以提高分布式应用程序的数据本地性和可用性。您还可以为专用目的(如灾难恢复、报告或备份)维护其他副本。

 

9.1 mongodb中的副本集

9.1.1 副本集的概念

为了解决单点故障的问题,以及进行读写分离,可以使用mongodb的副本集。副本集是维护相同数据集的一组mongod实例。副本集包含多个数据承载节点和可选的一个仲裁器节点。在数据承载节点中,有一个主节点(primary),而其它节点为副本节点(secondary)。

主节点接收所有写入操作。一个副本集只能有一个主副本集;尽管在某些情况下,另一个mongod实例可能会暂时认为自己也是主要的。主数据库在其操作日志(即oplog)中记录其数据集的所有更改。

mongodb学习笔记_第11张图片

副本节点复制主节点的oplog日子并将操作应用于其数据集,以便副本节点的数据集映射主节点的数据。如果主节点不可用,将会进行选举,选出新的主节点。副本节点复制主节点的oplog,并异步地将操作应用于其数据集。即使一个或多个成员出现故障,副本集仍然可以继续运行。

mongodb学习笔记_第12张图片

在一些情况下(例如由于成本限制,只能有一个主节点和副本节点),这时可以增加mongod作为仲裁节点。仲裁节点参与选举,但是不会持有数据(即不提供数据冗余)。

mongodb学习笔记_第13张图片

9.1.2 自动故障切换

当一个主节点在超过配置的electionTimeoutMillis时段(默认情况下为10秒)的时间内没有与集合中的其他成员通信时,符合条件的副本成员将要求进行选举,以将自己指定为新的主要成员。集群试图完成新初选并恢复正常运行。

mongodb学习笔记_第14张图片

在选举完成之前副本集无法处理写操作。如果将查询配置为在主服务器脱机时在辅助服务器上运行,则副本集可以继续提供读取查询。

9.1.3 读操作

默认情况下,客户端从主节点读取数据;但是,客户端可以指定读取首选项以将读取操作发送到副本节点。

mongodb学习笔记_第15张图片

9.1.4 使用docker搭建副本集

创建文件夹:

mkdir -p /root/mongo/mongo1/db  /root/mongo/mongo1/conf /root/mongo/mongo1/log 
mkdir -p /root/mongo/mongo2/db  /root/mongo/mongo2/conf /root/mongo/mongo2/log 
mkdir -p /root/mongo/mongo3/db  /root/mongo/mongo3/conf /root/mongo/mongo3/log 

启动三个mongod容器:

docker run -d --name mongo1 -p 27017:27017 -v /root/mongo/mongo1/db:/data/db -v /root/mongo/mongo1/conf:/data/conf  -v /root/mongo/mongo1/log:/data/log mongo --replSet "rs"
docker run -d --name mongo2 -p 27018:27017 -v /root/mongo/mongo2/db:/data/db -v /root/mongo/mongo2/conf:/data/conf  -v /root/mongo/mongo2/log:/data/log mongo --replSet "rs"
docker run -d --name mongo3 -p 27019:27017 -v /root/mongo/mongo3/db:/data/db -v /root/mongo/mongo3/conf:/data/conf  -v /root/mongo/mongo3/log:/data/log mongo --replSet "rs"

进入其中一个容器:

docker exec -it mongo1 /bin/bash
# 连接mongodb
mongo

配置副本集:

# 在mongosh中输入
var config={
     _id:"rs",
     members:[
         {_id:0,host:"你服务器ip:27017"},
         {_id:1,host:"你服务器ip:27018"},
         {_id:2,host:"你服务器ip:27019"}
]}
rs.initiate(config)

mongodb学习笔记_第16张图片

查看集群状态:

rs.status()

查看数据库:

在这里插入图片描述

进入其它容器中查看数据库:

mongodb学习笔记_第17张图片

发现查看不了,默认其它节点是没有开启的,使用下面的命令开启:

rs.slaveOk()        # 老版本
rs.secondaryOk()    # 新版

在这里插入图片描述

你可能感兴趣的:(数据库,数据库,mongodb)