Contents

MongoDB基础

参考资料

安装与配置

安装

  • 下载地址:https://www.mongodb.com/zh-cn/products/updates/version-release

  • https://www.zhihu.com/question/548433431/answer/2672554145

    在 mongodb4.5 的版本中逐渐改用 mongosh 来连接数据库,在 6 版本中已经完全弃用 mongo 这个 shell 命令
    mongosh 是功能更强大的下一代 MongoDB Shell,提供了语法高亮、智能提示等增强功能

配置

配置环境变量

双击 Path,新建一个并输入自己 MongoDB 的安装位置
例如:D:\MongoDB\Server\6.0\bin

运行 MongoDB 服务

  • 创建数据库文件的存放位置

    为了防止启动 MongoDB 服务失败,启动之前需要必须创建数据库文件的存放文件夹
    在路径 D:\MongoDB\Server\6.0\data 下,创建一个 db 文件夹

  • 启动 MongoDB 服务

    1、打开 cmd
    2、切换到 MongoDB 的 db 文件路径下
    3、输入命令 mongod --dbpath D:\MongoDB\Server\6.0\data\db,回车
    4、仔细看一下是否有 "port":27017 的字样,端口号一般为 27017
    5、然后在浏览器中输入地址和端口号为:http://localhost:27017
    会出现一句话: It looks like you are trying to access MongoDB over HTTP on the native driver port.

  • 结束此次 MongoDB 服务

    连续按两次 Ctrl C

  • 配置本地 Windows MongoDB 服务(可选)

    此步骤是为了 方便启动和关闭 MongoDB 服务

    • 先在 D:\MongoDB\Server\6.0\data 文件下创建新文件夹 log(用来存放日志文件)

    • 新建配置文件 mongo.config

      切换到路径 D:\MongoDB\Server\6.0\bin 下,新建一个 mongo.config 文本文件,输入:

      1
      2
      
      dbpath=D:\Mongodb\Server\6.0\data\db
      logpath=D:\Mongodb\Server\6.0\data\log\mongo.log
      
    • 管理员身份 运行 cmd,切换到路径 D:\MongoDB\Server\6.0\bin 下,执行

      1
      
      mongod -dbpath "D:\Mongodb\Server\6.0\data\db" -logpath "D:\Mongodb\Server\6.0\data\log\mongo.log" -install -serviceName "MongoDB"
      
    • 在这之后,可以通过 cmd 打开服务

      启动 MongoDB net start MongoDB
      关闭 MongoDB net stop MongoDB
      查看正在运作的 MongoDB 服务 services.msc

快速入门

SQL MongoDB 解释
database database 数据库
table collection 表/集合
row document 行/文档
column field 字段/域
index index 索引
table joins \ 表连接,MongDB 不支持
primary key primary key 主键
Mysqld mongod 服务端
mysql mongo 客户端

MongoDB 的 数据库 中有很多 集合
集合中每行称为 文档
每列称为

数据库(database)

可以建立多个数据库
默认数据库为 db,该数据库存储在 data 目录中

show dbs 显示所有数据的列表
db 显示当前数据库对象或集合
use mydatabase 连接到一个指定的数据库 mydatabase

数据库命名注意:
1、不能是空字符串
2、不能含有 空格 $ / \ \0 和 .
5、不要命名为 adminlocalconfig
3、全部小写
4、最多 64 字节

集合(collection)

集合又叫做文档组
存在于数据库中,没有固定的结构,所以可以插入 不同格式和类型 的数据
但是通常会有 一定的关联性

集合命名注意:
1、不能是空字符串
2、不能含有 $ \0
3、不要以 system. 开头

  • capped collections

    即固定大小的 collection,意味着存储空间提前分配
    拥有很高的性能、队列过期的特性
    适合记录日志

    创建一个名为 mycoll 的 capped collection
    指定大小为 100000 字节

    1
    
    db.createCollection("mycoll", {capped:true, size:100000})
    

文档(document)

键值对
文档之间不需要设置相同的字段
即使相同的字段也不需要设置相同的数据类型

注意:
1、键值对 有序
2、区分类型和大小写
3、不能有重复的键
4、键一般为字符串,少数例外
5、值可以是其他数据类型
6、键不能含有 $ \0 和 .,因为 \0 表示字符串的结尾
7、尽量不使用 _ 开头的键名

元数据
使用了系统的命名空间:dbname.system.*

命名 描述
dbname.system.namespaces 列出所有的名字空间
dbname.system.indexes 列出所有的索引
dbname.system.profile 列出该数据库的概要信息
dbname.system.users 列出所有的可以访问该数据库的用户
dbname.system.sources 列出服务器信息和状态

核心特性

无架构 No Schema

文档型数据库,文档组织结构是类 JSON 的 BSON
是二进制的存储格式(额外支持 Date 等数据类型)

这使得数据库中的对象的表现能力更强
字段管理灵活,便于增减字段,也就便于开发和快速迭代

高可用

对于单个 mongod 实例的崩溃的情况下,可以通过 checkpoint(持久化点)恢复到上一个 60 秒之前的数据
而最后一个 checkpoint 到重启期间的数据可以通过 oplog 恢复

  • 怎么做到的高可用?

    通过复制集群,本质是数据存储多份,保证一台机器宕机而数据不会丢失
    一个副本集至少有 3 个节点,主节点>=1、从节点>=2、仲裁节点 0/1
    主节点负责写操作,从节点进行数据同步,仲裁节点负责主节点选举

    在副本集的高可用场景下,主节点宕机后,系统是通过选举出新主节点来恢复服务的,数据由新主节点提供,这个过程是自动的,对应用透明
    原主节点重启后会作为从节点,通过拉取新主节点的 oplog 来同步数据

可扩展性

支持水平扩展,不再担心分库分表的问题

分片集群架构
将存储进行分片,再通过一个 proxy 作为请求路由
一个重要的特性是 数据均衡,数据各个分片的分布均匀,负载均衡,最大化利用集群资源

部分 解释
Configs 配置,是 MongoDB 的副本集,存储元数据和配置
Mongos 路由服务,转发请求到分片,整合分片结果
Mongod 单个分片
  • 数据均衡实现方式

    在 MongoDB 分片集群中,数据被分割成多个 Chunk 作为管理和迁移的基本单位。每个 Chunk 默认大小为 64MB,并可在一定范围内配置(0~1024M)
    当 Chunk 的大小因数据写入而超过设定阈值时,MongoDB 会自动将其分裂为两个较小的 Chunk。所有 Chunk 的元数据信息都存储在配置服务器 Configs 中
    为了实现数据的均匀分布,MongoDB 的均衡器会持续监控各个分片上的 Chunk 数量。当分片间的 Chunk 数量差异达到迁移阈值时,均衡器会自动发起 Chunk 迁移,将 Chunk 从负载较高的分片移动到负载较低的分片
    由于 Chunk 迁移会消耗较多的集群资源(如网络和磁盘 I/O),通常建议在业务低峰期进行,或者通过设置均衡窗口来严格控制迁移的执行时间

  • MongoDB 的分片机制的核心

    首先需要为集合选择一个或多个字段作为 分片键 shardkey
    根据 shardkey 的值,通过特定的 分片策略 将其映射到逻辑值域空间

    • 范围分片:f(x) = x。分片键值相近的文档很可能存储在同一个 Chunk 中,适合范围查询
    • 哈希分片:f(x) = hash(x)。分片键值会被计算成一个哈希值,尽可能地将数据随机、均匀地分布 across 所有分片,有利于数据写入的均匀分布

    上述的值域空间被划分为多个连续的区间,每个区间就是一个 Chunk。Chunk 的元数据信息(如区间范围、所在分片)被记录在配置服务器中
    当进行数据操作时,路由节点(mongos)会根据分片键和分片策略计算出数据所属的 Chunk,再通过查询配置服务器,最终将请求路由到正确的分片上

数据压缩

MongoDB 会把客户数据先压缩再写入磁盘,节省存储空间

进阶知识

存储引擎

默认存储引擎 Wired Tiger,对应的存储结构是 B+ 树

B+ 树的特点是 中间节点只有索引,数据都存在叶子节点
MongoDB 的 B+ 树叶子节点的基本单元是 Page
每个 Page 上面有三个 list:
1、WT_ROW 磁盘加载进来的数据
2、WT_UPDATE 加载之后到下一个 checkpoint 之间被修改的数据
3、WT_INSERT 加载之后到下一个 checkpoint 之间新增的数据

内存 cache
MongoDB 的读写性能会随着数据量达到某个点出现断崖式跌落,而后趋于稳定
根本原因是内存的大小和数据量的关系
工作集(Working Set) 大小超过了物理内存容量,导致大量缺页中断(Page Fault),迫使系统在内存和磁盘间频繁交换数据,使得性能降低

checkpoint
将数据持久化到磁盘

Chunk

chunk 是一组 Document 组成的逻辑数据单元
分片集群的基本单元,用来管理数据存储和路由信息

chunk 分裂条件:chunk 的容量达到阈值;数据的条数达到阈值

rebalance:自动数据均衡,通过将 chunk 从数据多的分片迁移到数据少的分片来实现数据均衡
由于被设计成会尽快完成数据迁移,所以过程非常消耗系统资源

一致性/高可用

CAP 理论
一个分布式系统最多只能同时满足一致性、可用性、分区容错性这三项中的两项

MongoDB 遵循 BASE 理论(基本可用、软状态、最终一致性)
在副本集的 主从同步 存在延迟的时间窗口内,MongoDB 提供的是最终一致性(BASE),但在 写入被确认并复制 到大多数节点后,它能在这些节点上提供 强一致性

选举算法,基于 Raft 协议改进
三种状态:
1、leader,主节点,负责写操作
2、candidate,候选者,主节点挂掉后参与竞选
3、follower,从节点

  • 完整的选举过程

    多个 follower 节点未收到 leader 的心跳,便将自己改变成 candidate 状态,向其他节点发起投票请求
    等待其他节点的投票返回,期间也可以给收到的请求投票
    某个 candidate 收到 过半的赞成票 之后就把自己转换成 leader 状态,并发送心跳宣布继位
    其他的 candidate 就退回到 follower 状态

  • 存在的逻辑问题

    follower 转换成 candidate 是随机并行的,不能保证选出的主节点是最优的
    故 MongoDB 新增了 catchup 的操作,继位之前进行数据同步操作,保证最新的数据

  • 主从同步

    保证数据的一致性和可靠性
    主节点接收客户端请求,更新操作写到 oplog,从节点再从同步源拉取 oplog 实现数据同步

常用语句

增删语句

  • 创建数据库

    1
    
    use mydatabase
    

    若 mydatabase 数据库存在的话,则切换过去

  • 查看所有数据库

    1
    
    show dbs
    

    会发现上面新建的 mydatabase 数据库显示不出来
    因为是空的,需要先添加一些数据

  • 添加数据

    1
    
    db.mydatabase.insert({"game":"popkart"})
    

    再这之后查看数据库就可以看见 mydatabase 这个数据库了

    注意
    默认的数据库是 test
    如果没有创建,集合将被存放在 test 数据库中

  • 查看当前数据库名

    1
    
    db
    
  • 删除数据库

    1
    
    db.dropDatabase()
    
  • 删除集合

    1
    
    db.collection.drop()
    
  • 插入文档

    数据结构和 JSON 基本一样

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    db.col.insert(
      {
        title: 'MongoDB 教程',
        description: 'MongoDB 是一个 Nosql 数据库',
        by: 'w3cschool',
        url: 'http://www.w3cschool.cn',
        tags: ['mongodb', 'database', 'NoSQL'],
        likes: 100
      }
    )
    

    上述 col 是集合名
    也可以先将数据定义为一个变量,再进行插入操作:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    document=(
      {
        title: 'MongoDB 教程',
        description: 'MongoDB 是一个 Nosql 数据库',
        by: 'w3cschool',
        url: 'http://www.w3cschool.cn',
        tags: ['mongodb', 'database', 'NoSQL'],
        likes: 100
      }
    );
    
    db.col.insert(document)
    -- 或者
    db.col.save(document)
    -- 区别是 save 可以指定 _id 字段
    
  • 查看已插入文档

    1
    2
    3
    
    db.col.find()
    -- 或者
    db.col.find().pretty()
    
  • 更新已存在的文档

    1
    2
    3
    4
    5
    6
    
    db.col.update(
      {'year':'2019'},
      {$set:{'year':'2020'}},
      {multi:false}
    )
    -- 其中的 multi 参数若为 true,则会修改多条相同的文档
    
  • 删除文档

    推荐删除之前使用 db.col.find() 查看一下数据

    1
    2
    3
    4
    5
    
    db.col.remove(
      {"year":"2019"},
      {justone:1}
    )
    -- 其中的参数 justone 若为 1,表示只删除第一条找到的数据
    
    1
    2
    
    -- 删除所有数据
    db.col.remove({})
    

查询语句

  • 数据展示

    1
    
    db.COLLECTION_NAME.find().pretty()
    
  • AND 条件

    逗号隔开 即可

    1
    2
    3
    4
    5
    6
    
    db.col.find(
      {
        key1:value1,
        key2:value2
      }
    ).pretty()
    

    筛选出 key1 的值为 value1 key2 的值为 value2 的数据

  • OR 条件

    使用关键字 $or

    1
    2
    3
    4
    5
    6
    7
    8
    
    db.col.find(
      {
        $or:[
          {key1:value1},
          {key2:value2}
        ]
      }
    ).pretty()
    

    筛选出 key1 的值为 value1 或者 key2 的值为 value2 的数据

  • AND 和 OR 联合使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    db.col.find(
      {
        key1:value1,
        $or:[
          {key21:value21},
          {key22:value22}
        ]
      }
    ).pretty()
    

    筛选出 key1 的值为 value1 且 (key21 的值为 value21 或者 key22 的值为 value22) 的数据

  • 大小比较

    操作 关键字 MongoDB MySQL
    等于 db.col.find({"cnt": 10}).pretty() where cnt = 10
    小于 $lt db.col.find({"cnt": {$lt: 10}}).pretty() where cnt < 10
    小于等于 $lte db.col.find({"cnt": {$lte: 10}}).pretty() where cnt <= 10
    大于 $gt db.col.find({"cnt": {$gt: 10}}).pretty() where cnt > 10
    大于等于 $gte db.col.find({"cnt": {$gte: 10}}).pretty() where cnt >= 10
    不等于 $ne db.col.find({"cnt": {$ne: 10}}).pretty() where cnt != 10

    除上述操作外,还可以联合使用 < 和 >

    1
    2
    3
    4
    5
    
    db.col.find(
      {
        key: {$lt: 20, $gt: 10}
      }
    )
    

    筛选出 10 < key < 20 的数据

  • 覆盖查询(Covering Query)

    指的是查询操作能够完全通过索引来完成
    不需要回表(即不需要访问实际的数据文档)来获取数据
    提高查询效率和性能

    例如,如果你有一个索引包含了字段 A 和 B,而此时你的查询条件只涉及 A 和 B,那么这个查询就可以是覆盖查询

    1
    2
    3
    4
    5
    
    -- 假设在 {name: 1, age: 1} 上创建了复合索引
    db.users.find(
      {name: "Alice"},
      {_id: 0, name: 1, age: 1}  -- 只返回索引中包含的字段,且排除_id
    )
    
    • 速度快

      因为索引通常存储在 RAM 中,而 RAM 的访问速度远快于磁盘
      所以通过索引获取数据比扫描整个文档集要快得多

    • 减少 I/O 操作

      由于不需要回表,减少了对磁盘的读取操作,提高查询性能

    • 减少数据传输

      不需要从磁盘读取整个文档,减少了数据传输量

  • 查询优化

    • 查询分析

      explain() 用来分析一条语句的索引使用情况、影响行数、执行时间等
      可以检查自己的语句是否合理,建立的索引是否有效,以及可以据此进行优化

      1
      2
      3
      4
      5
      6
      
      db.users.find(
        {
          name: "Alice",
          age: {$gt: 20}
        }
      ).explain("executionStats")
      
    • hint()

      强制 MongoDB 使用开发者所认为的最优的索引字段来执行查询
      可以测试不同的索引对查询性能的影响

      1
      2
      3
      4
      5
      
      db.users.find(
        {name: "Alice"}
      ).hint({name: 1, age: -1}).toArray()
      
      -- 强制使用 {name: 1, age: -1} 索引来执行查询
      

$type 条件操作符

常见的 $type 对应的数字

类型 数字 备注
Double 1
String 2
Object 3
Array 4
Binary data 5
Undefined 6 已废弃
Object id 7
Boolean 8
Date 9
Null 10
Regular Expression 11
JavaScript 13
Symbol 14
JavaScript (with scope) 15
32-bit integer 16
Timestamp 17
64-bit integer 18
Min key 255 Query with -1.
Max key 127

假设要筛选出集合 “col” 中,title 为 String 类型的数据

1
2
3
4
5
db.col.find(
  {
    "title": {$type: 2}
  }
)

limit 与 skip 方法

  • 读取集合 col 中 num1 条数据记录

    1
    
    db.col.find().limit(num1)
    
  • 跳过一定数量 num2 的数据再进行读取,可以配合 limit 使用

    1
    2
    
    db.col.find().limit(num1).skip(num2)
    -- skip 默认参数值为 0
    

    上述实例会显示从第 num2+1 条开始到第 num2+num1 条的数据
    [num2+1, num2+num1]

排序、索引

  • 排序

    1
    
    db.col.find().sort({key: 1})
    

    上述语句查找出数据,并根据字段 key 进行 升序 排序
    1 为升序排列,-1 为降序排列

索引是特殊的数据结构
对数据库表中一列或者多列的值进行排序,它存储在一个易于遍历读取的数据集合中

  • 创建索引

    索引通常可以提高查询的效率,否则每次筛选时都要扫描集合中的每个文件

    • key 是要创建的索引字段,1 指定为升序创建索引,同理 -1 为降序创建索引

      1
      
      db.col.createIndex({key: 1})
      
    • 设置多个字段来创建索引

      1
      
      db.col.createIndex({key1: 1, key2: -1})
      
    • 让创建索引的工作在后台执行

      1
      2
      
      -- createIndex() 方法还有很多可选参数
      db.col.createIndex({key: 1}, {background: true})
      
  • 高级索引

    假设文档集合 users 中包含子文档 address 和数组 tags

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    {
      "address": {
        "city": "Los Angeles",
        "state": "California",
        "pincode": "123"
      },
      "tags": ["music", "cricket", "blogs"],
      "name": "Tom Benzamin"
    }
    

    创建索引,这样可以通过 address 或者 tags 来检索用户

    • 为子文档 address 创建索引

      1
      2
      3
      4
      5
      6
      7
      
      db.users.createIndex(
        {
          "address.city": 1,
          "address.state": 1,
          "address.pincode": 1
        }
      )
      

      通过子文档的字段检索数据

      1
      2
      3
      4
      5
      6
      7
      
      db.users.find(
        {
          "address.city": "Los Angeles",
          "address.state": "California"
        }
      )
      -- 注意按照索引创建的顺序进行筛选
      
    • 为数组 tags 创建索引

      1
      2
      3
      4
      5
      
      db.users.createIndex(
        {
          "tags": 1
        }
      )
      

      通过数组的字段检索数据

      1
      2
      3
      4
      5
      
      db.users.find(
        {
          tags: "cricket"
        }
      )
      
  • 索引限制

    • 创建的索引会占据一定的索引空间

      如果很少对集合进行读取操作,建议不要使用索引

    • 索引存储在内存(RAM)中,所以需要注意索引的大小

      如果超过了内存,MongoDB 会删除一些索引,这将导致性能下降

    • 可以用 .explain() 来进行查询分析

    • 限制

      集合中索引不能超过 64 个
      索引名的长度不能超过 125 个字符
      一个复合索引最多有 31 个字段

    • 无法使用索引的查询操作

      正则表达式
      非操作符,\$not
      算术运算符,\$mod
      \$where 子句

  • 全文检索

    对每个词建立索引,指明该词在文章中的 位置和出现的次数
    可以提高搜索效率

    • 启用全文索引

      1
      2
      3
      4
      5
      6
      
      db.adminCommand(
        {
          setParamter: true,
          textSearchEnable: true
        }
      )
      

      或者命令行执行

      1
      
      mongod --setParamter textSearchEnable=true
      
    • 创建全文索引

      1
      2
      3
      4
      5
      6
      7
      
      db.posts.createIndex(
        {
          post_text: "text"
        }
      )
      
      -- 对 posts 集合中的 post_text 字段建立全文索引
      
    • 使用全文索引

      1
      2
      3
      4
      5
      
      db.posts.find(
        {
          $text: {$search: 。。。}
        }
      )
      
    • 删除全文索引

      1
      2
      3
      4
      5
      
      -- 先查找索引名
      db.posts.getIndexes()
      
      -- 删除索引
      db.posts.dropIndex("post_text_text")
      
  • 正则表达式

    使用单个字符串来描述、匹配一系列符合某个句法规则的字符串
    使用 $regex 操作符来设置 匹配字符串 的正则表达式

    • 使用正则表达式

      1
      2
      3
      4
      5
      6
      7
      8
      
      db.col.find(
        {
          name: {$regex: "Tom", $options: "$i"}
        }
      )
      
      -- 使用正则表达式查询 col 集合中,name 里面包含 Tom 的数据
      -- $options: "$i" 设置查询不区分大小写
      

      等同于

      1
      
      db.col.find({ name: /Tom/ })
      

聚合

可以用于统计平均值,求和等等,返回计算后的数据结果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
db.col.aggregate(
  [
    {
      $group: {
        _id: "$by_user",
        num_tutorial: {$sum: 1}
      }
    }
  ]
)

-- 统计每个作者所写的文章数

等价于

1
2
3
4
5
6
7
SELECT
  by_user,
  count(*)
FROM
  col
GROUP BY
  by_user
  • 常用的聚合表达式

    表达式 描述
    $sum 计算总和
    $avg 计算平均值
    $min 获取集合中所有文档对应值的最小值
    $max 获取集合中所有文档对应值的最大值
    $push 在结果文档中,插入值到一个数组中
    $addToSet 同上,但是不创建副本
    $first 根据资源文档的排序获取第一个文档数据
    $last 根据资源文档的排序获取最后一个文档数据
  • 管道的概念

    在 linux 中,管道一般用于 将当前命令的输出结果作为下一个命令的参数。在 MongoDB 中是类似的
    管道操作是可以重复的
    表达式是无状态的,只能用于计算当前聚合管道的文档,不能用于处理其他文档

    表达式 描述
    $project 用于重命名、增加或删除域、创建计算结果或者嵌套文档
    $match 用于过滤数据
    $limit 限制管道返回的文档数
    $skip 跳过指定数量的文档
    $unwind 将文档中的某一个数组类型的字段拆分成多条,每条包含一个值
    $group 将集合中的文档分组
    $sort 将输入文档排序后输出
    $geoNear 输出接近某一地理位置的有序文档

其他

复制(副本集)

  • MongoDB 的复制

    将数据同步在多个服务器的过程
    复制提供了数据的冗余备份,提高数据的可用性,保证数据的安全性
    当出现硬件故障或者服务中断的情况,可以恢复数据

  • 原理

    至少需要两个节点
    1、主节点:一个,用于处理客户端请求
    2、从节点:可以多个,用于复制主节点上的数据

    从节点会定期轮询主节点上记录的操作,然后对自己的数据副本执行
    以保证了主从节点的数据一致

  • 尝试做 MongoDB 主从实验

    先关闭正在运行的 MongoDB 服务器
    然后执行:

    1
    
    mongod --port 27017 --dbpath "D:\set up\mongodb\data" --replSet rs0
    

    上述语句会启动一个命为 rs0 的 MongoDB 实例,端口号为 27017

    • 启动一个新的副本集

      1
      
      rs.initiate()
      
    • 查看副本集的配置

      1
      
      rs.conf()
      
    • 查看副本集状态

      1
      
      rs.status()
      
    • 添加副本集的成员

      1
      2
      3
      4
      
      rs.add("mongo1.net : 27017")
      
      -- 假设之前已经启动了一个命为 mongo1.net,端口号为 27017 的服务
      -- 上述语句可以将它添加到副本集中
      

      配置副本集的操作(如添加成员)需要在主节点上执行
      在副本集初始化后,应该从主节点执行 rs.add() 命令,将其他服务器添加为新的从节点

    • 判断当前服务是否为主节点

      1
      
      db.isMaster()
      

    副本集在主机宕机后,副本会接管主节点成为新的主节点

关系

文档之间的关系实际上是通过文档中的字段(通常是 ID 引用)来实现的
表示多个文档之间,在逻辑上的相互关系

例如:
1 : 1(1 对 1)
1 : N(1 对多)
N : 1(多对 1)
N : N(多对多)

  • 嵌入式关系

    正常查询即可
    缺点是若数据量不断变大,会影响读写性能

  • 引用式关系

    users 集合和 address 集合通过引用 id 字段来建立关系

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    -- 先查询 users 集合中符合条件的用户地址 id
    var result = db.users.findOne(
      {"name": "Tom"},
      {"address_ids": 1}
    )
    
    -- 再通过查询到的用户地址 id 获取用户的详细地址信息
    var addresses = db.address.find(
      {
        "_id": {"$in": result["address_ids"]}
      }
    )
    

原子操作

在早期版本中,MongoDB 不支持事务,主要依靠原子操作来保证单个文档级别的完整性
但是从 MongoDB 4.2 版本开始,引入了多文档 ACID 事务支持,事务支持扩展到了分片集群。这意味着现在可以在 MongoDB 中执行跨多个文档、多个集合的真正事务操作,从而保证数据的完整性

原子操作
一件事要么完成要么不完成,所以文档要么保存成功要么没有保存

操作符 示例 说明
$set { $set: { field: value } } 指定一个键并更新其值,若键不存在则进行创建
$unset { $unset: { field: 1 } } 删除一个键
$inc { $inc: { field: value } } 对值为数值型的键进行增减
$push { $push: { field: value } } 将 value 追加到 field 中,要求 field 需要为数组类型的
若 field 不存在,则自动进行创建,且为数组类型的
$pushAll { $pushAll : { field: value_array } } 同 push,区别是可以一次追加多个值进去
$pull { $pull: { field: value } } 从 field 中删除一个等于 value 的值
$addToSet { $addToSet: { field: value } } 增加一个值到 field 中,当值不存在时才增加
$pop { $pop: { field: 1 } } 删除 field 的第一个或者最后一个元素
$rename { $rename: { old_field_name: new_field_name } } 修改字段名称
$bit { $bit: { field: { and: 5 } } }

GridFS

GridFS 会把大文件对象分割成多个小的 chunk(文件片段)
每个大小一般为 256 k,存储在 chunks 集合中
以及一个 files 文件来存放文件名、目录类型、自定义的属性

用于存储、恢复大小超过 16M 的文件

  • put 命令

    1
    2
    
    // 进入到 MongoDB 的 bin 目录中,使用 mongofiles.exe
    mongofiles.exe -d gridfs put song.mp3
    

固定集合

性能出色、大小固定
集合空间用完之后,会从头开始,并且覆盖原数据,可以理解为环形队列
可以用于存储日志信息,缓存一些少量的文档

  • 创建固定集合

    1
    2
    3
    4
    5
    6
    
    db.createCollection(
      "cappedLogCollection",
      {
        capped : true, size : 10000, max : 1000
      }
    )
    
  • 判断集合 col 是否为固定集合

    1
    
    db.col.isCapped()
    
  • 将已存在的集合 col 转换成固定集合

    1
    2
    3
    4
    5
    6
    
    db.runCommand(
      {
        "convertCapped" : "col",
        size : 10000
      }
    )
    
  • 调整数据返回顺序

    固定集合返回的数据按照的是 插入数据的顺序
    也可以使用 $natural 进行调整

    1
    
    db.cappedLogCollection.find().sort({$natural: -1})
    

分片

  • 为什么使用分片

    数据量大量增长,本地磁盘不足,不足以提供可接受的读写吞吐量
    垂直扩展价格昂贵
    请求量巨大时,会出现内存不足

  • 三个重要组件

    1、Shard:存储实际的数据块
    2、Config Server:mongod 实例
    3、Query Routers:前端路由 mongos

  • 分片步骤

    启用分片是在 集合 级别进行的,需要先对数据库启用分片,再对指定集合启用分片并选择分片键

    1
    2
    
    sh.enableSharding("myDatabase")  // 对数据库启用分片
    sh.shardCollection("myDatabase.myCollection", {"shardKey": 1})  // 对集合进行分片
    

    1、启动 Shard Server
    2、启动 Config Server
    3、启动 Route Process
    4、配置 Sharding

备份与恢复

  • 数据备份

    1
    
    mongodump -h dbhost -d dbname -o dbdirectory
    

    -h:MongoDB 所在服务器地址 -d:需要备份的实例 -o:备份数据存放位置

  • 数据恢复

    1
    
    mongorestore -h dbhost -d dbname --directoryperdb
    

注意以上命令需要切换到 mongodb\bin 目录下再执行

监控

目的是了解 MongoDB 的运行情况、查看 MongoDB 的性能

  • mongostat

    内置的状态检测工具
    间隔固定时间获取当前的运行状态
    首先切换到 mongodb\bin 的目录下,再执行 mongostat

  • mongotop

    也是内置的工具
    提供一个方法,可以 跟踪 一个 MongoDB 的实例,查看时间主要消耗在哪里
    同理首先切换到 mongodb\bin 的目录下,再执行 mongotop

数据库引用

  • 手动引用

  • DBRefs

    一个文档从多个集合中引用文档

    1
    2
    3
    4
    5
    
    {
      $res: ,
      $id: ,
      $db:
    }
    
    • $ref:集合名称
    • $id:引用的 id
    • $db:数据库名称,可选参数

ObjectId

ObjectId 是一个 12 字节的 BSON 类型数据

xxxx xxx xx xxx
时间戳 机器识别码 PID,进程 id 随机数

每个集合都有唯一的 _id 值,确保里面的 文档能够被唯一标识
文档需要有一个 _id 键,其值可以是任意类型的,默认是 ObjectId

  • 创建新的 ObjectId

    1
    
    newObjectId = ObjectId()
    
  • 获取文档的时间戳

    1
    
    newObjectId.getTimestamp()
    
  • ObjectId 类型转字符串

    1
    
    new ObjectId.str
    

Map Reduce

Map Reduce 是一种计算模式
将大规模工作分解(Map)执行,再将结果合并(Reduce)
可以用来构建大型复杂的聚合查询

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
db.posts.mapReduce(
  -- map
  function() {
    emit(this.user_id, 1);
  },

  -- reduce
  function(key, values) {
    return Array.sum(values)
  },

  {
    query: {status:"active"},
    out: "post_total"
  }
)

包括 map 函数和 reduce 函数
map 函数生成键值对序列,把它作为参数传给 reduce 函数,注意必须调用 emit(key, value) 函数
reduce 函数做统计,将相同的键值整合起来
上述两个函数可以使用 js 实现,使用更加灵活且功能强大

虽然 Map-Reduce 功能强大且灵活,但需要指出的是,对于大多数聚合任务,聚合管道(Aggregation Pipeline) 是 MongoDB 官方更推荐、性能通常也更好的选择
Map-Reduce 使用 JavaScript,执行速度比用 C++实现的聚合管道要慢。可以将 Map-Reduce 视为一种更底层、更灵活但性能开销更大的工具

实操练习

CRUD

  • 创建

    db.collection.insert()
    db.collection.insertOne()
    db.collection.insertMany()

    1
    2
    3
    4
    5
    6
    7
    
    db.collection.insertOne(
      {
        name: 'sue',
        age: 26,
        status: 'working'
      }
    )
    
    1
    2
    3
    4
    5
    6
    7
    
    db.collection.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" }
    ])
    
  • 查询

    db.collection.find()
    后面可以跟 .limit(n) 或者 .pretty() 等等

    1
    2
    3
    4
    
    db.collection.find(
      { age: {$gt: 18} },
      { name: 1, address: 1 }
    )
    
  • 更新

    db.collection.update()
    db.collection.updateOne()
    db.collection.updateMany()
    db.collection.replaceOne()

    1
    2
    3
    4
    
    db.collection.updateMany(
      { age: {$gt: 18} },
      { $set: { status: 'working' } }
    )
    
  • 删除

    db.collection.deleteOne()
    db.collection.deleteMany()

    1
    2
    3
    
    db.collection.deleteMany(
      { status: 'working' }
    )
    

实例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
-- 查看目前已有的数据库
show dbs

-- 创建并使用 mydatabase 数据库
use mydatabase

-- 此时 mydatabase 数据库为空,需要向其添加一些数据
-- 随便在一个集合 casual 中添加数据
db.casual.insert({"name": "蓝色妖姬你不爱"})

-- 查看当前所在的数据库
db
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
-- 插入数据
db.mycol.insert([
  {
    game_name: 'popkart',
    description: '跑跑卡丁车是一款赛车游戏',
    by: '世纪天成',
    url: 'https://popkart.tiancity.com/homepage/v3/',
    tags: ['休闲', '竞速', '道具'],
    myscore: 90,
  },
  {
    game_name: '英雄联盟',
    description: '英雄对战MOBA竞技网游',
    by: 'Riot Games',
    url: 'https://www.leagueoflegends.com/',
    tags: ['竞技', 'moba'],
    myscore: 80,
  },
  {
    game_name: '原神',
    description: '米哈游的一款开放世界冒险RPG',
    by: 'mihoyo',
    url: 'https://ys.mihoyo.com/',
    tags: ['冒险', 'RPG'],
    myscore: 99,
  },
  {
    game_name: '未定事件簿',
    description: '米哈游的一款律政恋爱推理游戏',
    by: 'mihoyo',
    url: 'https://wd.mihoyo.com/fab/bdsem',
    tags: ['恋爱', '推理'],
    myscore: 83,
  },
  {
    game_name: 'FGO',
    description: '是一款卡牌游戏',
    by: 'TYPE-MOON',
    url: 'https://game.bilibili.com/fgo/',
    tags: ['卡牌', '剧情'],
    myscore: 80,
  }
])
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
-- 查看所有数据
db.mycol.find().pretty()

-- 筛选出【米哈游旗下且给出的分数等于 99】的游戏
db.mycol.find(
  {
    by: 'mihoyo',
    myscore: 99
  }
).pretty()

-- 筛选出【米哈游旗下】或【给出的分数大于等于 85 小于等于 99】的游戏
db.mycol.find(
  {
    $or:[
      {by: 'mihoyo'},
      {myscore: {$gte: 85, $lte: 99}}
    ]
  }
).pretty()

-- 筛选出【米哈游旗下且是冒险类型】或【米哈游旗下且是冒险类型且给出的分数大于等于 90】的游戏
db.mycol.find(
  {
    by: 'mihoyo',
    $or: [
      {tags: ['冒险', 'RPG']},
      {myscore: {$gte: 90}}
    ]
  }
).pretty()
1
2
3
4
5
-- 输出 3 条数据
db.mycol.find().limit(3)

-- 输出第 1 行数据后的 3 条数据
db.mycol.find().limit(3).skip(1)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
-- 将游戏名 BGO 更改为 FGO
db.mycol.update(
  {'game_name': 'BGO'},
  {$set: {'game_name': 'FGO'}},
  {multi: false}
)
db.mycol.find().pretty()

-- 删除一条游戏名为 FGO 的数据
db.mycol.remove(
  {'game_name': 'FGO'},
  {justone: 1}
)
db.mycol.find().pretty()
1
2
3
4
5
-- 将数据根据字段 myscore 进行升序排序
db.mycol.find().sort({myscore: 1})

-- 为 myscore 字段创建索引,便于排序
db.mycol.createIndex({myscore: 1})
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
-- 统计每个公司旗下的游戏数量
db.mycol.aggregate(
  [
    {
      $group: {
        _id: "$by",
        num_game: {$sum: 1}
      }
    }
  ]
)
1
2
3
4
5
6
7
8
-- 清空集合 mycol 中的所有数据
db.mycol.remove({})

-- 删除集合 mycol
db.mycol.drop()

-- 删除当前使用的数据库
db.dropDatabase()