前言
本章介绍 MongoDB 的索引。索引使你能够高效地执行查询。它们是应用程序开发的重要组成部分,甚至对于某些类型的查询是必需的。
索引简介
数据库索引类似于图书索引。有了索引便不需要浏览整本书,只查看一个有内容引用的有序列表。这使得 MongoDB 的查找速度提高了好几个数量级
。
不使用索引的查询称为集合扫描
,这意味着服务器端必须“浏览整本书”才能得到查询的结果。
查询模式
为了使 MongoDB 高效地响应查询,应用程序中的所有查询模式都应该有索引支持
。
所谓查询模式,是指应用程序向数据库提出的不同类型的问题
。例如按用户名查询 users 集合。这就是一个特定查询模式的示例。
创建索引
创建索引可以使用 createIndex 集合方法。
> db.users.createIndex({"username" : 1})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}
创建索引只需几秒的时间,除非集合特别大。如果 createIndex 调用在几秒后没有返回,则可以运行 db.currentOp()
(在另一个 shell 中)或检查 mongod 的日志
以查看索引创建的进度。
索引可以显著缩短查询时间。然而,使用索引是有代价的
:修改索引字段的写操作(插入、更新和删除)会花费更长的时间。这是因为在更改数据时,除了更新文档,MongoDB 还必须更新索引
。
要选择为哪些字段创建索引,可以查看常用的查询、需要快速执行的查询以及遇到性能瓶颈的查询,并尝试从中找到一组通用的键。
复合索引简介
对于许多查询模式来说,在两个或更多的键上创建索引是必要的。
例如,可以在 "age" 和"username" 上创建索引,如果查询中有多个排序方向或者查询条件中有多个键,那么这个索引会非常有用。
> db.users.createIndex({"age" : 1, "username" : 1})
上例创建的称为复合索引
(compound index)。复合索引是创建在多个字段上的索引。
MongoDB如何选择索引
当有查询进来时,MongoDB 会查看这个查询的形状。这个形状与要搜索的字段和一些附加信息(比如是否有排序)有关。基于这些信息,系统会识别出一组可能用于满足查询的候选索引。
假设有一个查询进入,5 个索引中的 3 个被标识为该查询的候选索引。然后,MongoDB 会创建 3 个查询计划,分别为每个索引创建 1 个,并在 3 个并行线程中运行此查询,每个线程使用不同的索引。这样做的目的是看哪一个能够最快地返回结果。可以将其看作一场竞赛,到达目标状态的第一个查询计划成为赢家。但更重要的是,以后会选择它作为索引,用于具有相同形状的其他查询。每个计划会相互竞争一段时间(称为试用期),之后每一次竞赛的结果都会用来在总体上计算出一个获胜的计划。
使用复合索引
在考虑复合索引的设计时,需要知道对于利用索引的通用查询模式,如何处理其等值过滤
、多值过滤
以及排序
这些部分。对于所有复合索引都必须考虑这 3 个因素,而且如果在设计索引时可以正确地平衡这些关注点,那么你的查询就会从MongoDB 中获得最佳的性能。
为了避免内存排序,需要检查比返回的文档数量更多的键,这对于复合索引来说往往是必需的。为了使用索引进行排序,MongoDB 应该能够按顺序遍历索引键。这意味着需要在复合索引键中包含排序字段。
概括来说,在设计复合索引时:
- 等值过滤的键应该在最前面
- 用于排序的键应该在多值字段之前
- 多值过滤的键应该在最后面。
在设计复合索引时遵循这些准则,然后在实际工作负载下进行测试,这样就可以确定索引所支持的查询模式有哪些了。
选择键的方向
只有基于多个查询条件进行排序时,索引方向才是重要的。如果只是基于一个键进行排序,那么 MongoDB 可以简单地从相反方向读取索引。
使用覆盖查询
当一个索引包含用户请求的所有字段时,这个索引就覆盖了本次查询。只要切实可行,就应该优先使用覆盖查询
,而不是去获取实际的文档,这样可以使工作集大幅减小。
隐式索引
复合索引具有“双重功能”,而且针对不同的查询可以充当不同的索引。如果有一个拥有 N 个键的索引,那么你同时“免费”得到了所有这些键的前缀所组成的索引。如果有一个类似 {"a": 1, "b": 1, "c": 1 ..., "z": 1} 这样的索引,那么实际上也等于有了 {"a": 1}、{"a": 1, "b" : 1}、{"a": 1,"b": 1, "c": 1} 等一系列索引。
$运算符如何使用索引
有些查询可以比其他查询更高效地使用索引,有些查询则根本不能使用索引。
低效的运算符
部分运算符如"$ne"、"$not"会扫描整个索引或大部分索引,效率较低。
如果需要快速执行这些类型的查询,可以尝试看看是否能找到另一个使用索引的语句,将其添加到查询中,这样就可以在MongoDB 进行无索引匹配时先将结果集的文档数量减少到一个比较小的数量。
范围
复合索引使 MongoDB 能够高效地执行具有多个子句的查询。当设计基于多个字段的索引时,应该将用于精确匹配的字段(如 "x" : 1)放在最前面,将用于范围匹配的字段(如 "y": {"lt" : 5})放在最后面。
"OR查询"
如果在 {"x": 1} 上有一个索引,在 {"y" : 1} 上有另一个索引,然后在 {"x" : 123, "y" : 456} 上进行查询时,MongoDB 会使用其中一个索引,而不是两个一起使用。唯一的例外是 "$or",每个 "$or" 子句都可以使用一个索引,因为实际上 "$or" 是执行两次查询然后将结果集合并。
索引对象和数组
MongoDB 允许深入文档内部,对内嵌字段和数组创建索引。内嵌对象和数组字段可以和顶级字段一起在复合索引中使用。尽管在某些方面比较特别,但是它们的大多数行为与“普通”索引字段是一致的。
索引内嵌文档
可以在内嵌文档的键上创建索引,方法与在普通键上创建索引相同。也可以对任意深层次的字段(如 "x.y.z.w.a.b.c")创建索引。
对整个子文档创建索引只会提高针对整个子文档进行查询的速度。只有在进行与子文档字段顺序完全匹配
的查询时,查询优化器才能使用子文档上的索引。
索引数组
- 整个数组是无法作为一个实体创建索引的:对数组创建索引就是对数组中的每个元素创建索引,而不是对数组本身创建索引。
- 数组元素上的索引并不包含任何位置信息:要查找特定位置的数组元素,查询是无法使用索引的。
索引基数
基数
(cardinality)是指集合中某个字段有多少个不同的值。
- 通常来说,一个字段的基数越高,这个字段上的索引就越有用。这是因为这样的索引能够迅速将搜索范围缩小到一个比较小的结果集。
- 根据经验来说,应该在基数比较高的键上创建索引,或者至少应该把基数比较高的键放在复合索引的前面(在低基数的键之前)。
explain输出
explain 可以为查询提供大量的信息。对于慢查询来说,它是最重要的诊断工具之一。
最常见的 explain 输出有两种类型:使用索引的查询和未使用索引的查询。
使用explain时会输出以下信息字段:
- isMultiKey:本次查询是否使用了多键索引
- nReturned: 本次查询返回的文档数量。
- totalDocsExamined: MongoDB 按照索引指针在磁盘上查找实际文档的次数。如果查询中包含的查询条件不是索引的一部分,或者请求的字段没有包含在索引中,MongoDB 就必须查找每个索引项所指向的文档。
- totalKeysExamined: 如果使用了索引,那么这个数字就是查找过的索引条目数量。如果本次查询是一次全表扫描,那么这个数字就表示检查过的文档数量。
- stage:MongoDB 是否可以使用索引完成本次查询。如果不可以,那么会使用"COLLSCAN" 表示必须执行集合扫描来完成查询。
- needYield:为了让写请求顺利进行,本次查询所让步(暂停)的次数。如果有写操作在等待执行,那么查询将定期释放它们的锁以允许写操作执行。在本次查询中,由于并没有写操作在等待,因此查询永远不会进行让步。
- executionTimeMillis:数据库执行本次查询所花费的毫秒数。这个数字越小越好。
- indexBounds:这描述了索引是如何被使用的,并给出了索引的遍历范围。
何时不适用索引
索引在提取较小的子数据集时是最高效的,而有些查询在不使用索引时会更快。结果集在原集合中所占的百分比越大,索引就会越低效,因为使用索引需要进行两次查找:一次是查找索引项,一次是根据索引的指针去查找其指向的文档。而全表扫描只需进行一次查找:查找文档。
没有一个固定的规则,因为这实际上取决于数据、索引、文档和平均结果集的大小。
索引类型
在创建索引时可以指定一些选项来改变索引的行为方式。
唯一索引
唯一索引确保每个值最多只会在索引中出现一次。如果想保证不同文档的"firstname" 键拥有不同的值,则可以使用 partialFilterExpression 仅为那些有firstname 字段的文档创建唯一索引。
复合唯一索引
在复合唯一索引中,单个键可以具有相同的值,但是索引项中所有键值的组合最多只能在索引中出现一次。
GridFS 是在 MongoDB 中存储大文件的标准方式,它就用到了复合唯一索引。
去除重复值
当尝试在现有集合中创建唯一索引时,如果存在任何重复值,则会导致创建失败,通常需要使用聚合框架对数据进行处理。
部分索引
在很多情况下,你可能希望仅在键存在时才强制执行唯一索引。如果一个字段可能存在也可能不存在,但当其存在时必须是唯一的,那么可以将 "unique"
选项与 "partial"
选项组合在一起使用。
要创建部分索引,需要包含 "partialFilterExpression" 选项。部分索引提供了稀疏索引功能的超集,使用一个文档来表示希望在其上创建索引的过滤器表达式。
使用了部分索引之后查询某一字段会忽略没有该字段的文档。
索引管理
关于数据库索引的所有信息都存储在 system.indexes 集合中。这是一个保留集合,因此不能修改其中的文档或从中删除文档。只能通过 createIndex
、createIndexes
和 dropIndexes
数据库命令来对它进行操作。
标识索引
- 集合中的每个索引都有一个可用于标识该索引的名称,服务器端用这个名称来对其进行删除或者操作。
- 索引名称的默认形式是keyname1dir1_keyname2_dir2..._keynameN_dirN,其中 keynameX 是索引的键,dirX 是索引的方向(1 或 -1)。
- 其中 keynameX 是索引的键,dirX 是索引的方向(1 或 -1)。如果索引包含两个以上的键,那么这种方式就会很麻烦,因此可以将自己的名称指定为 createIndex 的选项之一。
- 索引名称是有字符数限制的,因此在创建复杂的索引时可能需要自定义名称。调用getLastError 就可以知道索引是否创建成功,或者为什么创建失败。
修改索引
随着应用程序不断变化,你可能会发现数据或者查询已经发生了改变,可以使用 dropIndex
命令删除不再需要的索引。