MongoDB权威指南(第3版)
香农·布拉德肖 克里斯蒂娜·霍多罗夫 约恩·布拉齐尔
第一部分 MongoDB 入门
它融合了二级索引、范围查询、排序、聚合以及地理空间索引等诸多特性
1.1 易于使用
MongoDB 不是关系数据库,而是面向文档(document-oriented)的数据库。
便于扩展是 MongoDB 没有使用关系模型的主要原因
面向文档的数据库使用更灵活的“文档”模型取代了“行”的概念。
MongoDB 中也没有预定义模式(predefined schema):文档键值的类型和大小不是固定的。由于没有固定的模式,因此按需添加或删除字段变得更容易。
1.2 易于扩展
随着所需存储数据量的增长,开发人员面临一个艰难的决定:应该如何扩展数据库?
纵向扩展(提高配置)和横向扩展(将数据分布到更多机器上)
MongoDB 的设计采用了横向扩展
MongoDB 会自动平衡跨集群的数据和负载,自动重新分配文档,并将读写操作路由到正确的机器上
MongoDB 集群的拓扑结构,或者说其连接的是一个集群还是单个节点,对应用程序来说都是透明的。
优势
开发人员能够专注于应用程序的开发,而无须考虑扩展问题。同样,如果需要扩展现有部署的拓扑结构以支持更多负载,那么也无须更改应用程序的逻辑。
开发人员能够专注于应用程序的开发,而无须考虑扩展问题。同样,如果需要扩展现有部署的拓扑结构以支持更多负载,那么也无须更改应用程序的逻辑。
1.3 功能丰富
索引
MongoDB 支持通用的二级索引,并提供唯一索引、复合索引、地理空间索引及全文索引功能
聚合
MongoDB 提供了一种基于数据处理管道的聚合框架。
充分利用数据库优化以构建复杂的分析引擎
特殊的集合和索引类型
MongoDB 支持生命周期有限(TTL)集合,适用于保存将在特定时间过期的数据,比如会话和固定大小的集合,以及用于保存最近的数据(日志)。
MongoDB 还支持部分索引,可以仅对符合某个条件的文档创建索引,以提高效率并减少所需的存储空间。
文件存储
针对大文件及文件元数据的存储,MongoDB 使用了一种非常易用的协议。
MongoDB 并不具备关系数据库中的一些常见功能,特别是复杂的连接操作。
1.4 性能卓越
它在其 WiredTiger 存储引擎中使用了机会锁,以最大限度地提高并发和吞吐量。它会使用尽可能多的 RAM(内存)作为缓存,并尝试为查询自动选择正确的索引。
对于某些功能,数据库服务器会将处理和逻辑交给客户端(由驱动程序或用户的应用程序代码处理)。这种新型的设计方式是 MongoDB 能够实现如此高性能的原因之一。
第 2 章 入门指南
文档是 MongoDB 中的基本数据单元,可以粗略地认为其相当于关系数据库管理系统中的行(但表达力要强得多)。
类似地,集合可以被看作具有动态模式的表。
一个 MongoDB 实例可以拥有多个独立的数据库,每个数据库都拥有自己的集合。
每个文档都有一个特殊的键 “_id”,其在所属的集合中是唯一的。
MongoDB 自带了一个简单但功能强大的工具:mongo shell。mongo shell 对管理 MongoDB 实例和使用 MongoDB 的查询语言操作数据提供了内置的支持
它也是一个功能齐全的 JavaScript 解释器,用户可以根据需求创建或加载自己的脚本。
2.1 文档
文档是 MongoDB 的核心概念:它是一组有序键值的集合。
键中不能含有 \0(空字符)。这个字符用于表示一个键的结束。
和 $ 是特殊字符,只能在某些特定情况下使用
属于保留字符,如果使用不当,那么驱动程序将无法正常工作。
MongoDB 会区分类型和大小写
例如,下面这两个文档是不同的:
{“count” : 5} {“count” : “5”}
下面这两个文档也不同:
{“count” : 5} {“Count” : 5}
MongoDB 中的文档不能包含重复的键
{“greeting” : “Hello, world!”, “greeting” : “Hello, MongoDB!”
2.2 集合
集合就是一组文档
集合具有动态模式的特性
既然不同类型的文档不需要区分模式,为什么还要使用多个集合呢?有以下几点原因。
对于开发人员和管理员来说,将不同类型的文档保存在同一个集合中可能是一个噩梦
获取集合列表比提取集合中的文档类型列表要快得多
如果在每个文档中都有一个 “type” 字段来指明这个文档是“skim”“whole”还是“chunky monkey”,那么在单个集合中查找这 3 个值要比查询 3 个相应的集合慢得多。
将相同类型的文档放入同一个集合中可以实现数据的局部性
相对于从既包含博客文章又包含作者数据的集合中进行查询,从一个只包含博客文章的集合中获取几篇文章可能会需要更少的磁盘查找次数。
创建索引(尤其是在创建唯一索引)时,我们会采用一些文档结构。
通过只将单一类型的文档放入集合中,可以更高效地对集合进行索引
集合由其名称进行标识。集合名称可以是任意 UTF-8 字符串,但有以下限制。
集合名称不能是空字符串(”“)。
• 集合名称不能含有 \0(空字符),因为这个字符用于表示一个集合名称的结束。
• 集合名称不能以 system. 开头,该前缀是为内部集合保留的。
用户创建的集合名称中不应包含保留字符 $。
子集合
使用 . 字符分隔不同命名空间的子集合是一种组织集合的惯例。
GridFS 是一种用于存储大型文件的协议,它使用子集合将文件元数据与内容块分开存储
大多数驱动程序为访问指定集合的子集合提供了一些语法糖。
例如,在数据库 shell 中,使用 db.blog 可以访问 blog 集合,使用 db.blog.posts 可以访问 blog.posts 集合。
在 MongoDB 中,使用子集合来组织数据在很多场景中是一个好方法。
创建集合
# options: 可选参数, 指定有关内存大小及索引的选项
> db.createCollection(name, options)
2.3 数据库
MongoDB 使用集合对文档进行分组,使用数据库对集合进行分组
一个值得推荐的做法是将单个应用程序的所有数据都存储在同一个数据库中
集合相同,数据库也是按照名称进行标识的。数据库名称可以是任意 UTF-8 字符串,但有以下限制。
• 数据库名称不能是空字符串(”“)。
• 数据库名称不能包含 /、\、.、”、*、<、>、:、 | 、?、$、单一的空格以及 \0(空字符),基本上只能使用 ASCII 字母和数字。 |
• 数据库名称区分大小写。
• 数据库名称的长度限制为 64 字节。
在 MongoDB 使用 WiredTiger 存储引擎之前,数据库名称会对应文件系统中的文件名。
admin 数据库会在身份验证和授权时被使用
config
MongoDB 的分片集群
会使用 config 数据库存储关于每个分片的信息。
通过将数据库名称与该库中的集合名称连接起来,可以获得一个完全限定的集合名称,称为命名空间。
如果你要使用 cms 数据库中的 blog.posts 集合,则该集合的命名空间为 cms.blog.posts。命名空间的长度限制为 120 字节,而实际使用时应该小于 100 字节
配置权限
> use admin
> db.createUser({ user: "admin", pwd: "admin", roles: [ "readWrite" ] })
查询所有数据库列表
show dbs
查看当前连接在哪个数据库下面,可以直接输入db
db
切换或创建数据库
> use test
2.4 启动MongoDB
在 Unix 命令行环境中运行 mongod 可执行文件
如果没有指定参数,则 mongod 会使用默认的数据目录 /data/db/
默认情况下,MongoDB 会监听 27017 端口上的套接字连接。
2.5 MongoDB shell介绍
MongoDB 自带 JavaScript shell,允许使用命令行与 MongoDB 实例进行交互。
启动 shell,请运行 mongo 可执行文件:
shell 在启动时会自动尝试连接到本地机器上运行的 MongoDB 服务器端
shell 是一个功能齐全的 JavaScript 解释器
基本的数学运算:
利用所有的 JavaScript 标准库
甚至可以定义和调用 JavaScript 函数:
连续 3 次按下回车键将取消未输入完成的命令并返回到 > 提示符。
启动时,shell 会连接到 MongoDB 服务器端的 test 数据库,并将此数据库连接赋值给全局变量 db
变量是通过 shell 访问 MongoDB 服务器端的主要入口点。
创建、读取、更新以及删除(CRUD)这 4 种基本操作在 shell 中操作和查看数据
- 创建
insertOne 函数可以将一个文档添加到集合中
movie = {“title” : “Star Wars: Episode IV - A New Hope”, … “director” : “George Lucas”, … “year” : 1977} { “title” : “Star Wars: Episode IV - A New Hope”, “director” : “George Lucas”, “year” : 1977
这个对象是一个有效的 MongoDB 文档,因此可以用 insertOne 方法将其保存到 movies 集合中:
db.movies.insertOne(movie) { “acknowledged” : true, “insertedId” : ObjectId(“5721794b349c32b32a012b11”) }
调用集合的 find 方法对其进行查看:
db.movies.find().pretty()
可以看到输入的键–值对都完整地保存了下来,并额外添加了一个 “_id” 键
- 读
find 和 findOne 方法可用于查询集合中的文档
使用 find 时,shell 将自动显示最多 20 个匹配的文档,但也可以获取更多文档。
- 更新
updateOne 会接受(至少)两个参数:第一个用于查找要更新文档的限定条件,第二个用于描述要进行更新的文档。
执行更新,需要使用更新运算符 set
db.movies.updateOne({title : “Star Wars: Episode IV - A New Hope”}, … {$set : {reviews: []}})
现在文档有了一个 “reviews” 键。如果再次调用 find,可以看到这个新生成的键:
. 删除
deleteOne 和 deletemany 方法会从数据库中永久删除文档。
使用 deleteMany 删除与过滤条件匹配的所有文档。
2.6 数据类型
MongoDB 中的文档可以被认为是“类似于 JSON”的形式
JSON 是一种简单的数据表示方式,其规范可以用一段话来描述,并且仅有 6 种数据类型。
易于理解、易于解析且易于记忆
由于只有 null、布尔值、数字、字符串、数组和对象这几种类型,因此 JSON 的表达能力比较有限。
例如,JSON 没有日期类型,这使得原本容易的日期处理变得很麻烦。而且 JSON 只有一个数字类型,因此没有办法区分浮点数和整数,更不用说区分 32 位和 64 位的数字了。同样,JSON 也无法表示其他的常用类型,比如正则表达式或函数。
MongoDB 在保留了 JSON 基本键–值对特性的基础上,增加了对许多额外数据类型的支持
常见的类型有以下几种。
null
null 类型用于表示空值或不存在的字段。
{“x” : null}
布尔类型
布尔类型的值可以为 true 或者 false。
{“x” : true}
数值类型
shell 默认使用 64 位的浮点数来表示数值类型。
对于整数,可以使用 NumberInt 或 NumberLong 类,它们分别表示 4 字节和 8 字节的有符号整数。
{“x” : NumberInt(“3”)} {“x” : NumberLong(“3”)}
字符串类型
任何 UTF-8 字符串都可以使用字符串类型来表示。
{“x” : “foobar”}
日期类型
MongoDB 会将日期存储为 64 位整数,表示自 Unix 纪元(1970 年 1 月 1 日)以来的毫秒数,不包含时区信息。
{“x” : new Date()}
正则表达式
查询时可以使用正则表达式,语法与 JavaScript 的正则表达式语法相同。
{“x” : /foobar/i}
数组类型
集合或者列表可以表示为数组。
{“x” : [“a”, “b”, “c”]}
内嵌文档
文档可以嵌套其他文档,此时被嵌套的文档就成了父文档的值。
{“x” : {“foo” : “bar”}}
Object ID
Object ID 是一个 12 字节的 ID,是文档的唯一标识。
{“x” : ObjectId()}
一些不太常用但可能也会需要的类型
二进制数据
二进制数据是任意字节的字符串,不能通过 shell 操作。如果要将非 UTF-8 字符串存入数据库,那么使用二进制数据是唯一的方法。
代码
MongoDB 还可以在查询和文档中存储任意的 JavaScript 代码:
{“x” : function() { /* … */ }}
在 JavaScript 中,Date 类可以用作 MongoDB 的日期类型。
创建日期对象时
调用 newDate()
如果将构造函数作为函数进行调用(不包括 new 的方式),那么返回的是日期的字符串表示,而不是 Date 对象。这个结果与 MongoDB 无关,是 JavaScript 的机制决定的。
数组是一组值,既可以作为有序对象(如列表、栈或队列)来操作,也可以作为无序对象(如集合)来操作。
数组可以包含不同数据类型的元素
文档可以作为键的值,这称为内嵌文档。
内嵌文档可以使数据组织的方式更加自然,而不仅仅是键–值对这样的扁平结构。
与数组一样,MongoDB“理解”内嵌文档的结构,并能够在其中创建索引、执行查询或进行更新。
另外,MongoDB 可能会导致更多的数据重复
MongoDB 中存储的每个文档都必须有一个 “_id” 键。”_id” 的值可以是任何类型,但其默认为 ObjectId。在单个集合中,每个文档的 “_id” 值都必须是唯一的,以确保集合中的每个文档都可以被唯一标识。
- ObjectId
MongoDB 的分布式特性是它使用 ObjectId 而不是其他传统做法(比如自动递增主键)的主要原因:跨多个服务器同步自动递增主键既困难又耗时。因为 MongoDB 的设计初衷就是作为一个分布式数据库,所以能够在分片环境中生成唯一的标识符非常重要。
ObjectId 占用了 12 字节的存储空间,可以用 24 个十六进制数字组成的字符串来表示:每字节存储两个数字
ObjectId 的 12 字节是按照如下方式生成的:1
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 时间戳 | 随机值 | 计数器(起始值随机) |
ObjectId 的前 4 字节是从 Unix 纪元开始以秒为单位的时间戳
因为时间戳在前,所以 ObjectId 将大致按照插入的顺序进行排列
这并不是一个很强的保证,但是确实在某些方面很有用,比如可以使 ObjectId 的索引效率更高。
前 9 字节保证了 ObjectId 在 1 秒内跨机器和进程的唯一性。最后 3 字节只是一个递增计数器,负责确保单个进程中 1 秒内的唯一性。这允许在 1 秒内为每个进程生成多达 2563(16 777 216)个唯一的 ObjectId。
- 自动生成 _id
这个工作可以由 MongoDB 的服务器端处理,但通常由客户端的驱动程序来完成。
2.7 使用MongoDB shell
连接到其他机器或端口上的 mongod,需要在启动 shell 时指定主机名、端口和数据库:
$ mongo some-host:30000/myDB
mongo 就是一个 JavaScript 的 shell,所以通过查看 JavaScript 的在线文档可以获得大量帮助
shell 内置了帮助文档,可以输入 help 命令进行访问
有一个可以了解函数具体行为的好方法,就是在不使用小括号的情况下输入函数名。这样会打印出函数的 JavaScript 源代码
在脚本中可以访问 db 变量以及任何其他全局变量。不过,shell 的辅助函数(如 use db 或 show collections)不能在文件中使用
.mongorc.js 最常见的用途之一是移除那些比较“危险”的 shell 辅助函数。可以在这里使用 no 选项重写类似 dropDatabase 或 deleteIndexes 这样的函数,或者取消它们的定义
通过将 prompt 变量设置为一个字符串或函数,可以重写默认的 shell 提示。
3.1 插入文档
一次发送数十、数百甚至数千个文档会明显提高插入的速度。
在使用 insertMany 执行批量插入时,如果中途某个文档发生了某种类型的错误,那么接下来会发生什么取决于所选择的是有序操作还是无序操作
如果一个文档产生了插入错误,则数组中在此之后的文档均不会被插入集合中。对于无序插入,MongoDB 将尝试插入所有文档,而不管某些插入是否产生了错误。
MongoDB 会对要插入的数据进行最基本的检查:检查文档的基本结构
如果不存在 “_id” 字段,则自动添加一个。其中一项最基本的结构检查就是文档大小:所有文档都必须小于 16MB。这是一个人为设定的限制(将来可能会提高),主要是为了防止不良的模式设计并确保性能上的一致
对 16MB 的数据量有个概念,以《战争与和平》为例,整部著作也只有 3.14MB。
所有主流语言的 MongoDB 驱动程序以及大部分其他语言的驱动程序,在向数据库发送任何内容之前,都会进行各种无效数据的校验(比如文档过大、包含非 UTF-8 字符串,或使用无法识别的类型)。
3.2 删除文档
CRUD API 为此提供了 deleteOne 和 deleteMany 两种方法
如果想清空整个集合,那么使用 drop 直接删除集合
数据删除是永久性的。没有任何方法可以撤回删除文档或者删除集合的操作,也无法恢复被删除的文档,当然从以前的备份中恢复除外
3.3 更新文档
更新文档是原子操作:如果两个更新同时发生,那么首先到达服务器的更新会先被执行,然后再执行下一个更新。
replaceOne 会用新文档完全替换匹配的文档。
把 “friends” 和 “enemies” 两个字段移到 “relationships” 子文
在 shell 中更改文档的结构,然后使用 replaceOne 替换数据库中的当前文档
一个常见的错误是查询条件匹配到了多个文档,然后更新时由第二个参数产生了重复的 “_id” 值
避免这种情况的最好方法是确保更新始终指定一个唯一的文档,例如使用 “_id” 这样的键来匹配。
使用更新运算符时,”_id” 的值是不能改变的。(注意,整个文档替换时是可以改变 “_id” 的。)
“$set”修饰符入门
“$set” 用来设置一个字段的值。如果这个字段不存在,则创建该字段。
可以用 “$unset” 将这个键完全删除
应该始终使用 $ 修饰符来增加、修改或删除键
“$inc” 运算符可以用来修改已存在的键值或者在该键不存在时创建它
小球落入了“加分”区,要加 100 00 分。可以给 “$inc” 传递一个不同的值:
db.games.updateOne({“game” : “pinball”, “user” : “joe”}, … {“$inc” : {“score” : 10000}})
“$inc” 只能用于整型、长整型或双精度浮点型的值
添加元素。如果数组已存在,”$push” 就会将元素添加到数组末尾;如果数组不存在,则会创建一个新的数组。
可以对 “$push” 使用 “$each” 修饰符,在一次操作中添加多个值
db.stock.ticker.updateOne({“_id” : “GOOG”}, … {“$push” : {“hourly” : {“$each” : [562.776, 562.790, 559.123]}}})
如果只允许数组增长到某个长度,则可以使用 “$slice” 修饰符配合 $push 来防止数组的增长超过某个大小,从而有效地生成“top N ”列表
db.movies.updateOne({“genre” : “horror”}, … {“$push” : {“top10” : {“$each” : [“Nightmare on Elm Street”, “Saw”], … “$slice” : -10}}})
在截断之前可以将 “$sort” 修饰符应用于 “$push” 操作
注意,不能只将 “$slice” 或 “$sort” 与 “$push” 配合使用,必须包含 “$each”。
数组作为集合使用。你可能希望将数组视为集合,仅当一个值不存在时才进行添加。这可以在查询文档中使用 “$ne” 来实现
添加新地址时,可以使用 “$addToSet” 来避免插入重复
删除元素
如果将数组视为队列或者栈,那么可以使用 “$pop” 从任意一端删除元素。{“$pop” : {“key” : 1}} 会从数组末尾删除一个元素,{“$pop” : {“key” : -1}} 则会从头部删除它。
有时需要根据特定条件而不是元素在数组中的位置删除元素。”$pull” 用于删除与给定条件匹配的数组元素。
基于位置的数组更改。当数组中有多个值,但我们只想修改其中的一部分时,在操作上就需要一些技巧了。有两种方法可以操作数组中的值:按位置或使用定位运算符($ 字符)。
MongoDB 提供了一个定位运算符 $,它可以计算出查询文档匹配的数组元素并更新该元素
db.blog.updateOne({“comments.author” : “John”}, … {“$set” : {“comments.$.author” : “Jim”}})
定位运算符只会更新第一个匹配到的元素
使用数组过滤器进行更新。MongoDB 3.6 引入了另一个用于更新单个数组元素的选项:arrayFilters
db.blog.updateOne( {“post” : post_id }, { $set: { “comments.$[elem].hidden” : true } }, { arrayFilters: [ { “elem.votes”: { $lte: -5 } }] } )
upsert 是一种特殊类型的更新。如果找不到与筛选条件相匹配的文档,则会以这个条件和更新文档为基础来创建一个新文档;如果找到了匹配的文档,则进行正常的更新
upsert 来消除竞争条件并减少代码量(updateOne 和 updateMany 的第三个参数是一个选项文档,使我们能够对其进行指定):
db.analytics.updateOne({“url” : “/blog”}, {“$inc” : {“pageviews” : 1}}, … {“upsert” : true})
原子性
“$setOnInsert” 的作用。
“$setOnInsert” 是一个运算符,它只会在插入文档时设置字段的值
save 辅助函数
save 是一个 shell 函数,它可以在文档不存在时插入文档,在文档存在时更新文档
如果文档中包含 “_id” 键,save 就会执行一个 upsert。否则,将执行插入操作
修改与筛选器匹配的所有文档,请使用 updateMany。updateMany 遵循与 updateOne 同样的语义并接受相同的参数。关键的区别在于可能会被更改的文档数量。
db.users.insertMany([ … {birthday: “10/13/1978”}, … {birthday: “10/13/1978”}, … {birthday: “10/13/1978”}]) { “acknowledged” : true, “insertedIds” : [ ObjectId(“5727d6fc6855a935cb57a65b”
findOneAndDelete、findOneAndReplace 和 findOneAndUpdate。这些方法与 updateOne 之间的主要区别在于,它们可以原子地获取已修改文档的值
第 4 章 查询
• 使用 $ 条件进行范围查询、数据集包含查询、不等式查询,以及其他一些查询;
查询会返回一个数据库游标,其只会在需要时才惰性地进行批量返回;
• 有很多可以针对游标执行的元操作,包括跳过一定数量的结果、限定返回结果的数量,以及对结果进行排序。
4.1 find简介
可以通过 find(或者 findOne)的第二个参数来指定需要的键。
既可以节省网络传输的数据量,也可以减少客户端解码文档的时间和内存消耗
查询在使用上是有一些限制的。传递给数据库的查询文档的值必须是常量
4.2 查询条件
“$lt”、”$lte”、”$gt” 和 “$gte” 都属于比较运算符,分别对应 <、<=、> 和 >=。可以将它们组合使用以查找
“$in” 可以用来查询一个键的多个值。”$or” 则更通用一些,可以在多个键中查询任意的给定值。
“$in” 的用法非常灵活,可以指定不同类型的条件和值
与 “$in” 相反的是 “$nin”,此运算符会返回与数组中所有条件都不匹配的文档。
“$or” 会接受一个包含所有可能条件的数组作为参数
db.raffle.find({“$or” : [{“ticket_no” : 725}, {“winner” : true}]}
OR 类型的查询则相反:如果第一个参数能够匹配尽可能多的文档,则其效率最高。
“$not” 是一个元条件运算符:可以用于任何其他条件之上
4.3 特定类型的查询
对一个键进行 null 值的请求还会返回缺少这个键的所有文档
如果仅想匹配键值为 null 的文档,则需要检查该键的值是否为 null,并且通过 “$exists” 条件确认该键已存在。
db.c.find({“z” : {“$eq” : null, “$exists” : true}}
“$regex” 可以在查询中为字符串的模式匹配提供正则表达式功能
MongoDB 会使用 Perl 兼容的正则表达式(PCRE)库来对正则表达式进行匹配。
查询数组元素的方式与查询标量值相同
- “$all”
如果需要通过多个元素来匹配数组,那么可以使用 “$all”
- “$size”
“$size” 条件运算符对于查询数组来说非常有用,可以用它查询特定长度的数组
“$size” 并不能与另一个 $ 条件运算符(如 “$gt”)组合使用,但这种查询可以通过在文档中添加一个 “size” 键的方式来实现
- “$slice”
find 的第二个参数是可选的,可以指定需要返回的键。这个特别的 “$slice” 运算符可以返回一个数组键中元素的子集
假设现在有一个关于博客文章的文档,我们希望返回前 10 条评论:
db.blog.posts.findOne(criteria, {“comments” : {“$slice” : 10}})
同样,如果想返回后 10 条评论,则可以使用 -10:
db.blog.posts.findOne(criteria, {“comments” : {“$slice” : -10}})
“$slice” 也可以指定偏移量和返回的元素数量来获取数组中间的结果:
db.blog.posts.findOne(criteria, {“comments” : {“$slice” : [23, 10]}})
- 返回一个匹配的数组元素
如果知道数组元素的下标,那么 “$slice” 非常有用。但有时我们希望返回与查询条件匹配的任意数组元素。这时可以使用 $ 运算符来返回匹配的元素。
注意,这种方式只会返回每个文档中第一个匹配的元素
- 数组与范围查询的相互作用
文档中的标量(非数组元素)必须与查询条件中的每一条子句相匹配
4.3.4 查询内嵌文档
查询内嵌文档的方法有两种:查询整个文档或针对其单个键–值对进行查询。
如果可能,最好只针对内嵌文档的特定键进行查询。这样,即使数据模式变了,也不会导致所有查询因为需要精确匹配而无法使用。可以使用点表示法对内嵌文档的键进行查询:
查询文档可以包含点,表示“进入内嵌文档内部”的意思。点表示法也是待插入文档不能包含 . 字符的原因。当人们试图将 URL 保存为键时,常常会遇到这种限制。解决这个问题的一种方法是在插入前或者提取后始终执行全局替换,用点字符替换 URL 中不合法的字符。
要正确指定一组条件而无须指定每个键,请使用 “$elemMatch”。
“$elemMatch” 允许你将限定条件进行“分组”。仅当需要对一个内嵌文档的多个键进行操作时才会用到它。
4.4 $where查询
无法以其他方式执行的查询,可以使用 “$where” 子句,它允许你在查询中执行任意的 JavaScript 代码。这样就能在查询中做大部分事情了
除非绝对必要,否则不应该使用 “$where” 查询:它们比常规查询慢得多。每个文档都必须从 BSON 转换为 JavaScript 对象,然后通过 “$where” 表达式运行。此外,”$where” 也无法使用索引。
4.5 游标
数据库会使用游标返回 find 的执行结果。游标的客户端实现通常能够在很大程度上对查询的最终输出进行控制。
var cursor = db.collection.find();
要遍历结果,可以在游标上使用 next 方法。可以使用 hasNext 检查是否还有其他结果。典型的结果遍历如下所示:
while (cursor.hasNext()) { … obj = cursor.next(); … // 执行任务 … }
cursor.hasNext() 会检查是否有后续结果存在,而 cursor.next() 用来对其进行获取。
cursor 类还实现了 JavaScript 的迭代器接口,因此可以在 forEach 循环中使用:
var cursor = db.people.find(); > cursor.forEach(function(x) { … print(x.name); … }); adam matt zak
4.5.1 limit、skip和sort
最常用的查询选项是限制返回结果的数量、略过一定数量的结果以及排序。
skip 与 limit 类似:
db.c.find().skip(3)
这个操作会略过前 3 个匹配的文档,然后返回剩下的文档。如果集合中匹配的文档少于 3 个,则不会返回任何文档。
sort 会接受一个对象作为参数,这个对象是一组键–值对,键对应文档的键名,值对应排序的方向。排序方向可以是 1(升序)或 -1(降序)。
比较顺序
如果对混合类型的键进行排序,那么会有一个预定义的排序顺序。从最小值到最大值,顺序如下。
- 最小值
-
null
-
数字(整型、长整型、双精度浮点型、小数型)
-
字符串
-
对象/文档
-
数组
-
二进制数据
-
对象 ID
-
布尔型
-
日期
-
时间戳
-
正则表达式
4.5.3 游标生命周期
游标包括两个部分:面向客户端的游标和由客户端游标所表示的数据库游标
在服务器端,游标会占用内存和资源。一旦游标遍历完结果之后,或者客户端发送一条消息要求终止,数据库就可以释放它正在使用的资源
5.1 索引简介
不使用索引的查询称为集合扫描
这意味着服务器端必须“浏览整本书”才能得到查询的结果
如果对这个集合执行查询,可以使用 explain 命令来查看 MongoDB 在执行查询时所做的事情
5.1.1 创建索引
要创建索引,需要使用 createIndex 集合方法:
db.users.createIndex({“username” : 1})
如果 createIndex 调用在几秒后没有返回,则可以运行 db.currentOp()(在另一个 shell 中)或检查 mongod 的日志以查看索引创建的进度
使用索引是有代价的:修改索引字段的写操作(插入、更新和删除)会花费更长的时间
MongoDB 索引的工作原理与典型的关系数据库索引几乎相同
5.1.2 复合索引
对于许多查询模式来说,在两个或更多的键上创建索引是必要的
索引只有在作为排序的前缀时才有助于排序
复合索引(compound index)
每个索引项都包含年龄和用户名,并指向一个记录标识符。存储引擎在内部使用记录标识符来定位文档数据。
这意味着 MongoDB 需要在返回结果之前在内存中对结果进行排序,而不是简单地遍历已经按需排好序的索引。因此,这种类型的查询通常效率较低。
如果结果超过了 32MB,MongoDB 就会报错,拒绝对这么多数据进行排序。
每个计划会相互竞争一段时间(称为试用期),之后每一次竞赛的结果都会用来在总体上计算出一个获胜的计划。
让多个查询计划相互竞争的真正价值在于,对于具有相同形状的后续查询,MongoDB 会知道要选择哪个索引
其他会导致计划从缓存中被淘汰的事件有:重建特定的索引、添加或删除索引,或者显式清除计划缓存。此外,mongod 进程的重启也会导致查询计划缓存丢失
概括来说,在设计复合索引时:
• 等值过滤的键应该在最前面;
• 用于排序的键应该在多值字段之前;
• 多值过滤的键应该在最后面。
大多数使用 “$not” 的查询会退化为全表扫描 1。而 “$nin” 总是使用全表扫描。
如果不得不使用 “$or”,则要记住 MongoDB 需要检查两次查询的结果集并从中移除重复的文档(那些被多个 “$or” 子句匹配到的文档)。
5.1.6 索引对象和数组
- 索引内嵌文档
可以在内嵌文档的键上创建索引,方法与在普通键上创建索引相同。
可以在 “loc” 的其中一个子字段(如 “loc.city”)上创建索引,以提高这个字段的查询速度:
db.users.createIndex({“loc.city” : 1})
- 索引数组
也可以对数组创建索引,这样就能高效地查找特定的数组元素了。
对数组创建索引实际上就是对数组的每一个元素创建一个索引项
使得数组索引的代价比单值索引要高:对于单次的插入、更新或删除,每一个数组项可能都需要更新(也许会有上千个索引项)
数组元素上的索引并不包含任何位置信息:要查找特定位置的数组元素(如 “comments.4”),查询是无法使用索引的。
索引项中只有一个字段是可以来自数组的。这是为了避免在多键索引中的索引项数量爆炸式地增长
. 多键索引的影响
如果一个文档有被索引的数组字段,则该索引会立即被标记为多键索引。
5.3 何时不使用索引
结果集在原集合中所占的百分比越大,索引就会越低效,因为使用索引需要进行两次查找:一次是查找索引项,一次是根据索引的指针去查找其指向的文档。
根据经验,如果查询返回集合中 30% 或更少的文档,则索引通常可以加快速度。
表 5-1:影响索引效率的属性
5.4 索引类型
5.4.1 唯一索引
db.users.createIndex({“firstname” : 1}, … {“unique” : true, “partialFilterExpression”:{ “firstname”: {$exists: true } } } ) {
如果一个键不存在,那么索引会将其作为 null 存储。这意味着如果对某个键创建了唯一索引并试图插入多个缺少该索引键的文档,那么会因为集合中已经存在了一个该索引键值为 null 的文档而导致插入失败。
- 复合唯一索引
还可以创建复合唯一索引。在复合唯一索引中,单个键可以具有相同的值,但是索引项中所有键值的组合最多只能在索引中出现一次。
- 去除重复值
当尝试在现有集合中创建唯一索引时,如果存在任何重复值,
5.4.2 部分索引
如果一个字段可能存在也可能不存在,但当其存在时必须是唯一的,那么可以将 “unique” 选项与 “partial” 选项组合在一起使用。
要创建部分索引,需要包含 “partialFilterExpression” 选项
部分索引提供了稀疏索引功能的超集,使用一个文档来表示希望在其上创建索引的过滤器表达式。
5.5 索引管理
关于数据库索引的所有信息都存储在 system.indexes 集合中
5.5.1 标识索引
集合中的每个索引都有一个可用于标识该索引的名称,服务器端用这个名称来对其进行删除或者操作
索引名称的默认形式是 keyname1dir1_keyname2_dir2…_keynameN_dirN,其中 keynameX 是索引的键,dirX 是索引的方向(1 或 -1)。如果索引包含两个以上的键,那么这种方式就会很麻烦,因此可以将自己的名称指定为 createIndex 的选项之一:
db.soup.createIndex({“a” : 1, “b” : 1, “c” : 1, …, “z” : 1}, … {“name” : “alphabet”})
5.5.2 修改索引
db.people.dropIndex(“x_1_y_1”) { “nIndexesWas” : 3, “ok” : 1 }
如果可以选择,在现有文档中创建索引要比先创建索引然后插入所有文档中稍微快一些。
第 6 章 特殊的索引和集合类型
• 用于类队列数据的固定集合(capped collection);
• 用于缓存的 TTL 索引;
• 用于简单字符串搜索的全文本索引;
• 用于二维平面和球体空间的地理空间索引;
• 用于存储大文件的 GridFS。
6.4 TTL索引
如果需要更加灵活的过期机制,那么可以使用 TTL 索引,它允许为每一个文档设置一个超时时间。当一个文档达到其预设的过期时间之后就会被删除。这种类型的索引对于类似会话保存这样的缓存场景非常有用。
第 7 章 聚合框架
MongoDB 为使用聚合框架原生地进行分析提供了强大的支持
• 聚合框架
• 聚合阶段
• 聚合表达式
• 聚合累加器
7.1 管道、阶段和可调参数
聚合框架是 MongoDB 中的一组分析工具,可以对一个或多个集合中的文档进行分析。
聚合框架基于管道的概念。使用聚合管道可以从 MongoDB 集合获取输入,并将该集合中的文档传递到一个或多个阶段,每个阶段对其输入执行不同的操作(参见图 7-1)。每个阶段都将之前阶段输出的内容作为输入。所有阶段的输入和输出都是文档——可以称为文档流。

图 7-1:聚合管道
聚合管道中,一个阶段就是一个数据处理单元。它一次接收一个输入文档流,一次处理一个文档,并且一次产生一个输出文档流(参见图 7-2)。

图 7-2:聚合管道的阶段
每个阶段都会提供一组旋钮或可调参数(tunables),可以通过控制它们来设置该阶段的参数,以执行任何感兴趣的任务
7.2 阶段入门:常见操作
7.2 阶段入门:常见操作
匹配(match)、投射(project)、排序(sort)、跳过(skip)和限制(limit)这 5 个阶段。
db.companies.aggregate([ {$match: {founded_year: 2004}}, {$project: { _id: 0, name: 1, founded_year: 1 }} ])
首先注意 aggregate 方法的使用。这是要运行聚合查询时调用的方法。要进行聚合,就需要传入一个聚合管道。管道是一个以文档为元素的数组。每个文档必须规定一个特定的阶段运算符
例中使用了包含两个阶段的管道:一个是用于过滤的匹配阶段,另一个是投射阶段。在投射阶段中,每个文档的输出被限制为只有两个字段。
匹配阶段会对集合进行过滤,并将结果文档一次一个地传递到投射阶段。
然后投射阶段会执行其操作,调整文档形状,并从管道中将输出传递回来。
限制阶段。我们将使用相同的查询进行匹配,但是把结果集限制为 5,然后投射出想要的字段
db.companies.aggregate( {$match: {founded_year: 2004}}, {$limit: 5}, {$project: { _id: 0, name: 1}} ])
注意,构建的这条管道已在投射阶段之前进行限制。如果先运行投射阶段,然后再进行限制,那么就像下面的查询一样,将得到完全相同的结果,但这样就必须在投射阶段传递数百个文档,最后才能将结果限制为 5 个
无论 MongoDB 查询规划器在给定版本中进行何种类型的优化,都应该始终注意聚合管道的效率。
如果顺序很重要,那么就需要在限制阶段之前进行排序
db.companies.aggregate([ { $match: { founded_year: 2004 } }, { $sort: { name: 1} }, { $limit: 5 }, { $project: { _id: 0, name: 1 } } ])
最后,再将跳过阶段包含进来
db.companies.aggregate([ {$match: {founded_year: 2004}}, {$sort: {name: 1}}, {$skip: 10}, {$limit: 5}, {$project: { _id: 0, name: 1}}, ])