indexing-strategies

应用程序的最佳索引必须考虑许多因素,包括您期望的查询类型,读取与写入的比率以及系统上的可用内存量。

在开发索引策略时,您应该深入了解应用程序的查询。在构建索引之前,请指出要运行的查询类型,以便构建引用这些字段的索引。

索引具有性能成本,但是对于大型数据集上的频繁查询而言,它们的价值更高。考虑应用程序中每个查询的相对频率以及查询是否证明索引是合理的。

设计索引的最佳总体策略是使用与您将在生产中运行的数据集类似的数据集来分析各种索引配置,以查看哪些配置性能最佳。检查为集合创建的当前索引,以确保它们支持您当前和计划的查询。如果不再使用索引,请删除索引。

通常,MongoDB仅使用一个索引来完成大多数查询。

但是,$or 查询的每个子句可以使用不同的索引,从2.6开始,MongoDB可以使用多个索引的交集。

为查询创建索引

如果所有查询都使用相同的单个键, 则创建单键索引

如果您只查询给定集合中的单个键, 则只需要为该集合创建一个单键索引。

例如, 您可以在产品集合中创建类别的索引:

db.products.createIndex( { "category": 1 } )

创建复合索引以支持多个不同的查询

如果有时只查询一个键, 有时查询该键与第二个键组合, 则创建复合索引比创建单键索引更有效。

mongodb 将对这两个查询使用复合索引。

例如, 您可以在类别和项上创建索引。

db.products.createIndex( { "category": 1, "item": 1 } )

这允许您使用这两个选项。您可以只查询类别, 也可以查询与项目组合的类别。

多个字段上的单个复合索引可以支持搜索这些字段的 “前缀” 子集的所有查询。

案例

  • collection 的索引假设如下
{ x: 1, y: 1, z: 1 }
  • 下面的查询都会命中索引
{ x: 1 }
{ x: 1, y: 1 }

在某些情况下, 前缀索引可能会提供更好的查询性能: 例如, 如果 z 是一个大数组。

{x:1, y:1, z:1} 索引还可以支持与以下索引相同的许多查询:

{ x: 1, z: 1 }

此外, {x:1, z:1} 还有其他用途。给定以下查询:

db.collection.find( { x: 5 } ).sort( { z: 1} )

{x:1, z:1} 索引同时支持查询和排序操作, 而 {x:1, y:1, z:1} 索引仅支持查询。

有关排序的详细信息, 请参阅使用索引对查询结果进行排序。

从2.6 版开始, mongodb 可以使用索引交集来完成查询。

创建支持查询的复合索引还是依赖于索引交集的选择取决于系统的具体情况。

有关详细信息, 请参阅索引交集和复合索引。

索引使用和排序规则

若要使用索引进行字符串比较, 操作还必须指定相同的排序规则。

也就是说, 如果某个操作指定了不同的排序规则, 则具有排序规则的索引不能支持在索引字段上执行字符串比较的操作。

例如, 集合 mycoll 在字符串字段类别上有一个索引, 其排序规则区域设置为 “fr”。

db.myColl.createIndex( { category: 1 }, { collation: { locale: "fr" } } )
  • 命中索引
db.myColl.find( { category: "cafe" } ).collation( { locale: "fr" } )
  • 下面的查询不命中
db.myColl.find( { category: "cafe" } )

复合索引

对于索引前缀键不是字符串、数组和嵌入文档的复合索引, 指定不同排序规则的操作仍然可以使用索引来支持索引前缀键上的比较。

例如, 集合 mycoll 在数字字段分数和价格以及字符串字段类别上有一个复合索引;索引是使用排序规则区域设置 “fr” 创建的, 用于字符串比较:

db.myColl.createIndex(
   { score: 1, price: 1, category: 1 },
   { collation: { locale: "fr" } } )

以下操作使用 “简单” 二进制排序规则进行字符串比较, 可以使用索引:

db.myColl.find( { score: 5 } ).sort( { price: 1 } )
db.myColl.find( { score: 5, price: { $gt: NumberDecimal( "10" ) } } ).sort( { price: 1 } )

下面的操作使用 “简单” 二进制排序规则对索引类别字段中的字符串进行比较, 它可以使用索引来仅完成查询的分数:

db.myColl.find( { score: 5, category: "cafe" } )

使用索引去排序查询结果

在 mongodb 中, 排序操作可以通过检索基于索引中的排序的文档来获取排序顺序。

如果查询计划程序无法从索引中获取排序顺序, 它将对内存中的结果进行排序。

使用索引的排序操作通常比不使用索引的操作具有更好的性能。

此外, 不使用索引的排序操作在使用32兆字节的内存时将中止。

注意

由于 mongodb 3.6 中数组字段的排序行为发生了变化, 在对带有多键索引的数组进行排序时, 查询计划包括阻塞的 sort 阶段。

新的排序行为可能会对性能产生负面影响。

在阻塞的 sort 中, 所有输入都必须通过排序步骤来使用, 然后才能产生输出。

在非阻塞或索引排序中, 排序步骤将扫描索引, 以按请求的顺序生成结果。

单个索引字段排序

如果升序或下降索引位于单个字段上, 则该字段上的排序操作可以在任一方向。

例如, 在集合记录的字段 a 上创建一个升序索引:

db.records.createIndex( { a: 1 } )

索引支持下面的排序:

db.records.find().sort( { a: 1 } )

也支持下面的查询,通过反转数据:

db.records.find().sort( { a: -1 } )

多个索引字段排序

创建复合索引以支持对多个字段进行排序。

您可以指定对索引的所有键或子集的排序;但是, 排序键的列出顺序必须与它们在索引中显示的顺序相同。例如, 索引键模式 {a:1, b:1} 可以支持对 {a:1, b:1} 的排序, 但不能支持 {b:1, a: 1}。

要使查询对排序使用复合索引, cursors.sort()文档中所有键的指定排序方向必须与索引键模式匹配或与索引键模式的反转匹配。例如, 索引键模式 {a:1, b:-1} 可以支持对 {a:1, b:-1} 和 {a:-1, b:1}, 但不在 {a:1, b:-1} 或 {a:1, b:1}。

Sort and Index Prefix

如果排序键与索引键或索引前缀相对应, mongodb 可以使用索引对查询结果进行排序。

复合索引的前缀是由索引键模式开头的一个或多个键组成的子集。

例如, 在数据 collection 上创建复合索引:

db.data.createIndex( { a:1, b: 1, c: 1, d: 1 } )

下面的都是前缀索引:

{ a: 1 }
{ a: 1, b: 1 }
{ a: 1, b: 1, c: 1 }

下面的查询和排序操作使用索引前缀对结果进行排序。

这些操作不需要对内存中的结果集进行排序。

查询 前缀索引
db.data.find().sort( { a: 1 } ) { a: 1 }
db.data.find().sort( { a: -1 } ) { a: 1 }
db.data.find().sort( { a: 1, b: 1 } ) { a: 1, b: 1 }
db.data.find().sort( { a: -1, b: -1 } ) { a: 1, b: 1 }
db.data.find().sort( { a: 1, b: 1, c: 1 } ) { a: 1, b: 1, c: 1 }
db.data.find({ a: { $gt: 4 } } ).sort( { a: 1, b: 1 }) { a: 1, b: 1 }

请考虑下面的示例, 其中索引的前缀键同时出现在查询谓词和排序中:

db.data.find( { a: { $gt: 4 } } ).sort( { a: 1, b: 1 } )

在这种情况下, mongodb 可以使用索引按排序指定的顺序检索文档。

如示例所示, 查询谓词中的索引前缀可以不同于排序中的前缀。

Sort and Non-prefix Subset of an Index

索引可以支持索引键模式的非前缀子集上的排序操作。

为此, 查询必须在排序键之前的所有前缀键上包含相等条件。

例如, collection 数据具有以下索引:

{ a: 1, b: 1, c: 1, d: 1 }
查询 索引
db.data.find( { a: 5 } ).sort( { b: 1, c: 1 } ) { a: 1 , b: 1, c: 1 }
db.data.find( { b: 3, a: 4 } ).sort( { c: 1 } ) { a: 1, b: 1, c: 1 }
db.data.find( { a: 5, b: { $lt: 3} } ).sort( { b: 1 } ) { a: 1, b: 1 }

如最后一个操作所示, 只有排序子集之前的索引字段必须在查询文档中具有相等条件;其他索引字段可以指定其他条件。

如果查询未指定排序规范之前的索引前缀上的相等条件, 则操作将无法有效地使用该索引。

例如, 以下操作指定 {c: 1} 的排序文档, 但查询文档在前面的索引字段 a 和 b 上不包含相等匹配:

db.data.find( { a: { $gt: 2 } } ).sort( { c: 1 } )
db.data.find( { c: 5 } ).sort( { c: 1 } )

这些查询就不会很好的命中索引。

索引使用和排序规则

若要使用索引进行字符串比较, 操作还必须指定相同的排序规则。

也就是说, 如果某个操作指定了不同的排序规则, 则具有排序规则的索引不能支持在索引字段上执行字符串比较的操作。

ps: 这个前面已经提及过,就不再赘述。

对于索引前缀键不是字符串、数组和嵌入文档的复合索引, 指定不同排序规则的操作仍然可以使用索引来支持索引前缀键上的比较。

例如, 集合 mycoll 在数字字段分数和价格以及字符串字段类别上有一个复合索引;索引是使用排序规则区域设置 “fr” 创建的, 用于字符串比较:

db.myColl.createIndex(
   { score: 1, price: 1, category: 1 },
   { collation: { locale: "fr" } } )
  • 场景 1

以下操作使用 “简单” 二进制排序规则进行字符串比较, 可以使用索引:

db.myColl.find( { score: 5 } ).sort( { price: 1 } )
db.myColl.find( { score: 5, price: { $gt: NumberDecimal( "10" ) } } ).sort( { price: 1 } )

下面的操作使用 “简单” 二进制排序规则对索引类别字段中的字符串进行比较, 它可以使用索引来仅完成查询的分数:

db.myColl.find( { score: 5, category: "cafe" } )

确保索引适合 RAM

为了最快的处理, 请确保索引完全适合 ram, 以便系统可以避免从磁盘读取索引。

  • 查看数据库索引大小
db.explain.totalIndexSize();

49152

上面的示例显示了近 49152 字节的索引大小。

为了确保此索引适合 ram, 您不仅必须有更多的可用 ram, 而且还必须有可用于工作集其余部分的 ram。

还请记住:

如果有并使用多个集合, 则必须考虑所有集合上所有索引的大小。索引和工作集必须能够同时放入内存中。

在某些情况下, 索引不需要放入内存中。

请参阅在 ram 中只保存最近值的索引

索引不必在所有情况下都完全适合 ram。

如果索引字段的值随每次插入而递增, 并且大多数查询选择最近添加的文档;

然后 mongodb 只需要保留索引中在 ram 中保存最新或 “最正确” 值的部分。

这样可以有效地使用用于读取和写入操作的索引, 并最大限度地减少支持索引所需的 ram 量。

创建确保选择性的查询

选择性是查询使用索引缩小结果范围的能力。

有效索引更具选择性, 允许 mongodb 将索引用于与实现查询相关的大部分工作。

为确保选择性, 请使用索引字段编写限制可能文档数量的查询。

编写相对于索引数据具有适当选择性的查询。

例子 1

假设您有一个名为 “状态” 的字段, 其中可能的值是新的并已处理。

如果添加状态索引, 则创建了低选择性索引。该索引在查找记录方面作用不大。

根据您的查询, 更好的策略是创建一个包含低选择性字段和另一个字段的复合索引。

例如, 您可以创建状态的复合索引并创建 _at。

另一个选项 (同样取决于您的用例) 可能是使用单独的集合, 每个状态一个。

例子 2

考虑集合上的索引 {a:1} (即按升序排序的键上的索引), 其中一个集合中的三个值均匀分布在集合中:

{ _id: ObjectId(), a: 1, b: "ab" }
{ _id: ObjectId(), a: 1, b: "cd" }
{ _id: ObjectId(), a: 1, b: "ef" }
{ _id: ObjectId(), a: 2, b: "jk" }
{ _id: ObjectId(), a: 2, b: "lm" }
{ _id: ObjectId(), a: 2, b: "no" }
{ _id: ObjectId(), a: 3, b: "pq" }
{ _id: ObjectId(), a: 3, b: "rs" }
{ _id: ObjectId(), a: 3, b: "tv" }

如果查询 {a:2, b: “no”} mongodb 必须扫描集合中的3个文档, 以返回一个匹配的结果。

同样, 查询 {a:{$gt: 1}, b: “tv”} 必须扫描6个文档, 也要返回一个结果。

考虑集合上的相同索引, 其中 a 在集合中均匀分布了九个值:

{ _id: ObjectId(), a: 1, b: "ab" }
{ _id: ObjectId(), a: 2, b: "cd" }
{ _id: ObjectId(), a: 3, b: "ef" }
{ _id: ObjectId(), a: 4, b: "jk" }
{ _id: ObjectId(), a: 5, b: "lm" }
{ _id: ObjectId(), a: 6, b: "no" }
{ _id: ObjectId(), a: 7, b: "pq" }
{ _id: ObjectId(), a: 8, b: "rs" }
{ _id: ObjectId(), a: 9, b: "tv" }

如果查询 {a:2, b: “cd”}, mongodb 必须只扫描一个文档才能完成查询。索引和查询更具选择性, 因为 a 的值分布均匀, 查询可以使用索引选择特定文档。

但是, 尽管 a 上的索引更具选择性, 但诸如 {a: {$gt: 5}、b: “tv”} 之类的查询仍需要扫描4个文档。

如果总体选择性较低, 并且 mongodb 必须读取多个文档才能返回结果, 则某些查询在没有索引的情况下执行速度可能会更快。

若要确定性能, 请参阅测量索引使用情况。

参考资料

indexing-strategies