MongoDB 文档数据库学习总结

332 阅读3分钟

MongoDB 基础知识

文档数据库

MongoDB中的记录是一个文档,它是由字段和值对组成的数据结构。MongoDB文档类似于JSON对象。字段的值可以包括其他文档,数组和文档数组。下图为A MongoDB document。

它记录的是一个文档,所以相比 Mysql 这种关系型数据库来说,「不用写 DDL 语句来维护表字段的关系了」,文档里面,你想怎么存就怎么存。

使用文档的优点

  1. 文档(即对象)对应于许多编程语言中的内置数据类型。

文档内的**「数据类型是自己定义」**的,可以对应不同编程语言中的各种内置数据类型

  1. 嵌入式文档和数组减少了对昂贵连接的需求。

最直白的说就是类似于 Mysql 当中的 Join 语句少了

  1. 动态模式支持流畅的多态性。

由于文档内容是自定义的,所以会有各种格式,比如下面这种格式就体现了其多态性

#普通电话,具有打电话发短信的功能
{
  "type": "basic_phone",
  "message":1,
  "call":1
}

#iphone,具有打电话发短信的功能,并且还能玩游戏
{
  "type": "iphone",
  "message":1,
  "call":1,
  "game":1
}

集合/视图/按需实例化视图

MongoDB将文档存储在集合中。集合类似于关系数据库中的表。 除集合外,MongoDB还支持:

  1. 只读视图(从MongoDB 3.4开始)

和 SQL 的视图没有什么差异,视图是基于表/集合之上进行动态查询的一层对象,可以是虚拟的,也可以是物理的(物化视图)。

  1. 按需实例化视图(从MongoDB 4.2开始)

从4.2版本开始,MongoDB 为 aggregation pipeline 添加了 $merge 阶段。此阶段可以将管道结果合并到现有集合中,而不是完全替换现有集合。此功能允许用户创建按需物化视图,每次运行管道时都可以更新输出集合的内容。

主要特性

高性能

MongoDB提供高性能的数据持久化。特别是,

  1. 对嵌入式数据模型的支持减少了数据库系统上的I / O操作。
  2. 索引支持更快的查询,并且可以包含来自嵌入式文档和数组的键。

「什么是嵌入式的数据模型呢? 」 MongoDB 提供高性能的数据持久化。特别是, 对嵌入式数据模型的支持减少了数据库系统上的 I / O 操作(不用连表查询了)。索引支持更快的查询,并且可以包含来自嵌入式文档和数组的键。 「为什么嵌入式模型可以减少数据库系统上的 I / O 操作?」

丰富的查询语言

MongoDB支持丰富的查询语言以支持读写操作(CRUD)以及:

也可以看看

「其实数据库的核心作用就是两个,存储+查询」 各种不同的数据库几乎都是围绕着这两个点去设计的,所以查询方式也是非常重要的,MongoDB 并**「不支持 sql 语句查询」**,但是对于已经熟悉 sql 语句查询的人来说,官方给了我们一个很简单的理解方式,就是 sql 查询和 mongo 查询的对照

image.png

# 如上图 https://docs.mongodb.com/v4.2/reference/sql-comparison/

SELECT * FROM people = db.people.find

SELECT user_id, status FROM people 
= 
db.people.find(
  { },
  { user_id: 1, status: 1, _id: 0 }
)


SELECT user_id, status FROM people WHERE status = "A"
=
db.people.find(
  { status: "A" },
  { user_id: 1, status: 1, _id: 0 }
)

高可用

MongoDB的复制工具(称为副本集)提供:

  • _自动_故障转移
  • 数据冗余。

副本集是一组维护相同数据集合的 mongod实例,提供了冗余和提高了数据可用性。

水平拓展

MongoDB提供水平可伸缩性作为其_核心_ 功能的一部分:

  • 分片将数据分布在一个集群的机器上。
  • 从3.4开始,MongoDB支持基于分片键创建数据区域。在平衡群集中,MongoDB仅将区域覆盖的读写定向到区域内的那些分片。有关 更多信息,请参见区域章节。

支持多种存储引擎

MongoDB提供高性能的数据持久化。特别是,

  • 对嵌入式数据模型的支持减少了数据库系统上的I / O操作。
  • 索引支持更快的查询,并且可以包含来自嵌入式文档和数组的键。

丰富的查询语言

MongoDB支持丰富的查询语言以支持读写操作(CRUD)以及:

也可以看看

高可用

MongoDB的复制工具(称为副本集)提供:

  • 自动_故障转移
  • 数据冗余。

副本集是一组维护相同数据集合的 mongod实例,提供了冗余和提高了数据可用性。

水平拓展

MongoDB提供水平可伸缩性作为其_核心_ 功能的一部分:

  • 分片将数据分布在一个集群的机器上。
  • 从3.4开始,MongoDB支持基于分片键创建数据区域。在平衡群集中,MongoDB仅将区域覆盖的读写定向到区域内的那些分片。有关 更多信息,请参见区域章节。

支持多种存储引擎

MongoDB支持多个存储引擎

另外,MongoDB提供可插拔的存储引擎API,允许第三方为MongoDB开发存储引擎。这其实也是**「类似于 mysql 存储引擎可拔插的设计」**。

架构

我们可以看到,在 mongoDB 的架构中,核心的有三个组件

  • 数据分片(Shards)
    • 分片用于存储真正的集群数据,可以是一个单独的 Mongod实例,也可以是一个副本集。生产环境下Shard一般是一个 Replica Set,以防止该数据片的单点故障。
    • 对于分片集合(sharded collection)来说,每个分片上都存储了集合的一部分数据(按照分片键切分),如果集合没有分片,那么该集合的数据都存储在数据库的 Primary Shard中。
  • 配置服务器(Config Servers)
    • 保存集群的元数据(metadata),包含各个Shard的路由规则,也包括了 chunk (分片数据块)的信息。
  • 查询路由(Query Routers)
    • Mongos是 Sharded Cluster 的访问入口,其本身并不持久化数据 。Mongos启动后,会从 Config Server 加载元数据,开始提供服务,并将用户的请求正确路由到对应的Shard。
    • Sharding 集群可以部署多个 Mongos 以分担客户端请求的压力。

MongoDB 和 Mysql 有什么区别

数据库MongoDBMySQL
数据库模型非关系型关系型
存储方式bson格式不同引擎有不同的存储方式
查询语句MongoDB查询方式SQL语句
数据处理方式基于内存,将热数据存放在物理内存中,从而达到高速读写不同引擎有自己的特点
事务性仅支持单文档事务操作,弱一致性支持事务操作
占用空间占用空间大占用空间小
join操作MongoDB没有joinMySQL支持join

MongoDB 部署方式

单机模式

单机模式,它**「是最基本的一种模式」**,无数的数据库的雏形都是这种方式。 这种部署方式只含有一个 mongod 实例。这种部署方式最简单,但是它并没有数据备份,一旦该节点出现故障,很难快速切换到其他节点,当数据损坏的时候可能会丢失数据,一般不建议采用这种方式。

主从模式

「master」:主节点,负责数据的读写工作 「slave」:从节点,只负责数据的读工作 主从模式也是集群部署中最常见的方式之一,比如 mysql,redis 都支持主从的部署方式,「主要用于备份,故障恢复,读扩展,读写分离」.

主从同步流程

  1. 主节点接受用户的写请求,更新用户表和oplog表。「如果用户设置了 writeConcern 属性」,则可能开启了写确认,处理线程可能会阻塞
  2. 从节点上的后台线程到主节点上**「获取 oplog」,并「放入到 OplogBuffer中」**
  3. "replBatcher" 「从 OplogBuffer 中消费并保存到 OpQueue 中」
  4. "OplogApplier" 线程通过多个(默认16个)worker 线程**「从 OpQueue 获取数据并回放 Oplog」**,并更新 lastAppliedOpTime 和 lastDurableOpTime
  5. 从节点上的后台线程感知到有新数据写入成功,「将自身最新的 lastAppliedOpTime和lastDurableOpTime 等信息返回给主节点」
  6. 主节点**「接受」到各个从节点最新的 「lastAppliedOpTime 和 lastDurableOpTime」,计算大多数节点当前的数据同步进展,并「更新 lastCommittedOpTime」**, 然后唤醒正在等待的请求处理线程主节点上的用户处理线程给用户返回处理结果

image.png

总的来说 ,mongoDB 的 slave 节点之间是无感知的,在 master 收到写请求后,会将该信息**「写入到 oplog」** 中,「oploog 是一个固定大小的文件」,slave 会**「定时拉取 oplog」,来完成数据的同步,这是属于「增量同步」**

全量同步

当然还有两种情况是全量同步

  1. 新 slave 节点进入
  2. slave 节点数据落后太多(slave 节点的最新数据时间戳小于 oplog 最老数据的时间戳)

特点 & 缺点

主动模式特点

  • Master-Slave 的角色是静态配置的,不能自动切换角色,必须人为指定;
  • 用户只能写 Master 节点,Slave 节点只能从 Master 拉数据;
  • Slave 节点只和 Master 通信,Slave 之间相互不感知,
  • 读写分离

主从模式缺点

  • 系统明显存在单点,那么多 Slave 只能从 Master 拉数据,而无法提供自己的判断;
  • 在主从模式下如果 master 挂了,「只能手工完成故障恢复,无法自动完成故障转移」
  • 可用性差。因为主节点挂掉的时候,必须要人为操作处理
  • 短期的数据不一致问题,对于必须需要数据强一致的场景是不合适这种读写分离的
  • master 同步压力较大,slave 都要从 master 去同步

副本集模式

主从模式具有一系列缺点,那么副本集模式就是优化了它的一系列缺点 副本集模式的角色除了 「master(也可以叫做 primary)」「slave(也可以的叫做 Secondary)」 之外,还多了一个 「arbiter」(仲裁者),仲裁节点**「没有数据集的副本」,并且「不能成为主节点」。然而,仲裁节点「可以参与主节点选举」**。一个仲裁节点只有 1 票选举权。

选举 master

slave 之间会间歇性的**「发送心跳包来维护各个节点的信息」**,节点根据自己的集群状态判断是否需要更新新的 primary。在实现的时候主要由两个异步的过程分别处理心跳响应和超时,每个复制集成员都会在后台运行与复制集所有节点的心跳线程,在以下几种情况下会触发状态检测过程:

  • slave 节点权重(Priority)比 master 节点高
  • slave 节点发现集群中没有 master 时
  • master 节点不能访问到大部分成员时主动降级,降级操作会断开连接,终止用户请求
  • 复制集成员心跳检测结果发生变化,比如某个节点挂了或者新增节点,发起重新投票选举规则
  • 超过4s没有执行状态检测过程

「选举发起」 发起选举的节点首先需要做一些条件判断,维护主节点的有 N 个备用节点,备用节点中的所有节点都可能被选举成为主节点,成为主节点前每个备节点都会检测自身以及全局条件是否满足,检测条件如下:

  • 是否看见复制集中是否有 majority 在线
  • priority 是否大于0
  • 不为 arbiter
  • 同步进度不能落后于最新节点 10s 以上
  • 存储的集群信息为最新

如果所有条件满足,则将自身添加到主节点的备用列表中,否则,将自身从列表中移除 「自身检测」

  • MongoDB 选举需要获得大多数投票才能通过,如果没有节点投反对票,且获得成票数超过有权投票节点总数的1/2,则能成为 Primary。否则进入下一轮选举。为避免陷入无限重复选举,MongoDB 建议复制集的成员个数为奇数,当 Secondary 为双数时,可以增加一个 Arbiter 节点。
  • 选举过程中,复制集没有主节点,所有成员都是只读状态
  • 选举过程很复杂,一般情况下需要 5s 左右进行选主。
  • 如果新选择的主节点立刻挂掉,至少需要 30s 时间重新选主。

同步数据

初始化同步源的选择(全量) 初始化同步源的选择取决于启动参数 「initialSyncSourceReadPreference」

  • primary (禁用级联后的默认值),则选择主节点作为同步源。如果主服务器不可用或无法访问,则记录错误并定期检查主服务器的可用性。
  • primaryPreferred,则优先尝试选择主节点作为同步源。如果主节点不可用或者无法访问,则将从剩余可用的副本集成员中选择同步源。
  • secondary:操作只能从集合的次要成员中读取。如果没有可用的辅助节点,则此读取操作会产生错误或异常。
  • secondaryPreferred:在大多数情况下,操作从辅助成员中读取,但在该集合由单个 主成员(并且没有其他成员)组成的情况下,读取操作将使用副本集的主成员。
  • nearest (启用级联后的默认值),则从副本集成员中选择网络时延最小的节点最为同步源。

执行初始化同步源选择的成员将**「会遍历所有副本集成员的列表两次」**:

  • 第一次遍历
    • 当为选择复制同步源进行第一次遍历时,执行同步源选择的成员将检查每个副本集成员是否满足如下条件:
    • 同步源必须处于 PRIMARY 或者 SECONDARY 的复制状态。
    • 同步源必须是在线且可访问的。
    • 同步源必须比该成员具有更新的oplog条目(即同步源数据同步领先于该成员)。
    • 同步源必须是可见的。
    • 同步源必须和主节点最新的oplog条目同步时间相差在30s之内。
    • 如果该成员是可创建索引的,则同步源也必须可创建索引。
    • 如果该成员可参与副本集选举投票,则同步源也必须具有投票权。
    • 如果该成员不是一个延迟成员,则同步源也不能是延迟成员。
    • 如果该成员是一个延迟成员,则同步源必须配置一个更短的延迟时间。
    • 同步源必须比当前最好的同步源更快(即更低的时延)。

如果第一次遍历没有产生候选的同步源,则该成员会用更宽松的条件进行第二次遍历。 请参考同步源选择 第二次遍历。

  • 第二次遍历,当为选择复制同步源进行第二次遍历时,执行同步源选择的成员将检查每个副本集成员是否满足如下条件:
    • 同步源必须处于 PRIMARY 或者 SECONDARY 的复制状态。
    • 同步源必须是在线且可访问的。
    • 如果该成员是可创建索引的,则同步源也必须可创建索引。
    • 同步源必须比当前最好的同步源更快(即更低的时延)。
    • 「如果该成员在两次遍历后依然无法选择出初始同步源,它会记录报错并在等待1s后重新发起选择的过程」

复制同步源的选择 (增量) 复制同步源的选择取决于副本集参数 chaining 的设置:

  • 启用后从副本集成员间执行同步源选择。
  • 禁用后选择主节点作为复制源。

执行复制同步源选择的成员将会**「遍历」所有副本集成员的列表「两次」**:

  • 同步源选择(第一次) 当为选择复制同步源进行第一次遍历时,执行同步源选择的成员将检查每个副本集成员是否满足如下条件:
    • 同步源必须处于 PRIMARY 或者 SECONDARY 的复制状态。
    • 同步源必须是在线且可访问的。
    • 同步源必须比该成员具有更新的oplog条目(即同步源数据同步领先于该成员)。
    • 同步源必须是可见的。
    • 同步源必须和主节点最新的oplog条目同步时间相差在30s之内。
    • 如果该成员是可创建索引的,则同步源也必须可创建索引。
    • 如果该成员可参与副本集选举投票,则同步源也必须具有投票权。
    • 如果该成员不是一个延迟成员,则同步源也不能是延迟成员。
    • 如果该成员是一个延迟成员,则同步源必须配置一个更短的延迟时间。
    • 同步源必须比当前最好的同步源更快(即更低的时延)。

如果**「第一次遍历没有产生候选的同步源」,则该成员会用更宽松的条件「进行第二次遍历」**

  • 同步源选择(第二次遍历) 当为选择复制同步源进行第二次遍历时,执行同步源选择的成员将检查每个副本集成员是否满足如下条件:
    • 同步源必须处于 PRIMARY 或者 SECONDARY 的复制状态。
    • 同步源必须是在线且可访问的。
    • 如果该成员是可创建索引的,则同步源也必须可创建索引。
    • 同步源必须比当前最好的同步源更快(即更低的时延)。
    • 如果该成员在两次遍历后依然无法选择出初始同步源,它会记录报错并在等待1s后重新发起选择的过程。

链式复制

MongoDB通过使用**「多线程批量应用写操作来提高并发」。MongoDB根据文档 id 进行分批,同时使用不同的线程应用每组操作。MongoDB总是「按照原始的写顺序对给定的文档应用写操作」 「选择从从节点复制数据就叫做链式复制」, 链式复制带来的「好处」**是:

  • 不用所有从节点都到主节点同步数据,可以有效减少主节点的压力。
  • 对于写完主节点即返回,并读主节点的业务来说,开启链式复制能在一定程度上提升性能。

链式复制带来的**「缺陷」**是:

  • 数据复制的链路变长。对于 WriteConcern 设置比较大的请求,处理时长会变长。
  • 读oplog的压力从主节点转移到了部分从节点上,会一定程度上影响从节点的性能。

流控制

我们知道磁盘文件级别的**「读写操作是不能进行」的,所以也就是说,当 mongoDB 收到大量的写请求写入 oplog 后,由于数据量大,则从节点拉取 oplog 可能会造成长时间阻塞,那么就有可能造成「主从不一致」的显现出现 mongoDB 为了减少「主从不一致」这种情况,从 MongoDB 4.2 开始,管理员可以「限制主节点应用其写操作的速度」**,目的是将大多数提交延迟保持在可配置参数的最大值之下,从而保证主从之间的一致性。

MongoDB 集群

分片集群架构

其他的不多说,我们先甩一张分片集群的架构图 在分片集群当中,一共有以下三种角色

  • mongos: 路由层,主要用来处理客户端的请求,连接客户端与 shard
  • config server: 主要用来存储分片集群的元数据和配置信息
  • shard: 每个 Shard 就相当于一个 mongod 数据库实例,用于存储数据,整个数据库会**「分散在不同的 shard 当中」**,每一个分片都满足高可用,一般都是一主二从(建议部署位副本集架构),分片的个数最大可以到1024个

一个集群包含了多个分片组成,而一个分片又存储了多个块(每个块包含一定范围片键的数据,互不相交且并集为全部数据),一个块当中包含了多个文档。

mongoDB 是怎么做数据分片的?

mongo 提供了**「三种方式来做数据分片」**

哈希分片

这是很多技术最常用的一种方式,就是将数据通过 hash 散列化,打在不同的机器上,实现**「均匀分布」,但是它很大的问题就是「数据不连续」**,比如业务需要查询工资在 10000~20000 之间的人员,你可能就需要遍历每一个分片了

范围分片

这种策略直接根据片键的范围确定分片。 比如现在我们将数据在逻辑上分为四个块。 在数据上数据 工资 05000一个块,500010000 一个块,1000015000 一个块,1500020000 一个块,2000025000 一个块,25000 以上一个块,由于公司人员薪资分布大概率都在 500015000,这个区域内,就会造成数据过分集中在 500010000 、1000015000 这两个块儿中,造成**「数据分布不均匀」,但是再做「范围查询的时候效率就会很高」**

zone 分片

简单来说 Zone 实际上像是范围分片的另一个版本,你为一定范围内的片键制定一个 Zone,然后再将一些分片加入到这个Zone中,于是这一范围内的数据最终就将存储在这个 Zone 中的分片上。

Chunk(块) 分裂

随着数据慢慢的写入,数据量越来越大,当 Chunk 增长到指定大小(默认为 64MB)时,MongoDB 会 对 Chunk 进行分裂。 Chunk 分裂的⽅式

  • 手动触发
  • 自动触发:当发生插入和更新操作才会触发自动块分裂。

JumboChunk 是一个最小的 Chunk 可以**「只包含一个唯一的 ShardKey」**,这样的 Chunk 不可以再进行分裂。

那么如果数据分片不均 mongoDB 是怎么做的?

这个时候就要说到我们的 「balancer(平衡器)」 了,用来**「保证集合的 Chunk 在各个 Shard 上是均衡的」**。 当某些分片数据不均匀的情况下,balancer 会发出一个命令让切割器去需要移动的分片上去做数据切割,再把数据移动到数据少的分片上。具体的步骤如下:

  1. 平衡器向源分片发送 moveChunk 的命令
  2. 源分片收到命令后,会启动自己内部的一个 moveChunk 命令,如果在数据移动过程中有客户端发来读写请求的话,都会发送到源分片。
  3. 目标片开始向源分片请求将要移动的数据块的文档,准备拷贝文档数据。
  4. 当目标分片接收到据块的最后一个文档后,目标分片会启动一个同步进程来检查,是否已经拷贝完全部的文档。
  5. 当同步完成后,目标分片会连接配置服务器,更新元数据列表中数据块的地址。
  6. 当目标分片完成元数据更新后,源分片就会删除原来的数据块.如果有新的数据块需要移动的话,可以继续进行移动。
  7. 配置服务器会通知 monogs 进程更新自己的映射表。

迁移过程对于应用是透明的,但由于**「迁移过程会占用相应节点的 CPU 和带宽资源」**,因此对分片集有一定程度的性能影响,并且对运维操作存在一些限制。

在对集合进行分片后是否可以更改片键?

「不可以」 MongoDB 中没有对集合分片后更改片键的自动支持。如果在集合分片后必须更改片键,可以按如下方式操作:

  1. 将MongoDB中的所有数据转储为外部格式,比如可以先放在 mysql 中。
  2. 删除原始分片集合。
  3. 使用新的的片键配置分片。
  4. 预分割片键范围以确保初始均匀分布。
  5. 将转储的数据恢复到 MongoDB 中。

mongos 是如何处理连接的?

每个 mongos 实例都**「维护一个与分片集群成员的连接池」。客户端「一次请求就会占用一个连接」**,客户端请求完成后,连接释放。但是客户端数量减少时,这些池不会收缩。这可能导致未使用的mongos占用大量打开的连接。如果 mongos 不再使用,则可以安全地重新启动进程以关闭现有连接。

MongoDB 索引

mongo 索引数据结构

网上对 mongoDB 的数据结构有很多种说法,有说 B- 树的,有说 B 树的,还有说 B+ 树的。这里先说一个常识性的误区,「没有 B减树」,B-tree 其实就是 B 树,中间的破折号只是用来连接而已,「只有 B 树和 B+ 树」

官方文档明确说到,在 WiredTiger 存储引擎当中,可以支持 B-Tree 和 LSM 两种结构组织数据,**「默认使用 B+ 树」**的数据结构在内存中维护表的数据,说 B 树也没错,因为 B+ 树就是 B 树的子集

对于 WiredTiger 存储引擎来说,集合所在的数据文件和相应的索引文件都是按 B-Tree 结构来组织的,不同之处在于数据文件对应的 B 树叶子结点上除了存储键名外(keys),还会存储真正的集合数据(values),所以数据文件的存储结构也可以认为是一种 B+Tree

mongo 支持的索引类型

单个索引

简而言之就是单个字段的索引,比如

db.children.createIndex({ age : 1 })

就相当于给 children 表的 age 字段建立了一个升序索引 (升序 ( 1) 或降序 ( -1) )

复合索引

符合索引其实就是多个字段自合成一个索引,比如

db.children.createIndex({ age : 1 })

就相当于给 children 表 以 age 字段升序 height 字段升序建立了一个索引

多键索引

在MongoDB中可以**「基于数组来创建索引」**。MongoDB为数组每一个元素创建索引值。多键索引支持数组字段的高效查询,比如

([{ _id: 1, name: "xiaohong", age: "1", ratings: [ 1, 2, 3 ] })
db.children.createIndex( { ratings: 1 } )

但是对于一个复合多键索引,「每个索引最多可以包含一个数组」。比如以下情况就无法建立索引

([{ _id: 1, name: "xiaohong", age: "1", ratings: [ 1, 2, 3 ],teams:[ 1 , 3 , 4] })
db.children.createIndex( { ratings: 1 ,teams : -1} )

地理空间索引

为了支持对地理空间坐标数据的高效查询,MongoDB提供了两个特殊的索引: 在返回结果时使用平面几何的 **2d索引 **和使用球面几何返回结果的 2dsphere索引 有关地理空间索引的高级介绍,请参见2d Index Internals

db.collection.createIndex( { <location field> : "2d" } ,
                           { min : <lower bound> , max : <upper bound> } )

文本索引

MongoDB提供了一种文本索引类型,它支持搜索集合中的字符串内容。这些文本索引不存储特定于语言的停止词(例如:“the”,“a”,“or”),并且在一个集合中只存储根词的词干。有关文本索引和搜索的更多信息,请参见文本索引

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

  • 如果集合包含使用不同语言的文档或嵌入文档,请包含在文档或嵌入文档中命名的字段,并将language该文档或嵌入文档的语言指定为其值。
  • textMongoDB 在构建索引时将使用该文档或嵌入文档的指定语言:
    • 文档中指定的语言会覆盖text索引的默认语言。
    • 嵌入文档中的指定语言会覆盖封闭文档中指定的语言或索引的默认语言

Hashed索引

为了支持基于Hashed的分片,MongoDB提供了Hashed索引类型,该索引类型对字段值的Hashed进行索引。这些索引在其范围内具有更随机的值分布,但只支持相等匹配,而不支持基于范围的查询。请参见哈希索引

db.collection.createIndex( { _id: "hashed" } )

索引特性

唯一索引

在创建集合期间,MongoDB 在_id字段上创建唯一索引,这也是默认的唯一索引。该索引主要是为了区分文档并且不能删除。创建方式就是加上 unique: true

db.children.createIndex( { age : 1 }, { unique: true } )

部分索引

部分索引仅索引集合中符合指定 **过滤器表达式 **的文档。用以下命令指定筛选条件的文档:

db.children.createIndex(
{age:1},
{partialFilterExpression: {age: {$gt:5}}})

建立部分索引可以节省存储空间,提升索引查询效率。比如该文档 2000 年前的数据为垃圾数据,不常用,那就可以根据时间大于 2000 年创建索引

稀疏索引

索引的稀疏属性可确保索引仅包含具有索引字段的文档的条目。索引会跳过没有索引字段的文档。创建方式就是加上 sparse: true

db.children.createIndex( { "age": 1 }, { sparse: true } )

TTL索引

TTL 索引是 MongoDB 可以使用的特殊索引,它可以在一定时间后自动从集合中删除文档

db.children.createIndex( { "lastModifiedDate": 1 }, { expireAfterSeconds: 5 } )

以上案例就是设置 5 秒后过去,使用方式只需要创建索引时加上 expireAfterSeconds: 5

覆盖索引

所有需要查询的数据都在索引当中,不需要从数据页中再去寻找数据

# 比如我此时为 children 表的时间创建了一个索引
db.children.createIndex({ age : 1 })
# 在此时我查找年龄为两岁的孩子时,就不需要从数据页中去寻找数据了
db.children.find({ age : 2 })

前缀索引

所有的前缀索引都可以被这条索引所覆盖,不需要再去针对这些前缀建立额外的索引,避免额外的开销 比如我此时为 children 表的时间创建了**「一个复合索引(多字段索引)」**

db.children.createIndex({ age : 1,name : 1,address : 1})

「那么其实这条索引等价于三条索引」,分别是

db.children.createIndex({ age : 1 })
db.children.createIndex({ age : 1,name : 1 })
db.children.createIndex({ age : 1,name : 1,address : 1})

使用索引的奇淫技巧

组合索引的最佳方式 ESR 原则

  • 精准匹配(Equal)的放前面
  • 排序(Sort)的放中间
  • 范围匹配(Range)的方最后

比如一条查询语句,最好的索引建立就应该是 {className:1,time:1,age:1}

db.largeClass.find({className:"a",age:{$gte:5}}).sort(time:1)

Equal 放在最前面大家应该都能理解,用等值匹配去过滤掉大量数据,「那为什么是 ESR 不是 ERS 呢?」

原因就是因为如果范围匹配放在中间,那么后续我们排序的时候只能进行**「内存排序」,而内存排序又是很消耗资源的,数据量大时可能会「面对着多次的磁盘读取刷内存操作」**,非常的消耗时间

合理使用部分索引

对于有些比较大的文档,可能很多数据都是无用的,比如文档中有三年的数据,但是业务只需要最近一年的数据,那么就可以只根据时间对最近一年的数据建立索引

后台创建索引

记得在创建索引时加上{background: true},在后台创建索引,防止影响 mongoDB 的正常工作,让其自动调配创建时间

explain 执行计划

怎么查看我到有没有用到索引? 在 mongoDB 中提供了 「explain 执行计划」,可以清晰的看到你当前的查询语句时候有使用到索引,使用方式也很简单,只要在查询语句右面加上 .explain 就可以了,有几个**「比较重要的属性」**在这里说下

  • 「executionTimeMillis」:指的是我们这条语句的执行时间
  • 「docsExamined」:文档扫描数
  • 「totalDocsExamined」:文档扫描条目
  • 「totalKeysExamined」:索引扫描条目
  • 「stage」:扫描类型,主要有
    • COLLSCAN:全表扫描
    • IXSCAN:索引扫描
    • FETCH:根据索引去检索指定document
    • SHARD_MERGE:将各个分片返回数据进行merge
    • SORT:表明在内存中进行了排序
    • LIMIT:使用limit限制返回数
    • SKIP:使用skip进行跳过
    • IDHACK:针对_id进行查询
    • SHARDING_FILTER:通过mongos对分片数据进行查询
    • COUNT:利用db.coll.explain().count()之类进行count运算
    • COUNTSCAN:count不使用Index进行count时的stage返回
    • COUNT_SCAN:count使用了Index进行count时的stage返回
    • SUBPLA:未使用到索引的$or查询的stage返回
    • TEXT:使用全文索引进行查询时候的stage返回
    • PROJECTION:限定返回字段时候stage的返回

所以当 「stage 为 IXSCAN」 的时候就是使用到了索引扫描

mongodb提供db.collection.explain()cursort.explain()explain命令获取查询计划及查询计划统计信息。利用explain命令,我们可以很好的观察系统如何使用索引来加快检索,同时可以做针对性的性能优化。

explain结果将查询计划以阶段树的形式呈现。每个阶段将其结果(文档或索引键)传递给父节点。叶节点访问集合或索引。中间节点操纵有子节点产生的文档或索引建。根节点是mongodb从中派生结果集的最后阶段。

现版本explain有三种模式,分别是:①queryPlanner、②executionStats、③allPlansExecution

#命令行语句.explain("pattern")  //
db.test.find({
             index_name:"event_detail_2852199351_wx58b4690f0ab8193f_1",
             systime:1600000001000000}).explain("executionStats")
db.mkt_track_detail_560.aggregate([ { $match: { kfuin: 2885772560, sa_list: { $in: [ "2_+8613266776354", "4_8643975168", "3_wxe232d626118f1acd_oDzMB5R1jrfOc3RXxVlXWcKXJNss", "7_12345@qq.com" ] }, sensor_event: "小程序启动", time: { $gte: 1620252229000000.0 } } }, { $group: { _id: null, count: { $sum: 1 } } } ],{explain:true})
 
#或者explain放前面也行
db.collection.explain().aggregate()
  1. queryPlanner

queryPlanner是现版本explain的默认模式。这种模式并不会真正进行query语句查询,而是针对query语句进行执行计划分析并选出winning plan。

  1. executionStats

executionStats返回的是获胜计划的执行细节。必须在executionStats或allPlansExcution详细模式下运行的explain才显示相关信息。

"queryPlanner" : {
  "mongosPlannerVersion" : <int>,
  "winningPlan" : {
  "stage" : <STAGE1>,
  "shards" : [
  {
  "shardName" : <string>,
  "connectionString" : <string>,
  "serverInfo" : {
  "host" : <string>,
  "port" : <int>,
  "version" : <string>,
  "gitVersion" : <string>
},
"plannerVersion" : <int>,
"namespace" : <string>,
"parsedQuery" : <document>,
"queryHash" : <hexadecimal string>,
"planCacheKey" : <hexadecimal string>,
"optimizedPipeline" : <boolean>, // Starting in MongoDB 4.2, only appears if true
"winningPlan" : {
  "stage" : <STAGE2>,
  "inputStage" : {
  "stage" : <STAGE3>
  ...,
}
},
"rejectedPlans" : [
  <candidate plan 1>,
  ...
]
},
...
]
}
}
explain.queryPlanner

参考信息:MongoDB中文手册|官方文档中文版