MongoDB复合索引——排序顺序是否重要?

3

最近我在为我的一个项目深入学习mongodb。我一直在阅读关于索引的内容,对于小集合来说,我知道这并不重要,但是当它增长时,如果没有正确的索引和查询,就会出现性能问题。

假设我有一个这样的集合:

{user_id:1,slug:'one-slug'}
{user_id:1,slug:'another-slug'}
{user_id:2,slug:'one-slug'}
{user_id:3,slug:'just-a-slug}

我需要搜索我的收藏,以确定:
user id == 1 and slug == 'one-slug'

在这个集合中,蛞蝓将唯一对应用户id。 也就是说,用户id 1 只能有一个值为 'one-slug' 的蛞蝓。
我知道由于其高基数,应该优先考虑用户id,但蛞蝓呢?由于它通常也是唯一的。我还没有理解升序和降序索引,或者它如何影响性能以及我应该在这个集合中使用的正确顺序。
我已经阅读了一些资料,但我无法理解,特别是对于我的情况。很希望能听听其他人的意见。

排序顺序?但是您没有进行任何排序。 - Sergio Tulentsev
来自手册的@SergioTulentsev: 复合索引中列出的字段顺序很重要。索引将包含对文档的引用,这些文档首先按照item字段的值排序,对于每个item字段的值,再按照stock字段的值进行排序。 - Haider Ali
1
只有在排序时索引方向才有影响。例如,db.posts.find({user_id: 1}).order({slug: -1})(https://docs.mongodb.com/manual/tutorial/sort-results-with-indexes/#sort-on-multiple-fields)。对于相等性查询,索引方向并不重要。 - Sergio Tulentsev
@SergioTulentsev 谢谢,我猜对于我的情况,当我创建索引时,用户ID应该放在第一位。但是,能否请您解释一下 {user_id:1,slug:1} 和 {slug:1,user_id:1} 之间的区别? - Haider Ali
这个的解释在我链接到的同一部分中。 - Sergio Tulentsev
本博客文章清晰地解释了复合索引:https://emptysqua.re/blog/optimizing-mongodb-compound-indexes/ - kevinadi
2个回答

14

你可以将MongoDB的单字段索引看作一个数组,其中包含指向文档位置的指针。例如,如果有一个集合(注意,顺序是故意打乱的):

[collection]
1: {a:3, b:2}
2: {a:1, b:2}
3: {a:2, b:1}
4: {a:1, b:1}
5: {a:2, b:2}

单字段索引

现在,如果您执行以下操作:

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

该指数大致如下:

[index a:1]
1: {a:1} --> 2, 4
2: {a:2} --> 3, 5
3: {a:3} --> 1

请注意以下三点:

  • a 升序排序
  • 每个条目指向相关文档所在的位置
  • 索引仅记录 a 字段的值。索引中根本不存在 b 字段

因此,如果您进行如下查询:

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

它所要做的就是从上到下遍历索引,提取并输出由该条目指向的文档。请注意,您也可以从底部遍历索引,例如:

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

唯一的区别是您反向遍历索引。

由于 b 根本不在索引中,因此在查询有关 b 的任何内容时都不能使用索引。

复合索引

在复合索引中,例如:

db.collection.createIndex({a:1, b:1})

这意味着你想先按a排序,然后再按b排序。索引会像这样:

[index a:1, b:1]
1: {a:1, b:1} --> 4
2: {a:1, b:2} --> 2
3: {a:2, b:1} --> 3
4: {a:2, b:2} --> 5
5: {a:3, b:2} --> 1

请注意:

  • 索引从a开始排序
  • 在每个a中,您拥有一个排序后的b
  • 相比之前单字段示例中的3个索引条目,您现在有5个索引条目

使用此索引,您可以进行以下查询:

db.collection.find({a:2}).sort({b:1})

它可以轻松找到 a:2 的位置,然后向前遍历索引。 但是,在给定的索引上,您不能执行以下操作

db.collection.find().sort({b:1})
db.collection.find({b:1})

在这两个查询中,你无法轻易地找到 b,因为它分布在整个索引中(即不在连续的条目中)。但是你可以进行以下操作:

db.collection.find({a:2}).sort({b:-1})

因为你可以找到 a:2 的位置,并向后遍历 b 条目。

编辑: 回答@marcospgp在评论中的问题的澄清:

使用索引 {a:1, b:1} 满足 find({a:2}).sort({b:-1}) 的可能性实际上是有意义的,如果你从排序表的角度来看的话。例如,索引 {a:1, b:1} 可以被认为是:

a | b
--|--
1 | 1
1 | 2
2 | 1
2 | 2
2 | 3
3 | 1
3 | 2

find({a:2}).sort({b:1})

索引 {a:1, b:1} 表示 按照 a 排序,然后在每个 a 中,对 b 值进行排序。如果你执行 find({a:2}).sort({b:1}),索引就知道所有 a=2 在哪里。在这个 a=2 块中,b 按照升序排序(根据索引规范),所以查询 find({a:2}).sort({b:1}) 可以被满足:

a | b
--|--
1 | 1
1 | 2
2 | 1 <-- walk this block forward to satisfy
2 | 2 <-- find({a:2}).sort({b:1})
2 | 3 <--
3 | 1
3 | 2

find({a:2}).sort({b:-1})

由于索引可以向前或向后遍历,因此需要遵循类似的过程,并在最后进行一些小的调整:

a | b
--|--
1 | 1
1 | 2
2 | 1  <-- walk this block backward to satisfy
2 | 2  <-- find({a:2}).sort({b:-1})
2 | 3  <--
3 | 1
3 | 2

索引可正向或反向遍历是实现查询find({a:2}).sort({b:-1})使用索引{a:1, b:1}的关键点。

查询计划器说明

您可以通过使用db.collection.explain().find(....)查看查询规划器的计划。如果您看到stageCOLLSCAN,则该查询未使用索引或无法使用索引。有关命令输出的详细信息,请参见explain results


根据手册,这个答案是不正确的。你无法“找到a:2在哪里,并向后遍历b条目”。这似乎有点傻,为什么索引必须是线性的呢?直接嵌套值不就行了吗?但看起来它是这样构建的:https://docs.mongodb.com/manual/core/index-compound/#sort-order - Marcos Pereira
@marcospgp 你是怎么得出这个结论的?你可以通过 db.collection.explain().find({a:1}).sort({b:-1}) 来检查我的答案是否正确。如果输出中看到了 COLLSCAN,那就意味着索引没有被使用。如果看到了 IXSCAN,那就意味着索引被使用了。 - kevinadi
这就是我包含的链接所说的内容。也许文档有误。 - Marcos Pereira
@marcospgp,你能指出一下文档中你所参考的部分吗?我试图理解你的观点。诚然,我在回答中提出的案例实际上是一个微妙的问题,因为文档中的示例使用两个字段进行排序,而我只使用了其中一个字段进行排序。 - kevinadi
你的示例与文档略有不同,而且文档没有提到那种情况。尽管如此,我期望如果你不能按 { a: 1, b: -1 } 进行排序,那么在使用特定的 a 查询后,你也无法按 { b: -1 } 进行排序。不确定!文档比应该更具体。 - Marcos Pereira
显示剩余5条评论

0

[由于声望不足,无法发表评论]

只有在排序时索引方向才有影响。

并非完全准确:某些查询可以通过特定方向的索引更快地执行,即使查询本身不需要排序(排序仅用于结果)。例如,具有日期条件的查询:使用索引上的降序方向比升序方向或无索引更快地搜索昨天订阅的用户。

{user_id:1,slug:1} 和 {slug:1,user_id:1} 之间的区别

Mongo将首先根据第一个字段过滤,然后根据第二个字段与第一个字段匹配(依此类推...)在索引中进行过滤。最具限制性的字段必须放在前面,以真正提高查询效率。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接