本文介绍云数据库 MongoDB 版创建索引的最佳实践,包括分析索引效率、优化索引选项以及如何针对具体查询创建高效索引。
创建索引
使用单键索引
如果所有查询都只查询单键,建议您创建单键索引。
使用复合索引
如果您需要查询单键和第二个键的组合,建议创建复合索引。MongoDB将对两个查询使用复合索引。例如,您可以同时创建category和item组合索引。
db.products.createIndex( { "category": 1, "item": 1 } )
使用文本(text)索引
常规索引用于匹配字段的整个值。如果您只想在包含大量文本的字段中匹配特定单词,则应使用文本索引来支持文本匹配。更多关于文本索引的介绍,请参见文本索引。
索引排序规则(Collation)
如果您需要使用索引进行字符串比较,则查询操作必须指定相同的排序规则。如果查询操作指定了不同的排序规则,则具有排序规则的索引无法支持对索引字段执行字符串比较的操作。
例如,集合在具有排序规则语言环境的字符串
myColl
字段上具有索引category "fr"
,示例如下:db.myColl.createIndex( { category: 1 }, { collation: { locale: "fr" } } )
下面的查询操作,指定了与索引相同的排序规则,就可以使用索引:
db.myColl.find( { category: "cafe" } ).collation( { locale: "fr" } )
以下默认使用
"simple"
二进制排序规则的查询操作,则不能使用索引:db.myColl.find( { category: "cafe" } )
对于索引前缀键不是字符串、数组和嵌入文档的复合索引,指定不同排序规则的操作仍然可以使用该索引来支持对索引前缀键的比较。更多关于排序规则的介绍,请参见Collation。
根据慢日志分析索引情况
MongoDB索引优化主要是为了降低集合的扫描量,所以主要关注慢日志中的DocsExamined和KeysExamined指标。
DocsExamined:表示该条查询扫描的文档个数。如果数量很大,表示数据库需要扫描大量的非索引条目,这种情况一般建议给扫描数量大的字段建索引。
KeysExamined:表示在该索引中扫描的key的个数。如果数量很大,但是返回的nreturned很小,则表示数据库扫描了大量的索引keys来得到结果文档,说明该索引不够高效,需要调整索引或创建其他索引。
索引优化思路如下:
全表扫描(关键字:COLLSCAN、DocsExamined)
全集合(表)扫描COLLSCAN 。当执行一个操作请求(如查询、更新、删除等)后,您在查看慢请求日志时发现COLLSCAN关键字,建议对查询的字段建立索引的方式来优化。
通过查看DocsExamined的值,可以了解到一个查询扫描了多少文档。该值越大,请求所占用的CPU开销越大,后续排队的阻塞越严重。
不合理的索引(关键字:IXSCAN、keysExamined)
通过查看keysExamined字段,可以查看到一个使用了索引的查询,扫描了多少条索引。该值越大,CPU开销越大。
如果索引建立得不太合理,或者是匹配的结果很多,即使使用索引,请求开销也不会优化很多,执行的速度也会很慢。
当在慢日志里发现SORT关键字时,可以考虑通过索引来优化排序。更多说明,请参见The ESR (Equality, Sort, Range) Rule。
索引优化建议
尽量使用覆盖查询(Covered Queries)
覆盖查询是直接从索引中返回结果,无需访问源文档,非常高效。要确定查询是否是覆盖查询,可以使用explain()
。如果explain()
的输出显示totalDocsExamined为0,说明查询是由索引覆盖的。
在尝试实现覆盖查询时,有一个常见的陷阱是_id字段默认始终返回。您需要明确地将其从查询结果中排除,或者将其添加到索引中。
在分片集群中,MongoDB内部需要访问分片键的字段。因此,只有在分片键是索引的一部分时,覆盖查询才可行。通常情况下,最好将分片键也作为索引的一部分。
去除冗余索引
索引是资源密集型的,即使在MongoDB的WiredTiger存储引擎中使用压缩,它们也会消耗RAM和磁盘。此外,随着字段的更新,相关的索引也必须进行维护,这会增加额外的CPU和磁盘I/O负载。因此,我们应该谨慎评估和删除不再需要的索引。
复合索引建议
多个字段的复合查询,字段顺序不一样时,也都是属于一类查询,您只需要建一个。比如索引
{a:1, b:1}
和{b:1, a:1}
只需存在一个。包含关系引起的冗余索引:比如有如下两个查询:
db.myCol.find({"b": 2, "c": 3})
db.myCol.find({"a": 1, "b": 2, "c": 3})
查询2中包含查询1中的字段,您可以只用一个索引满足这两个查询要求,并且将被包含的查询的字段放到最左边,即索引应为
{b: 1. c: 1. a: 1}
。唯一索引和其它字段组合导致的冗余索引,比如有如下两个查询:
db.myCol.find({"a": 1, "b": 1})
db.myCol.find({"a": 1, "c": 1})
如果a字段取值是唯一的,那么这两个查询中除a外的字段建索引没用,只需要建索引
{a: 1}
。
非等值索引建议
非等值组合查询索引创建不合理,比如如下查询:
db.myCol.find({"a": {$gte: 1} , "b": {$lte: 1}})
这种多字段的非等值查询,只有最左边的字段才能走索引,这里只会走a字段的索引,您只需对a字段建索引。
等值+非等值查询组合,比如如下查询:
db.myCol.find({"a": {$gte: 1} , "b": 1})
这种情况最优索引应该把等值查询放到左边,即应该建索引
{b: 1, a: 1}
。
$or类查询索引建议
$or类查询需要对各个条件分别建立索引,比如如下查询:
db.myCol.find({$or: [{"a": 1, "b": 1}, {"c": 1, "d": 1}]})
您需要针对$or中两个条件f分别建立最优索引,即{a: 1, b: 1}
和{c: 1, d: 1}
,而不是{a: 1, b: 1, c: 1, d: 1}
。
Sort类查询索引建议
同一字段不同Sort查询只需要建一个索引,比如如下查询:
db.myCol.find({}).sort({"a":1})
db.myCol.find({}).sort({"a":-1})
您只需要建索引
{a: 1}
。多字段Sort查询,比如如下查询:
db.myCol.find({}).sort({"a":1, "b": -1})
索引
{a: 1, b: 1}
是无效的,必须要创建{a: 1, b: -1}
的索引才有效。等值查询+非等值查询+Sort查询,比如如下查询:
db.myCol.find({"a": 1, "b": 2, "c": {$gte: 1}}).sort({"d": 1, "e": -1})
建设索引的字段顺序为:
等值->sort->非等值
,即{a: 1, b: 1, d: 1, e: -1, c: 1}
。$or+Sort查询,比如如下查询:
db.myCol.find({$or: [{"a": 1, "b": 1}, {"c": 1, "d": 1}]}).sort({"e": -1})
该查询可以拆分为
db.myCol.find({"a": 1, "b": 1}).sort({"e":-1})
和db.myCol.find({"c": 1, "d": 1}).sort({"e":-1})
两个查询,按照等值查询+非等值查询+Sort查询中规则,应该建索引{a: 1, b: 1, e: -1}
和{c: 1, d: 1, e: -1}
。
使用映射仅返回需要的字段
当您只需要文档中的部分字段时,可通过仅返回所需字段来实现更优性能。
例如,在针对posts集合的查询中,您仅需timestamp、title、author和abstract字段,则可使用以下查询命令:
db.posts.find( {}, { timestamp : 1 , title : 1 , author : 1 , abstract : 1} ).sort( { timestamp : -1 } )
使用hint()返回特定索引
大多数情况下,查询优化器会为特定操作选择最佳索引。特殊情况下,您也可以使用hint()
方法强制MongoDB使用特定索引。
例如,使用hint()
来支持性能测试,或将其用于必须选择某一字段或包含在多个索引中的某一字段的某些查询。
使用部分索引
通过仅包含将通过索引访问的文档来减小索引的大小和性能开销。
例如,在orderID字段上创建部分索引,只包括orderStatus为“正在进行中”的订单文档,或者仅在文档中存在emailAddress字段时创建索引。