【MongoDB】MongoDB的核心-索引原理及索引优化、及查询聚合优化实战案例(超详细)

在这里插入图片描述

文章目录

    • 一、数据库查询效率问题引出索引需求
    • 二、索引的基本原理及作用
      • (一)索引的创建及数据组织
      • (二)不同类型的索引
      • (三)索引的额外属性
    • 三、索引的优化与查询计划分析
      • (一)通过profiling监测慢请求
      • (二)查询计划分析优化索引使用
    • 四、查询聚合优化
      • (一)案例背景
        • 问题描述
        • 问题分析
          • 1. 定位慢查询
          • 2. 分析慢查询语句
            • 第一步:`$match`操作
            • 第二步:`$project`操作
            • 第三步:`$group`操作
        • 查看DB/Server/Collection的状态
          • 1. DB状态
          • 2. 查看`orders`这个collection的状态
        • 性能优化
          • 1. 性能优化 - 索引
          • 2. 性能优化 - 聚合大量数据
        • 小结

更多相关内容可查看

一、数据库查询效率问题引出索引需求

当在使用MongoDB等数据库进行集合查询时,如果遇到查询效率低下的情况,就可能需要考虑使用索引了。以MongoDB为例,在向集合插入多个文档后,每个文档经过底层存储引擎持久化会有一个位置信息(如mmapv1引擎里是『文件id + 文件内offset』,wiredtiger存储引擎里是其生成的一个key),通过这个位置信息能从存储引擎里读出该文档。

mongo-9552:PRIMARY> db.person.find()
{ "_id" : ObjectId("571b5da31b0d530a03b3ce82"), "name" : "jack", "age" : 19 }
{ "_id" : ObjectId("571b5dae1b0d530a03b3ce83"), "name" : "rose", "age" : 20 }
{ "_id" : ObjectId("571b5db81b0d530a03b3ce84"), "name" : "jack", "age" : 18 }
{ "_id" : ObjectId("571b5dc21b0d530a03b3ce85"), "name" : "tony", "age" : 21 }
{ "_id" : ObjectId("571b5dc21b0d530a03b3ce86"), "name" : "adam", "age" : 18 }

假设要执行一个查询操作,比如db.person.find( {age: 18} ),如果没有索引,就需要遍历所有的文档(即进行“全表扫描”),根据位置信息读出文档后,再对比age字段是否为18。当集合文档数量较少时,全表扫描的开销可能不大,但当文档数量达到百万、千万甚至上亿时,全表扫描的开销会非常大,一个查询耗费数十秒甚至几分钟都有可能。

二、索引的基本原理及作用

(一)索引的创建及数据组织

比如上面的例子里,person集合里包含插入了5个文档,假设其存储后位置信息如下

位置信息文档
pos1{“name” : “jack”, “age” : 19 }
pos2{“name” : “rose”, “age” : 20 }
pos3{“name” : “jack”, “age” : 18 }
pos4{“name” : “tony”, “age” : 21}
pos5{“name” : “adam”, “age” : 18}

如果想加速 db.person.find( {age: 18} ),就可以考虑对person表的age字段建立索引。

db.person.createIndex( {age: 1} )  // 按age字段创建升序索引

建立索引后,MongoDB会额外存储一份按age字段升序排序的索引数据,索引结构类似如下,索引通常采用类似btree的结构持久化存储,以保证从索引里快速(O(logN)的时间复杂度)找出某个age值对应的位置信息,然后根据位置信息就能读取出对应的文档。

age位置信息
18pos3
18pos5
19pos1
20pos2
21pos4

简单来说,索引就是将文档按照某个(或某些)字段顺序组织起来,以便能根据该字段高效地进行查询。它至少能优化以下场景的效率:

  • 查询场景:比如查询年龄为18的所有人,有了索引就无需全表扫描,可直接通过索引快速定位到符合条件的文档。
  • 更新/删除场景:在将年龄为18的所有人的信息进行更新或删除时,因为更新或删除操作需要先根据条件查询出所有符合条件的文档,所以本质上也是在优化查询环节。
  • 排序场景:将所有人的信息按年龄排序时,如果没有索引,需要全表扫描文档,然后再对扫描的结果进行排序;而有了索引,可利用索引的有序性更高效地完成排序。

MongoDB默认会为插入的文档生成_id字段(如果应用本身没有指定该字段),并且为了保证能根据文档id快速查询文档,MongoDB默认会为集合创建_id字段的索引。

mongo-9552:PRIMARY> db.person.getIndexes() // 查询集合的索引信息
[{"ns" : "test.person",  // 集合名"v" : 1,               // 索引版本"key" : {              // 索引的字段及排序方向"_id" : 1           // 根据_id字段升序索引},"name" : "_id_"        // 索引的名称}
]

(二)不同类型的索引

MongoDB支持多种类型的索引,每种类型适用于不同的使用场合:

  • 单字段索引(Single Field Index)

    • 通过db.person.createIndex( {age: 1} )语句可针对age创建单字段索引,能加速对age字段的各种查询请求,是最常见的索引形式,MongoDB默认创建的_id索引也属于这种类型。
    • {age: 1}代表升序索引,也可通过{age: -1}来指定降序索引,对于单字段索引,升序/降序效果是一样的。
      db.person.createIndex( {age: 1, name: 1} ) 
      
  • 复合索引 (Compound Index)

    • 它是单字段索引的升级版本,针对多个字段联合创建索引,先按第一个字段排序,第一个字段相同的文档按第二个字段排序,依次类推。例如,通过db.person.createIndex( {age: 1, name: 1} )可针对agename这2个字段创建一个复合索引。
    • 复合索引能满足的查询场景比单字段索引更丰富,不光能满足多个字段组合起来的查询(如db.person.find( {age: 18, name: "jack"} )),也能满足匹配复合索引前缀的查询(如{age: 1}{age: 1, name: 1}的前缀,所以db.person.find( {age: 18} )的查询也能通过该索引来加速),但像db.person.find( {name: "jack"} )这种只涉及部分字段且不符合前缀规则的查询则无法使用该复合索引。在创建复合索引时,字段的顺序除了受查询需求影响,还需考虑字段的值分布情况。比如age字段取值有限,相同age的文档较多,而name字段取值丰富,相同name的文档较少,此时先按name字段查找,再在相同name的文档里查找age字段会更为高效。
      db.person.createIndex( {name: 1, age: 1} ) 
      
  • 多key索引 (Multikey Index)

    • 当索引的字段为数组时,创建出的索引称为多key索引。例如,在person表加入一个habbit字段(数组)用于描述兴趣爱好,通过db.person.createIndex( {habbit: 1} )可自动创建多key索引,用于查询有相同兴趣爱好的人。
       {"name" : "jack", "age" : 19, habbit: ["football, runnning"]}db.person.createIndex( {habbit: 1} )  // 自动创建多key索引db.person.find( {habbit: "football"} )
      
  • 其他类型索引

    • 哈希索引(Hashed Index):按照某个字段的hash值来建立索引,目前主要用于MongoDB Sharded Cluster的Hash分片,hash索引只能满足字段完全匹配的查询,不能满足范围查询等。
    • 地理位置索引(Geospatial Index):能很好地解决O2O的应用场景,比如“查找附近的美食”、“查找某个区域内的车站”等。
    • 文本索引(Text Index):能解决快速文本查找的需求,比如对于一个博客文章集合,可针对博客的内容建立文本索引,以便根据博客内容快速查找。

(三)索引的额外属性

MongoDB除了支持多种不同类型的索引,还能对索引定制一些特殊的属性:

  • 唯一索引 (unique index):保证索引对应的字段不会出现相同的值,比如_id索引就是唯一索引。
  • TTL索引:可以针对某个时间字段,指定文档的过期时间(经过指定时间后过期 或 在某个时间点过期)。
  • 部分索引 (partial index):只针对符合某个特定条件的文档建立索引,在3.2版本才支持该特性。
  • 稀疏索引(sparse index):只针对存在索引字段的文档建立索引,可看做是部分索引的一种特殊情况。

三、索引的优化与查询计划分析

(一)通过profiling监测慢请求

MongoDB支持对DB的请求进行profiling,目前支持3种级别的profiling:

  • 0级:不开启profiling。
  • 1级:将处理时间超过某个阈值(默认100ms)的请求都记录到DB下的system.profile集合(类似于mysql、redis的slowlog),生产环境通常建议使用此级别,并根据自身需求配置合理的阈值,用于监测慢请求的情况,以便及时进行索引优化。
  • 2级:将所有的请求都记录到DB下的system.profile集合,生产环境需慎用。

(二)查询计划分析优化索引使用

当索引已经建立了,但查询还是很慢时,就需要深入分析索引的使用情况,可通过查看详细的查询计划来决定如何优化。通过执行计划可以看出以下问题:

  • 根据某个/些字段查询,但没有建立索引。
  • 根据某个/些字段查询,但建立了多个索引,执行查询时没有使用预期的索引。

例如,建立索引前,db.person.find( {age: 18} )必须执行COLLSCAN(全表扫描);

mongo-9552:PRIMARY> db.person.find({age: 18}).explain()
{"queryPlanner" : {"plannerVersion" : 1,"namespace" : "test.person","indexFilterSet" : false,"parsedQuery" : {"age" : {"$eq" : 18}},"winningPlan" : {"stage" : "COLLSCAN","filter" : {"age" : {"$eq" : 18}},"direction" : "forward"},"rejectedPlans" : [ ]},"serverInfo" : {"host" : "localhost","port" : 9552,"version" : "3.2.3","gitVersion" : "b326ba837cf6f49d65c2f85e1b70f6f31ece7937"},"ok" : 1
}

建立索引后,通过查询计划可以看出,先进行[IXSCAN](从索引中查找),然后FETCH`,读取出满足条件的文档。

mongo-9552:PRIMARY> db.person.find({age: 18}).explain()
{"queryPlanner" : {"plannerVersion" : 1,"namespace" : "test.person","indexFilterSet" : false,"parsedQuery" : {"age" : {"$eq" : 18}},"winningPlan" : {"stage" : "FETCH","inputStage" : {"stage" : "IXSCAN","keyPattern" : {"age" : 1},"indexName" : "age_1","isMultiKey" : false,"isUnique" : false,"isSparse" : false,"isPartial" : false,"indexVersion" : 1,"direction" : "forward","indexBounds" : {"age" : ["[18.0, 18.0]"]}}},"rejectedPlans" : [ ]},"serverInfo" : {"host" : "localhost","port" : 9552,"version" : "3.2.3","gitVersion" : "b326ba837cf6f49d65c2f85e1b70f6f31ece7937"},"ok" : 1
}

需要注意的是,索引并不是越多越好,集合的索引太多,会影响写入、更新的性能,因为每次写入都需要更新所有索引的数据。所以system.profile里的慢请求可能是索引建立得不够导致,也可能是索引过多导致。

四、查询聚合优化

(一)案例背景

我们有一个电商订单分析系统,使用MongoDB存储订单数据。当执行一个分析接口,获取特定店铺在某一周内的订单商品分类统计信息时,发现查询速度非常慢,严重影响用户体验。

问题描述

执行订单分析接口,查询特定店铺(假设店铺ID为“20001”)在某一周(2024 - 05 - 01T00:00:00.000Z到2024 - 05 - 07T23:59:59.999Z)内的订单商品分类统计,需要花费约12秒,这明显不符合性能要求。

问题分析
1. 定位慢查询

首先查看当前mongo profile的级别,通过db.getProfilingLevel()发现其为0,即默认没有记录。设置profile级别为记录慢查询模式,设置阈值为1000ms,即db.setProfilingLevel(1, 1000)。再次执行订单分析查询接口,查看Profile记录。

2. 分析慢查询语句

通过查看Profile记录,发现执行的查询是一个聚合管道(pipeline):

第一步:$match操作
{"$match": {"storeId": "20001","$and": [{"orderTime": {"$gte": ISODate("2024-05-01T00:00:00.000Z"),"$lte": ISODate("2024-05-07T23:59:59.999Z")}}]}
}

用于匹配店铺ID为“20001”且订单时间在指定一周内的订单记录。

第二步:$project操作
{"$project": {"productCategory": 1,"orderDate": {"$concat": [{"$substr": [{"$year": ["$orderTime"]},0,4]},"-",{"$substr": [{"$month": ["$orderTime"]},0,2]},"-",{"$substr": [{"$dayOfMonth": ["$orderTime"]},0,2]}]}}
}

除了提取productCategory字段外,还对orderTime字段进行处理,拼接为“yyyy - MM - dd”格式,并将其命名为orderDate

第三步:$group操作
{"$group": {"_id": {"orderDate": "$orderDate","productCategory": "$productCategory"},"count": {"$sum": 1}}
}

orderDateproductCategory进行分组,统计不同日期和商品分类对应的订单数量。

从Profile中可以看到相关指标:

  • millis:花费了12010毫秒返回查询结果。
  • ts:命令执行时间。
  • info:命令内容。
  • query:代表查询。
  • nsecommerce.orders(代表查询的库与集合)。
  • nreturned:返回记录数及用时。
  • reslen:返回的结果集大小(字节数)。
  • nscanned:扫描记录数量。

发现nscanned数很大,接近记录总数,可能没有使用索引查询。

查看DB/Server/Collection的状态
1. DB状态

查看数据库整体状态,包括服务器版本、运行时间、连接数、各种操作计数器(如插入、查询、更新、删除等操作的次数)、存储引擎信息等。示例部分信息如下:

{"host": "ECOMMONGODB","version": "6.0.5","process": "mongod","pid": NumberLong(2005),"uptime": 12345678.0,"uptimeMillis": NumberLong(12345678901),"uptimeEstimate": NumberLong(12345678),"localTime": ISODate("2024-05-08T10:20:30.123Z"),"asserts": {"regular": 0,"warning": 0,"msg": 0,"user": 12345,"rollovers": 0},"connections": {"current": 120,"available": 800,"totalCreated": 13000},// 其他更多信息..."ok": 1.0
}
2. 查看orders这个collection的状态
{"ns": "ecommerce.orders","size": 987654321,"count": 3500000,"avgObjSize": 282,"storageSize": 234567890,"capped": false,"wiredTiger": {// wiredTiger存储引擎相关详细信息...},"nindexes": 1,"totalIndexSize": 30123456,"indexSizes": {"_id_": 30123456},"ok": 1.0
}
性能优化
1. 性能优化 - 索引

目前只有_id索引,接下来对orders集合创建storeIdorderTimeproductCategory字段的索引:

db.orders.ensureIndex({"storeId": 1, "orderTime": 1, "productCategory": 1});
db.orders.ensureIndex({"orderTime": 1});
db.orders.ensureIndex({"productCategory": 1});
db.orders.ensureIndex({"storeId": 1});

创建索引后,查询特定店铺一周内的订单商品分类统计信息,时间缩短到了500ms,效果显著。但当查询一个月的数据时,仍然需要15秒。

通过增加索引小结:添加索引解决了针对索引字段查询的效率问题,但对于大量数据的聚合操作,仅靠索引不能完全解决性能问题。例如,在没有索引的情况下,从500万条数据中找出特定店铺的订单可能需要全表扫描,耗时很长;而有了索引,命中索引查询(IXSCAN)速度提升明显。不过,对于聚合操作,随着数据量增大,性能问题依然存在。同时,判断效率优化情况应该看执行计划,而不仅仅是执行时间,因为执行时间可能受到多种因素影响。

2. 性能优化 - 聚合大量数据

对于这种查询聚合大量数据的问题,考虑到这是一个类似OLAP的操作,对其性能期望不能过高,因为大量数据的I/O操作远超OLTP操作。但仍有一定的优化空间:

  • 在订单插入或更新时,对每个店铺每天的每个商品分类的订单数量进行实时计数,并存储在一个专门的缓存集合中。例如,以{storeId: "20001", orderDate: "2024-05-01", productCategory: "Electronics", count: 10}的形式存储。
  • 每隔一段时间(如每天凌晨)对缓存集合进行一次完整的统计和更新,确保数据的准确性。这样在查询订单商品分类统计信息时,可以直接从缓存集合中获取数据,大大减少了查询和聚合的时间。
小结
  • 慢查询定位:通过Profile分析慢查询。
  • 查询优化:通过添加相应索引提升查询速度。
  • 聚合大数据方案:对于类似OLAP的聚合操作,要合理降低性能期望。从源头入手,在数据插入或更新时做好部分统计工作,缓存结果,以便在查询时直接使用,从而提升整体性能。同时,要结合执行计划来评估优化效果。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/59315.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

企业如何实现无缝数据中心进行大数据迁移呢?

数据中心迁移是企业面临的一个复杂而关键的挑战,涉及到大量的数据移动和系统的重新部署。随着业务的扩展和技术的进步,企业可能需要将数据中心迁移到新的位置或升级到更先进的设备。在进行数据迁移时,必须精心规划和执行,以确保数…

FOFA使用教程之从零到精通

FOFA使用教程之从零到精通 前言一、关于网络资产测绘的概念1、啥是网络空间资产测绘2、啥是互联网资产二、FOFA的简要介绍1、FOFA地址是啥?2、关于FOFA的简要介绍三、FOFA精讲1、运算符规则详解① 关于 = 号的使用说明② 关于 == 号的使用说明③ 关于 && 号的使用说明…

初始JavaEE篇 —— 网络编程(2):了解套接字,从0到1实现回显服务器

找往期文章包括但不限于本期文章中不懂的知识点: 个人主页:我要学编程程(ಥ_ಥ)-CSDN博客 所属专栏:JavaEE 目录 TCP 与 UDP Socket套接字 UDP TCP 网络基础知识 在一篇文章中,我们了解了基础的网络知识,网络的出…

【人工智能】10分钟解读-深入浅出大语言模型(LLM)——从ChatGPT到未来AI的演进

文章目录 一、前言二、GPT模型的发展历程2.1 自然语言处理的局限2.2 机器学习的崛起2.3 深度学习的兴起2.3.1 神经网络的训练2.3.2 神经网络面临的挑战 2.4 Transformer的革命性突破2.4.1 Transformer的核心组成2.4.2 Transformer的优势 2.5 GPT模型的诞生与发展2.5.1 GPT的核心…

Webpack 中无法解析别名路径的原因及解决方案

Webpack 中无法解析别名路径的原因及解决方案 文章目录 Webpack 中无法解析别名路径的原因及解决方案1. 引言2. 理解别名路径(Alias)2.1 什么是别名路径?2.2 别名路径的优势 3. 如何在Webpack中配置别名路径3.1 基本配置3.2 使用别名路径 4. …

最全最简单理解迭代器

1. 迭代器的基础概念(iterator) 1.1 本质 迭代器能够用来遍历容器的对象,与能够遍历数组的指针类似,是广义指针。 1.2 作用: 能够让迭代器与算法不干扰的相互发展,最后又能无间隙的粘合起来。重载了*,++,==,!=,=运算符。用以操作复杂的数据结构。容器提供迭代…

MTSET可溶于DMSO、DMF、THF等有机溶剂,并在水中有轻微的溶解性,91774-25-3

一、基本信息 中文名称:[2-(三甲基铵)乙基]甲硫基磺酸溴;MTSET巯基反应染料 英文名称:MTSET;[2-(Trimethylammonium)ethyl]methanethiosulfonate Bromide CAS号:91774-25-3 分子式:C6H16BrNO2S2 分子量…

CC1链学习记录

🌸 前言 上篇文章学习记录了URLDNS链,接下来学习一下Common-Colections利用链。 🌸 相关介绍 Common-Colections是Apache软件基金会的项目,对Java标准的Collections API提供了很好的补充,在其基础上对常用的数据结构…

Android 配置默认输入法

1.背景 最近有个国内的项目,预制了输入法apk,但是无法调出软键盘。原因是没有配置默认输入法,本文主要记录下如何配置默认输入法。 2.代码设置 设置默认输入法需要配置Settings.Secure.ENABLED_INPUT_METHODS和Settings.Secure.DEFAULT_IN…

【juc】ConcurrentHashMap

目录 1.说明2.基本结构3.线程安全机制3.1 分段锁3.2 CAS操作3.3 volatile关键字 4.扩容机制5.其他特性 1.说明 1.ConcurrentHashMap是Java中的一个线程安全的哈希表实现。 2.ConcurrentHashMap的底层结构主要由数组、链表和红黑树组成。 3.在JDK 1.8及之后的版本中,…

数据湖与数据仓库的区别

数据湖与数据仓库是两种不同的数据存储和管理方式,它们在多个方面存在显著的区别。以下是对数据湖与数据仓库区别的详细阐述: 一、数据存储方式 数据仓库 通常采用预定义的模式和结构来存储数据。数据在存储前通常经过清洗、转换和整合等处理&#xff0…

数据结构PTA

20:C 22:B 27:D 填空 4-2:19 4-4:66 4-5:8 5-x:不加分号 ⬇:top p->next 编程 单链表 每个节点除了存放数据元素外,还要存储指向下一节点的指针…

有哪些机器学习实战?——AI实战指南

机器学习已经从理论走向实际应用,各行业的公司和个人都希望通过机器学习来解决现实问题,提升效率。那么,有哪些值得学习和实践的机器学习项目呢?以下将介绍几类热门的机器学习实战项目,涵盖了推荐系统、图像识别、自然…

go语言中的通道(channel)详解

在 Go 语言中,通道(channel) 是一种用于在 goroutine(协程)之间传递数据的管道。通道具有类型安全性,即它只能传递一种指定类型的数据。通道是 Go 并发编程的重要特性,能够让多个 goroutine 之间…

Flutter-Padding组件

1. 说明 在html中常见的布局标签都有padding属性,但是Flutter中很多Widget是没有padding属性。这个时候 我们可以用Padding组件处理容器与子元素之间的间距 2. 属性 padding:padding值, EdgeInsetss设置填充的值 child:子组件 3. …

多叉树笔记

1 多叉树定义 多叉树是一种树形结构,它有一个特定的节点被称为“根”节点,而每个节点(除了根节点)恰好有一个前驱节点(父节点)。在有根多叉树中,每个节点可以拥有任意数量的后继节点&#xff0…

框架学习04-Spring 事务

1. 事务的基本概念 定义:事务是一组数据库操作的集合,这些操作要么全部成功执行,要么全部不执行。它是为了保证数据的一致性和完整性。例如,在一个银行转账系统中,从一个账户扣款和向另一个账户收款这两个操作应该作为…

【学术会议介绍,SPIE 出版】第四届计算机图形学、人工智能与数据处理国际学术会议 (ICCAID 2024,12月13-15日)

第四届计算机图形学、人工智能与数据处理国际学术会议 2024 4th International Conference on Computer Graphics, Artificial Intelligence and Data Processing (ICCAID 2024) 重要信息 大会官网:www.iccaid.net 大会时间:2024年12月13-15日 大会地…

2-UML概念模型测试

1. (单选题, 1.0 分) UML中的关系不包括()。 A. 抽象B. 实现C. 依赖D. 关联 我的答案:A正确答案: A 知识点: UML的构成 1.0分 2. (单选题, 1.0 分) 下列事物不属于UML结构事物的是()。 A. 组件B. 类C. 节点D. 状…

【go从零单排】Command-Line Flags、Command-Line Subcommands命令行和子命令

🌈Don’t worry , just coding! 内耗与overthinking只会削弱你的精力,虚度你的光阴,每天迈出一小步,回头时发现已经走了很远。 📗概念 在 Go 语言中,命令行标志(Command-Line Flags&#xff09…