跳至主要內容

MongoDB常见面试题总结(上)

Guide数据库NoSQLMongoDB约 5927 字大约 20 分钟

少部分内容参考了 MongoDB 官方文档的描述,在此说明一下。

MongoDB 基础

MongoDB 是什么?

MongoDB 是一个基于 分布式文件存储 的开源 NoSQL 数据库系统,由 C++ 编写的。MongoDB 提供了 面向文档 的存储方式,操作起来比较简单和容易,支持“无模式”的数据建模,可以存储比较复杂的数据类型,是一款非常流行的 文档类型数据库

在高负载的情况下,MongoDB 天然支持水平扩展和高可用,可以很方便地添加更多的节点/实例,以保证服务性能和可用性。在许多场景下,MongoDB 可以用于代替传统的关系型数据库或键/值存储方式,皆在为 Web 应用提供可扩展的高可用高性能数据存储解决方案。

MongoDB 的存储结构是什么?

MongoDB 的存储结构区别于传统的关系型数据库,主要由如下三个单元组成:

  • 文档(Document):MongoDB 中最基本的单元,由 BSON 键值对(key-value)组成,类似于关系型数据库中的行(Row)。
  • 集合(Collection):一个集合可以包含多个文档,类似于关系型数据库中的表(Table)。
  • 数据库(Database):一个数据库中可以包含多个集合,可以在 MongoDB 中创建多个数据库,类似于关系型数据库中的数据库(Database)。

也就是说,MongoDB 将数据记录存储为文档 (更具体来说是BSON 文档open in new window),这些文档在集合中聚集在一起,数据库中存储一个或多个文档集合。

SQL 与 MongoDB 常见术语对比

SQLMongoDB
表(Table)集合(Collection)
行(Row)文档(Document)
列(Col)字段(Field)
主键(Primary Key)对象 ID(Objectid)
索引(Index)索引(Index)
嵌套表(Embedded Table)嵌入式文档(Embedded Document)
数组(Array)数组(Array)

文档

MongoDB 中的记录就是一个 BSON 文档,它是由键值对组成的数据结构,类似于 JSON 对象,是 MongoDB 中的基本数据单元。字段的值可能包括其他文档、数组和文档数组。

MongoDB 文档
MongoDB 文档

文档的键是字符串。除了少数例外情况,键可以使用任意 UTF-8 字符。

  • 键不能含有 \0(空字符)。这个字符用来表示键的结尾。
  • .$ 有特别的意义,只有在特定环境下才能使用。
  • 以下划线_开头的键是保留的(不是严格要求的)。

BSON [bee·sahn] 是 Binary JSONopen in new window的简称,是 JSON 文档的二进制表示,支持将文档和数组嵌入到其他文档和数组中,还包含允许表示不属于 JSON 规范的数据类型的扩展。有关 BSON 规范的内容,可以参考 bsonspec.orgopen in new window,另见BSON 类型open in new window

根据维基百科对 BJSON 的介绍,BJSON 的遍历速度优于 JSON,这也是 MongoDB 选择 BSON 的主要原因,但 BJSON 需要更多的存储空间。

与 JSON 相比,BSON 着眼于提高存储和扫描效率。BSON 文档中的大型元素以长度字段为前缀以便于扫描。在某些情况下,由于长度前缀和显式数组索引的存在,BSON 使用的空间会多于 JSON。

BSON 官网首页
BSON 官网首页

集合

MongoDB 集合存在于数据库中,没有固定的结构,也就是 无模式 的,这意味着可以往集合插入不同格式和类型的数据。不过,通常情况下,插入集合中的数据都会有一定的关联性。

MongoDB 集合
MongoDB 集合

集合不需要事先创建,当第一个文档插入或者第一个索引创建时,如果该集合不存在,则会创建一个新的集合。

集合名可以是满足下列条件的任意 UTF-8 字符串:

  • 集合名不能是空字符串""
  • 集合名不能含有 \0 (空字符),这个字符表示集合名的结尾。
  • 集合名不能以"system."开头,这是为系统集合保留的前缀。例如 system.users 这个集合保存着数据库的用户信息,system.namespaces 集合保存着所有数据库集合的信息。
  • 集合名必须以下划线或者字母符号开始,并且不能包含 $

数据库

数据库用于存储所有集合,而集合又用于存储所有文档。一个 MongoDB 中可以创建多个数据库,每一个数据库都有自己的集合和权限。

MongoDB 预留了几个特殊的数据库。

  • admin : admin 数据库主要是保存 root 用户和角色。例如,system.users 表存储用户,system.roles 表存储角色。一般不建议用户直接操作这个数据库。将一个用户添加到这个数据库,且使它拥有 admin 库上的名为 dbAdminAnyDatabase 的角色权限,这个用户自动继承所有数据库的权限。一些特定的服务器端命令也只能从这个数据库运行,比如关闭服务器。
  • local : local 数据库是不会被复制到其他分片的,因此可以用来存储本地单台服务器的任意 collection。一般不建议用户直接使用 local 库存储任何数据,也不建议进行 CRUD 操作,因为数据无法被正常备份与恢复。
  • config : 当 MongoDB 使用分片设置时,config 数据库可用来保存分片的相关信息。
  • test : 默认创建的测试库,连接 mongodopen in new window 服务时,如果不指定连接的具体数据库,默认就会连接到 test 数据库。

数据库名可以是满足以下条件的任意 UTF-8 字符串:

  • 不能是空字符串""
  • 不得含有' '(空格)、.$/\\0 (空字符)。
  • 应全部小写。
  • 最多 64 字节。

数据库名最终会变成文件系统里的文件,这也就是有如此多限制的原因。

MongoDB 有什么特点?

  • 数据记录被存储为文档:MongoDB 中的记录就是一个 BSON 文档,它是由键值对组成的数据结构,类似于 JSON 对象,是 MongoDB 中的基本数据单元。
  • 模式自由:集合的概念类似 MySQL 里的表,但它不需要定义任何模式,能够用更少的数据对象表现复杂的领域模型对象。
  • 支持多种查询方式:MongoDB 查询 API 支持读写操作 (CRUD)以及数据聚合、文本搜索和地理空间查询。
  • 支持 ACID 事务:NoSQL 数据库通常不支持事务,为了可扩展和高性能进行了权衡。不过,也有例外,MongoDB 就支持事务。与关系型数据库一样,MongoDB 事务同样具有 ACID 特性。MongoDB 单文档原生支持原子性,也具备事务的特性。MongoDB 4.0 加入了对多文档事务的支持,但只支持复制集部署模式下的事务,也就是说事务的作用域限制为一个副本集内。MongoDB 4.2 引入了分布式事务,增加了对分片集群上多文档事务的支持,并合并了对副本集上多文档事务的现有支持。
  • 高效的二进制存储:存储在集合中的文档,是以键值对的形式存在的。键用于唯一标识一个文档,一般是 ObjectId 类型,值是以 BSON 形式存在的。BSON = Binary JSON, 是在 JSON 基础上加了一些类型及元数据描述的格式。
  • 自带数据压缩功能:存储同样的数据所需的资源更少。
  • 支持 mapreduce:通过分治的方式完成复杂的聚合任务。不过,从 MongoDB 5.0 开始,map-reduce 已经不被官方推荐使用了,替代方案是 聚合管道open in new window。聚合管道提供比 map-reduce 更好的性能和可用性。
  • 支持多种类型的索引:MongoDB 支持多种类型的索引,包括单字段索引、复合索引、多键索引、哈希索引、文本索引、 地理位置索引等,每种类型的索引有不同的使用场合。
  • 支持 failover:提供自动故障恢复的功能,主节点发生故障时,自动从从节点中选举出一个新的主节点,确保集群的正常使用,这对于客户端来说是无感知的。
  • 支持分片集群:MongoDB 支持集群自动切分数据,让集群存储更多的数据,具备更强的性能。在数据插入和更新时,能够自动路由和存储。
  • 支持存储大文件:MongoDB 的单文档存储空间要求不超过 16MB。对于超过 16MB 的大文件,MongoDB 提供了 GridFS 来进行存储,通过 GridFS,可以将大型数据进行分块处理,然后将这些切分后的小文档保存在数据库中。

MongoDB 适合什么应用场景?

MongoDB 的优势在于其数据模型和存储引擎的灵活性、架构的可扩展性以及对强大的索引支持。

选用 MongoDB 应该充分考虑 MongoDB 的优势,结合实际项目的需求来决定:

  • 随着项目的发展,使用类 JSON 格式(BSON)保存数据是否满足项目需求?MongoDB 中的记录就是一个 BSON 文档,它是由键值对组成的数据结构,类似于 JSON 对象,是 MongoDB 中的基本数据单元。
  • 是否需要大数据量的存储?是否需要快速水平扩展?MongoDB 支持分片集群,可以很方便地添加更多的节点(实例),让集群存储更多的数据,具备更强的性能。
  • 是否需要更多类型索引来满足更多应用场景?MongoDB 支持多种类型的索引,包括单字段索引、复合索引、多键索引、哈希索引、文本索引、 地理位置索引等,每种类型的索引有不同的使用场合。
  • ……

MongoDB 存储引擎

MongoDB 支持哪些存储引擎?

存储引擎(Storage Engine)是数据库的核心组件,负责管理数据在内存和磁盘中的存储方式。

与 MySQL 一样,MongoDB 采用的也是 插件式的存储引擎架构 ,支持不同类型的存储引擎,不同的存储引擎解决不同场景的问题。在创建数据库或集合时,可以指定存储引擎。

插件式的存储引擎架构可以实现 Server 层和存储引擎层的解耦,可以支持多种存储引擎,如 MySQL 既可以支持 B-Tree 结构的 InnoDB 存储引擎,还可以支持 LSM 结构的 RocksDB 存储引擎。

在存储引擎刚出来的时候,默认是使用 MMAPV1 存储引擎,MongoDB4.x 版本不再支持 MMAPv1 存储引擎。

现在主要有下面这两种存储引擎:

  • WiredTiger 存储引擎:自 MongoDB 3.2 以后,默认的存储引擎为 WiredTiger 存储引擎open in new window 。非常适合大多数工作负载,建议用于新部署。WiredTiger 提供文档级并发模型、检查点和数据压缩(后文会介绍到)等功能。
  • In-Memory 存储引擎In-Memory 存储引擎open in new window在 MongoDB Enterprise 中可用。它不是将文档存储在磁盘上,而是将它们保留在内存中以获得更可预测的数据延迟。

此外,MongoDB 3.0 提供了 可插拔的存储引擎 API ,允许第三方为 MongoDB 开发存储引擎,这点和 MySQL 也比较类似。

WiredTiger 基于 LSM Tree 还是 B+ Tree?

目前绝大部分流行的数据库存储引擎都是基于 B/B+ Tree 或者 LSM(Log Structured Merge) Tree 来实现的。对于 NoSQL 数据库来说,绝大部分(比如 HBase、Cassandra、RocksDB)都是基于 LSM 树,MongoDB 不太一样。

上面也说了,自 MongoDB 3.2 以后,默认的存储引擎为 WiredTiger 存储引擎。在 WiredTiger 引擎官网上,我们发现 WiredTiger 使用的是 B+ 树作为其存储结构:

WiredTiger maintains a table's data in memory using a data structure called a B-Tree ( B+ Tree to be specific), referring to the nodes of a B-Tree as pages. Internal pages carry only keys. The leaf pages store both keys and values.

此外,WiredTiger 还支持 LSM(Log Structured Merge)open in new window 树作为存储结构,MongoDB 在使用 WiredTiger 作为存储引擎时,默认使用的是 B+ 树。

如果想要了解 MongoDB 使用 B 树的原因,可以看看这篇文章:为什么 MongoDB 使用 B 树?open in new window

使用 B+ 树时,WiredTiger 以 page 为基本单位往磁盘读写数据。B+ 树的每个节点为一个 page,共有三种类型的 page:

  • root page(根节点):B+ 树的根节点。
  • internal page(内部节点):不实际存储数据的中间索引节点。
  • leaf page(叶子节点):真正存储数据的叶子节点,包含一个页头(page header)、块头(block header)和真正的数据(key/value),其中页头定义了页的类型、页中实际载荷数据的大小、页中记录条数等信息;块头定义了此页的 checksum、块在磁盘上的寻址位置等信息。

其整体结构如下图所示:

WiredTiger B+树整体结构
WiredTiger B+树整体结构

如果想要深入研究学习 WiredTiger 存储引擎,推荐阅读 MongoDB 中文社区的 WiredTiger 存储引擎系列open in new window

MongoDB 聚合

MongoDB 聚合有什么用?

实际项目中,我们经常需要将多个文档甚至是多个集合汇总到一起计算分析(比如求和、取最大值)并返回计算后的结果,这个过程被称为 聚合操作

根据官方文档介绍,我们可以使用聚合操作来:

  • 将来自多个文档的值组合在一起。
  • 对集合中的数据进行的一系列运算。
  • 分析数据随时间的变化。

MongoDB 提供了哪几种执行聚合的方法?

MongoDB 提供了两种执行聚合的方法:

  • 聚合管道(Aggregation Pipeline):执行聚合操作的首选方法。
  • 单一目的聚合方法(Single purpose aggregation methods):也就是单一作用的聚合函数比如 count()distinct()estimatedDocumentCount()

绝大部分文章中还提到了 map-reduce 这种聚合方法。不过,从 MongoDB 5.0 开始,map-reduce 已经不被官方推荐使用了,替代方案是 聚合管道open in new window。聚合管道提供比 map-reduce 更好的性能和可用性。

MongoDB 聚合管道由多个阶段组成,每个阶段在文档通过管道时转换文档。每个阶段接收前一个阶段的输出,进一步处理数据,并将其作为输入数据发送到下一个阶段。

每个管道的工作流程是:

  1. 接受一系列原始数据文档
  2. 对这些文档进行一系列运算
  3. 结果文档输出给下一个阶段
管道的工作流程
管道的工作流程

常用阶段操作符

操作符简述
$match匹配操作符,用于对文档集合进行筛选
$project投射操作符,用于重构每一个文档的字段,可以提取字段,重命名字段,甚至可以对原有字段进行操作后新增字段
$sort排序操作符,用于根据一个或多个字段对文档进行排序
$limit限制操作符,用于限制返回文档的数量
$skip跳过操作符,用于跳过指定数量的文档
$count统计操作符,用于统计文档的数量
$group分组操作符,用于对文档集合进行分组
$unwind拆分操作符,用于将数组中的每一个值拆分为单独的文档
$lookup连接操作符,用于连接同一个数据库中另一个集合,并获取指定的文档,类似于 populate

更多操作符介绍详见官方文档:https://docs.mongodb.com/manual/reference/operator/aggregation/open in new window

阶段操作符用于 db.collection.aggregate 方法里面,数组参数中的第一层。

db.collection.aggregate( [ { 阶段操作符:表述 }, { 阶段操作符:表述 }, ... ] )

下面是 MongoDB 官方文档中的一个例子:

db.orders.aggregate([
   # 第一阶段:$match阶段按status字段过滤文档,并将status等于"A"的文档传递到下一阶段。
    { $match: { status: "A" } },
  # 第二阶段:$group阶段按cust_id字段将文档分组,以计算每个cust_id唯一值的金额总和。
    { $group: { _id: "$cust_id", total: { $sum: "$amount" } } }
])

MongoDB 事务

MongoDB 事务想要搞懂原理还是比较花费时间的,我自己也没有搞太明白。因此,我这里只是简单介绍一下 MongoDB 事务,想要了解原理的小伙伴,可以自行搜索查阅相关资料。

这里推荐几篇文章,供大家参考:

我们在介绍 NoSQL 数据的时候也说过,NoSQL 数据库通常不支持事务,为了可扩展和高性能进行了权衡。不过,也有例外,MongoDB 就支持事务。

与关系型数据库一样,MongoDB 事务同样具有 ACID 特性:

  • 原子性Atomicity):事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  • 一致性Consistency):执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;
  • 隔离性Isolation):并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的。WiredTiger 存储引擎支持读未提交( read-uncommitted )、读已提交( read-committed )和快照( snapshot )隔离,MongoDB 启动时默认选快照隔离。在不同隔离级别下,一个事务的生命周期内,可能出现脏读、不可重复读、幻读等现象。
  • 持久性Durability):一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

关于事务的详细介绍这篇文章就不多说了,感兴趣的可以看看我写的MySQL 常见面试题总结这篇文章,里面有详细介绍到。

MongoDB 单文档原生支持原子性,也具备事务的特性。当谈论 MongoDB 事务的时候,通常指的是 多文档 。MongoDB 4.0 加入了对多文档 ACID 事务的支持,但只支持复制集部署模式下的 ACID 事务,也就是说事务的作用域限制为一个副本集内。MongoDB 4.2 引入了 分布式事务 ,增加了对分片集群上多文档事务的支持,并合并了对副本集上多文档事务的现有支持。

根据官方文档介绍:

从 MongoDB 4.2 开始,分布式事务和多文档事务在 MongoDB 中是一个意思。分布式事务是指分片集群和副本集上的多文档事务。从 MongoDB 4.2 开始,多文档事务(无论是在分片集群还是副本集上)也称为分布式事务。

在大多数情况下,多文档事务比单文档写入会产生更大的性能成本。对于大部分场景来说, 非规范化数据模型(嵌入式文档和数组)open in new window 依然是最佳选择。也就是说,适当地对数据进行建模可以最大限度地减少对多文档事务的需求。

注意

  • 从 MongoDB 4.2 开始,多文档事务支持副本集和分片集群,其中:主节点使用 WiredTiger 存储引擎,同时从节点使用 WiredTiger 存储引擎或 In-Memory 存储引擎。在 MongoDB 4.0 中,只有使用 WiredTiger 存储引擎的副本集支持事务。
  • 在 MongoDB 4.2 及更早版本中,你无法在事务中创建集合。从 MongoDB 4.4 开始,您可以在事务中创建集合和索引。有关详细信息,请参阅 在事务中创建集合和索引open in new window

MongoDB 数据压缩

借助 WiredTiger 存储引擎( MongoDB 3.2 后的默认存储引擎),MongoDB 支持对所有集合和索引进行压缩。压缩以额外的 CPU 为代价最大限度地减少存储使用。

默认情况下,WiredTiger 使用 Snappyopen in new window 压缩算法(谷歌开源,旨在实现非常高的速度和合理的压缩,压缩比 3 ~ 5 倍)对所有集合使用块压缩,对所有索引使用前缀压缩。

除了 Snappy 之外,对于集合还有下面这些压缩算法:

  • zlibopen in new window:高度压缩算法,压缩比 5 ~ 7 倍
  • Zstandardopen in new window(简称 zstd):Facebook 开源的一种快速无损压缩算法,针对 zlib 级别的实时压缩场景和更好的压缩比,提供更高的压缩率和更低的 CPU 使用率,MongoDB 4.2 开始可用。

WiredTiger 日志也会被压缩,默认使用的也是 Snappy 压缩算法。如果日志记录小于或等于 128 字节,WiredTiger 不会压缩该记录。

Amazon Document 与 MongoDB 的差异

Amazon DocumentDB(与 MongoDB 兼容) 是一种快速、可靠、完全托管的数据库服务。Amazon DocumentDB 可在云中轻松设置、操作和扩展与 MongoDB 兼容的数据库。

$vectorSearch 运算符

Amazon DocumentDB 不支持$vectorSearch作为独立运营商。相反,我们在$search运营商vectorSearch内部支持。有关更多信息,请参阅 向量搜索 Amazon DocumentDBopen in new window

OpCountersCommand

Amazon DocumentDB 的OpCountersCommand行为偏离于 MongoDB 的opcounters.command 如下:

  • MongoDB 的opcounters.command 计入除插入、更新和删除之外的所有命令,而 Amazon DocumentDB 的 OpCountersCommand 也排除 find 命令。
  • Amazon DocumentDB 将内部命令(例如getCloudWatchMetricsV2)对 OpCountersCommand 计入。

管理数据库和集合

Amazon DocumentDB 不支持管理或本地数据库,MongoDB system.*startup_log 集合也不支持。

cursormaxTimeMS

在 Amazon DocumentDB 中,cursor.maxTimeMS 重置每个请求的计数器。getMore因此,如果指定了 3000MS maxTimeMS,则该查询耗时 2800MS,而每个后续getMore请求耗时 300MS,则游标不会超时。游标仅在单个操作(无论是查询还是单个getMore请求)耗时超过指定值时才将超时maxTimeMS。此外,检查游标执行时间的扫描器以五 (5) 分钟间隔尺寸运行。

explain()

Amazon DocumentDB 在利用分布式、容错、自修复的存储系统的专用数据库引擎上模拟 MongoDB 4.0 API。因此,查询计划和explain() 的输出在 Amazon DocumentDB 和 MongoDB 之间可能有所不同。希望控制其查询计划的客户可以使用 $hint 运算符强制选择首选索引。

字段名称限制

Amazon DocumentDB 不支持点“。” 例如,文档字段名称中 db.foo.insert({‘x.1’:1})

Amazon DocumentDB 也不支持字段名称中的 $ 前缀。

例如,在 Amazon DocumentDB 或 MongoDB 中尝试以下命令:

rs0:PRIMARY< db.foo.insert({"a":{"$a":1}})

MongoDB 将返回以下内容:

WriteResult({ "nInserted" : 1 })

Amazon DocumentDB 将返回一个错误:

WriteResult({
  "nInserted" : 0,
  "writeError" : {
    "code" : 2,
    "errmsg" : "Document can't have $ prefix field names: $a"
  }
})

参考

JavaGuide 官方公众号
JavaGuide 官方公众号