说一说mongodb组合索引的匹配规则

一、背景

有一张1000多万条记录的大表,需要做归档至历史表,出现了大量慢查询。

查询条件是

bash 复制代码
"classroomId": {$in: ["xxx", "xxx", ..... "xxx","xxx", "xxx" ] }

耗时近5秒,且是全表扫描

为什么没有使用到任何索引呢?

请看该集合创建的索引有哪些:

建立了两个组合索引userId_1_classroomId_1_isDelete_1和userId_1_classroomId_1,

但是二者的重复度极高。(可以删掉userId_1_classroomId_1_isDelete_1,再新建一个单索引classroomId_1)

二、组合索引

1、最左匹配原则

MongoDB 中组合索引遵循最左匹配原则,即在检索数据时从复合索引的最左边开始匹配。

举例来说,上文的组合索引userId_1_classroomId_1,对于查询条件 {"userId": "xxx", "classroomId": "xxx"} 可以匹配该组合索引,因为查询条件包含了索引的最左前缀;而对于查询条件 {"classroomId":"xxx"} 则无法匹配该组合索引。

但是对查询条件 {"userId": "xxx"} 则可以匹配该组合索引。

所以,我们需要再新建一个单索引classroomId_1。

2、ESR规则

ESR(Equality, Sort, Range)规则是创建高效组合索引的一个重要原则

  • Equality(相等):将需要精确匹配的字段放在索引的前面。这些字段用于过滤数据,减少需要扫描的文档数量。例如查询 db.xxx.find({"classroomId": "GM03DI890"}) 中,classroomId 字段是精确匹配,应放在索引的前面。
  • Sort(排序):排序操作应放在精确匹配字段之后。因为精确匹配可以减少需要排序的文档数量,且这样可以让 MongoDB 进行非阻塞排序。例如查询 db.xxx.find({"classroomId": "GM03DI890"}).sort({createdOn: 1}) 中,createdOn 字段用于排序,应放在 classroomId 字段之后。
    也就是说,组合索引的顺序应该是classroomId_1_createdOn_1
  • Range(范围):范围查询字段应放在索引的最后面。范围查询会扫描一定范围内的数据,将其放在最后可以提高查询效率。例如查询 db.xxx.find({price: {$gte: 15000}}) 中,price 字段是范围查询,应放在索引的最后。

三、OR查询

如果是OR查询呢?

还是举如下例:

bash 复制代码
{
    "$or": [
        {
            "auth": 1
        },
        {
            "totalIds": {
                "$in": [
                    1002482
                ]
            }
        }
    ]
}

应该分别对 auth 和 totalIds 字段创建单独的索引,而不是创建一个组合索引。

bash 复制代码
db.xxx.createIndex({"auth":1}, {"name":"auth_1","background":true})
db.xxx.createIndex({"totalIds":1}, {"name":"totalIds_1","background":true})

这是因为 MongoDB 在使用 or 查询时,如果每个子句都有自己的索引,那么 MongoDB 可以分别使用这些索引来执行查询,然后合并结果。这通常比创建一个包含所有字段的复合索引更有效。

1、区分度问题

区分度低的字段-- auth 字段的值只有两个(0 和 1),区分度很低。通常情况下,区分度低的字段单独建立索引的收益较小,因为索引的目的是快速定位数据,而区分度低的字段在索引中并不能有效减少需要扫描的数据量。

但我们还得考虑另外一个因素。

2、查询频率

如果 auth 字段在查询中非常频繁地被使用,即使区分度低,建立索引也可能带来一些性能提升。例如,如果大部分查询都包含 auth 字段,那么建立索引可以减少全表扫描的次数。

对于 or 查询,MongoDB 会分别使用每个子查询的索引,然后合并结果。

因此,为 auth 和 totalIds 分别建立单独的索引是合理的。这样可以确保每个子查询都能高效地使用索引。

四、执行计划

使用 explain 分析查询计划:

可以通过 explain 方法来分析查询计划,查看是否使用了索引以及索引的使用情况。

json 复制代码
{"planSummary":"IXSCAN { totalIds: 1 }, IXSCAN { auth: 1 }"}
bash 复制代码
db.xxx.find({"$or":[{"auth":1},{"totalIds":{"$in":[636622]}}]}).explain("executionStats")

从执行计划可以看到,现在的OR查询能够使用到这两个单独索引。

五、总结

本文在OR查询中使用了分别创建两个单独索引来提高查询效率。

这里有一个问题,auth字段的区分度低,而totalIds字段的区分度高。

在索引及文档扫描的时候,整个查询的效率是取决于auth字段,尽管totalIds查询速度快。

上述OR查询语句,索引扫描行数以及文档扫描行数均为18000多。

从中也可以看出,区分度高和低,影响的检索效率高低。

改进:

  • 从业务角度,考虑将auth查询与非auth查询分开来查询,这样就不会有OR查询场景
  • 从数据库的角度,可以考虑分两张表,当要查询auth=0还是1的时候,从小表查询;如果没有auth查询,就可以使用totalIds字段,由于其区分度高,可以大大提高检索效率。
相关推荐
Java知识日历8 分钟前
SpringBoot整合Grizzly,提高您网站的并发能力
java·spring boot·后端·spring
Allen Bright16 分钟前
【JVM-7】JVM 命令行工具 jstack 的使用和具体应用案例
java·开发语言·jvm
东北赵四17 分钟前
JVM之垃圾回收器ZGC概述以及垃圾回收器总结的详细解析
java·jvm·算法
Allen Bright21 分钟前
【JVM-5】深入解析JVM垃圾回收器:分类与原理
java·jvm
宁静@星空35 分钟前
006-excel数据输出insert语句
数据库·mysql·excel
阿七想学习44 分钟前
数据结构《Map&Set&哈希表》
java·数据结构·算法
for621 小时前
langchain4j执行源码分析
java·langchain
xiaoshiguang31 小时前
LeetCode:131. 分割回文串
java·算法·leetcode
七禾页话1 小时前
数据库基础知识总结
数据库
做一个有信仰de人1 小时前
【面试题】Spring/SpringBoot部分[2025/1/6 ~ 2025/1/12]
java·spring boot·spring·面试