软件测试|Mongodb的分页优化及索引使用

本文涉及的产品
云数据库 MongoDB,通用型 2核4GB
简介: 软件测试|Mongodb的分页优化及索引使用

基于我们的数据特性,在进行数据库选型时选择了mongo数据库。在文档数量很大的情况下,存在慢查询,影响服务端性能。合理地对数据库命令及索引进行优化,可以很大幅度提升接口性能

mongo分页查询

在Java中使用mongodb的MongoTemplate进行分页时,一般的策略是使用skip+limit的方式,但是这种方式在需要略过大量数据的时候就显得很低效。

传统分页介绍

假设一页大小为10条。则:

//page 1
1-10

//page 2
11-20

//page 3
21-30
...

//page n
10*(n-1)+1-10*n

MongoDB提供了skip()和limit()方法。

skip: 跳过指定数量的数据. 可以用来跳过当前页之前的数据,即跳过pageSize*(n-1)。limit: 指定从MongoDB中读取的记录条数,可以当做页面大小pageSize。

所以,分页可以这样做:

//Page 1
db.getCollection('file').find({}).limit(10)

//Page 2
db.getCollection('file').find({}).skip(10).limit(10)

//Page 3
db.getCollection('file').find({}).skip(20).limit(10)
........

存在问题

官方文档对skip的描述:

skip方法从结果集的开头进行扫描后返回查询结果。这样随着偏移的增加,skip将变得更慢

The cursor.skip() method requires the server to scan from the beginning of the input results set before beginning to return results. As the offset increases, cursor.skip() will becomeslower.

所以,需要一种更快的方式。其实和mysql数量大之后不推荐用limit m,n一样。

官方建议使用范围查询,可以使用[索引]分页相比,偏移量增加时通常会产生更好的性能。即指定开始位置解决方案是先查出当前页的第一条,然后顺序数pageSize条。

指定范围分页介绍

我们假设基于_id的条件进行查询比较。事实上,这个比较的基准字段可以是任何你想要的有序的字段,比如时间戳。

//Page 1
db.getCollection('file').find({}).limit(pageSize);
//Find the id of the last document in this page
last_id =...

//Page 2
users =db.getCollection('file').find({
'_id':{"$gt":ObjectId("5b16c194666cd10add402c87")}
}).limit(10)

//Update the last id with the id of the last document in this page
last_id =...

显然,第一页和后面的不同。对于构建分页API, 我们可以要求用户必须传递pageSize, lastId。

●pageSize 页面大小

●lastId 上一页的最后一条记录的id,如果不传,则将强制为第一页

降序

_id降序,第一页是最大的,下一页的id比上一页的最后的id还小。

db.getCollection('file').find({ _id:{ $lt:lastId}})
.sort({ _id:-1})
.limit(pageSize)

升序

_id升序,下一页的id比上一页的最后一条记录id还大。

db.getCollection('file').find({ _id:{ $gt:lastId}})
.sort({ _id:1})
.limit(pageSize )

总条数

还有一共多少条和多少页的问题。所以,需要先查一共多少条count

db.getCollection('file').find({}).count();

ObjectId的有序性问题

先看ObjectId生成规则:

比如"_id" : ObjectId("5b1886f8965c44c78540a4fc")

取id的前4个字节。由于id是16进制的string,4个字节就是32位,对应id前8个字符。即5b1886f8, 转换成10进制为1528334072. 加上1970,就是当前时间。

事实上,更简单的办法是查看org.mongodb:bson:3.4.3里的ObjectId对象。

publicObjectId(Date date){
this(dateToTimestampSeconds(date), MACHINE_IDENTIFIER, PROCESS_IDENTIFIER, NEXT_COUNTER.getAndIncrement(),false);
}

//org.bson.types.ObjectId#dateToTimestampSeconds 
privatestatic int dateToTimestampSeconds(Date time){
return(int)(time.getTime()/ 1000L);
}

//java.util.Date#getTime
/**
 * Returns the number of milliseconds since January 1, 1970, 00:00:00 GMT
 * represented by this <tt>Date</tt> object.
 *
 * @return  the number of milliseconds since January 1, 1970, 00:00:00 GMT
 *          represented by this date.
 */
public long getTime(){
returngetTimeImpl();
}

MongoDB的ObjectId应该是随着时间而增加的,即后插入的id会比之前的大。但考量id的生成规则,最小时间排序区分是秒,同一秒内的排序无法保证。当然,如果是同一台机器的同一个进程生成的对象,是有序的。

如果是分布式机器,不同机器时钟同步和偏移的问题。所以,如果你有个字段可以保证是有序的,那么用这个字段来排序是最好的。_id则是最后的备选方案。

存在问题

上面的分页看起来看理想,虽然确实是,但有个问题是不能无法做到跳页。

我们的分页数据要和排序键关联,所以必须有一个排序基准来截断记录。而跳页,我只知道第几页,条件不足,无法分页了。

现实业务需求确实提出了跳页的需求,虽然几乎不会有人用,人们更关心的是开头和结尾,而结尾可以通过逆排序的方案转成开头。所以,真正分页的需求应当是不存在的。如果你是为了查找某个记录,那么查询条件搜索是最快的方案。如果你不知道查询条件,通过肉眼去一一查看,那么下一页足矣。

说了这么多,就是想扭转传统分页的概念,在互联网发展的今天,大部分数据的体量都是庞大的,跳页的需求将消耗更多的内存和cpu,对应的就是查询慢。

当然,如果数量不大,如果不介意慢一点,那么skip也不是啥问题,关键要看业务场景。

我今天接到的需求就是要跳页,而且数量很小,那么skip吧,不费事,还快。

比如google,看起来是有跳页选择的啊。再仔细看,只有10页,多的就必须下一页,并没有提供一共多少页,跳到任意页的选择。这不就是我们的find-condition-then-limit方案吗,只是他的一页数量比较多,前端或者后端把这一页给切成了10份。

同样,Facebook,虽然提供了总count,但也只能下一页。

其他场景,比如Twitter,微博,朋友圈等,根本没有跳页的概念的。

如果确实有跳页的需求,可以仍旧采用skip做分页,目前还没有发现性能问题

private List<DBObject> doFindItems(String collectionName,
      Map<String, Object> query, DBObject showFields, int skip,
      int limit, DBObject order) {

   List<DBObject> result = null;
   DBObject obj = genDBObject(query);
   DBCursor cursor = readDB.getCollection(collectionName)
         .find(obj, showFields);
   if (cursor != null) {
      try {
         if (order != null) {
            cursor.sort(order);
         }
         cursor.skip(skip).limit(limit);
         result = cursor.toArray();
      } finally {
         cursor.close();
      }
   }
  return result;
}

排序和性能

前面关注于分页的实现原理,但忽略了排序。既然分页,肯定是按照某个顺序进行分页的,所以必须要有排序的。

MongoDB的sort和find组合

db.getCollection('file').find().sort({'createTime':1}).limit(5)
db.getCollection('file').find().limit(5).sort({'createTime':1})

这两个都是等价的,顺序不影响执行顺序。即,都是先find查询符合条件的结果,然后在结果集中排序。

我们条件查询有时候也会按照某字段排序的,比如按照时间排序。查询一组时间序列的数据,我们想要按照时间先后顺序来显示内容,则必须先按照时间字段排序,然后再按照id升序。

db.getCollection('file').find({productId:5}).sort({createTime:1, _id:1}).limit(5)

我们先按照createTime升序,然后createTime相同的record再按照_id升序,如此可以实现我们的分页功能了。

多字段排序

db.getCollection('file').sort({taskRole:1,appId:-1})

表示先按照taskRole升序,再按appId降序

示例:

db.getCollection('file').find({});

结果:
/* 1 */
{
    "_id" : ObjectId("5e7179de0af8595d0bbe243f"),
    "fileName" : "test.apk",
    "fileCTime" : NumberLong(1584495748123),
    "version" : "1"
}
/* 2 */
{
    "_id" : ObjectId("5e7dc423e20bc4b7fa6c205a"),
"fileName" : "b.html",
"version" : "2"
}
/* 3 */
{
    "_id" : ObjectId("5e7dc423e20bc4b7fa6c205b"),
"fileName" : "b.html",
"version" : "3"
}

按照fileName升序,然后按照version降序

db.getCollection('file').find({}).sort({fileName:1,version:-1})

结果:
/* 1 */
{
    "_id" : ObjectId("5e7dc423e20bc4b7fa6c205a"),
"fileName" : "b.html",
"version" : "2 "
}
/* 2 */
{
    "_id" : ObjectId("5e7dc423e20bc4b7fa6c205b"),
"fileName" : " b.html",
"version" : "3"
}
/* 3 */
{
    "_id" : ObjectId("5e7179de0af8595d0bbe243f"),
    "fileName" : "test.apk",
    "fileCTime" : NumberLong(1584495748123),
    "version" : "1"
}

Mongo慢查询优化

监控

mongodb可以通过profile来监控查询,查出耗时查询,然后进行优化。

profile常用命令:

db.getProfilingLevel();//查看当前是否开启profile功能用命令,返回level等级,值为0-关闭、1-慢命令、2-全部

db.setProfilingLevel(level);//开启profile功能

level为1的时候,慢命令默认值为100ms,更改为db.setProfilingLevel(level,slowms)
如db.setProfilingLevel(1,50);//更改慢命令值为50ms

db.system.profile.find() //当前的监控日志。

db.system.profile.find({millis:{$gt:500}});//返回查询时间在500毫秒以上的查询命令。

{
    "op" : "query",
    "ns" : "ones.file",//慢日志是所在库和集合
    "command" : {    //具体查询命令
        "find" : "file",
        "filter" : {
            "qbuildCid" : 449557
        },
        "projection" : {},
        "limit" : 1,
        "singleBatch" : true,
        "$db" : "ones",
        "lsid" : {
            "id" : UUID("a9086c77-b0ae-4de1-b0d2-9db19a455762")
        }
    },
    "keysExamined" : 0,
    "docsExamined" : 221258,//此次查询遍历文档个数
    "cursorExhausted" : true,
    "numYield" : 1728,
    "nreturned" : 1,
    "locks" : {
        "Global" : {
            "acquireCount" : {
                "r" : NumberLong(1731)
            }
        },
        "Database" : {
            "acquireCount" : {
                "r" : NumberLong(1729)
            }
        },
        "Collection" : {
            "acquireCount" : {
                "r" : NumberLong(1729)
            }
        }
    },
    "storage" : {},
    "responseLength" : 712,
    "protocol" : "op_msg",
    "millis" : 220,//查询耗时
    "planSummary" : "COLLSCAN",
    "execStats" : {
        "stage" : "LIMIT",
        "nReturned" : 1,
        "executionTimeMillisEstimate" : 10,
        "works" : 221260,
        "advanced" : 1,
        "needTime" : 221258,
        "needYield" : 0,
        "saveState" : 1728,
        "restoreState" : 1728,
        "isEOF" : 1,
        "invalidates" : 0,
        "limitAmount" : 1,
        "inputStage" : {
            "stage" : "COLLSCAN",
            "filter" : {
                "qbuildCid" : {
                    "$eq" : 449557
                }
            },
            "nReturned" : 1,
            "executionTimeMillisEstimate" : 10,
            "works" : 221259,
            "advanced" : 1,
            "needTime" : 221258,
            "needYield" : 0,
            "saveState" : 1728,
            "restoreState" : 1728,
            "isEOF" : 0,
            "invalidates" : 0,
            "direction" : "forward",
            "docsExamined" : 221258
        }
    },
    "ts" : ISODate("2020-05-27T10:50:15.394Z"),//命令执行时间
    "client" : "10.10.10.10",
    "allUsers" : [ 
        {
            "user" : "mongo",
            "db" : "admin"
        }
    ],
    "user" : "mongo@admin"
}

millis为查询耗时,如果发现时间比较长,那么就需要作优化。

docsExamined代表查询遍历文档数,如果该值很大,或者接近记录总数,那么可能没有用到索引查询。

索引

如果发现查询的时间较长,那么可能需要为待查询的字段建立索引。

索引的原理是通过建立指定字段的B-Tree,通过搜索B-Tree来查找对应document的地址。如果需要查询超过一半的集合数据,那直接遍历效率反而会更高,因为省去了搜索B-Tree的过程。

结果集在原集合中所占的比例越大,查询效率越慢。因为使用索引需要进行两次查找:一次查找索引条目,一次根据索引指针去查找相应的文档。而全表扫描只需要进行一次查询。在最坏的情况,使用索引进行查找次数会是全表扫描的两倍。效率会明显比全表扫描低。例如,在文件表中,我们拥有一个"type"列索引,如果在"type"列中,android占了50%,如果现在要查询一个类型为android,文件名为“test.apk"的文件,我们则需要在表的50%的数据中查询,这样有索引的性能会降低。

而相反在提取较小的子数据集时,索引就非常有效,这就是我们为什么会使用分页。

索引设计原则

8.控制字段数:如果你设计的索引例如含有7、8个字段通常需要考虑设计是否合理

Explain查询计划

命令:

>db.getCollection('file').find({qbuildId:441557}).explain()

Explain结果

explain 结果将查询计划以阶段树的形式呈现。

每个阶段将其结果(文档或索引键)传递给父节点。

中间节点操纵由子节点产生的文档或索引键。

根节点是MongoDB从中派生结果集的最后阶段。

在看查询结果的阶段树的时候一定一定是从最里层一层一层往外看的,不是直接顺着读下来的。

在查询计划中出现了很多stage,下面列举的经常出现的stage以及他的含义:

TEXT:使用全文索引进行查询时候的stage返回通过这些信息就能判断查询时如何执行的了

其他

如果数据文件大于系统内存,查询速度会下降几个数量级,因为mongodb是内存数据库。1000万数据的时候没有索引情况下查询可能会几秒钟甚至更久。

另外一点是数据索引如果大于内存,速度也会下降很多。而且对于多条件查询,如果你查询的顺序和索引顺序不同,也不能使用索引。

如果你使用了replica set,这个会影响写入速度的,三个replica set,速度会降低到三分之一。

相关实践学习
MongoDB数据库入门
MongoDB数据库入门实验。
快速掌握 MongoDB 数据库
本课程主要讲解MongoDB数据库的基本知识,包括MongoDB数据库的安装、配置、服务的启动、数据的CRUD操作函数使用、MongoDB索引的使用(唯一索引、地理索引、过期索引、全文索引等)、MapReduce操作实现、用户管理、Java对MongoDB的操作支持(基于2.x驱动与3.x驱动的完全讲解)。 通过学习此课程,读者将具备MongoDB数据库的开发能力,并且能够使用MongoDB进行项目开发。   相关的阿里云产品:云数据库 MongoDB版 云数据库MongoDB版支持ReplicaSet和Sharding两种部署架构,具备安全审计,时间点备份等多项企业能力。在互联网、物联网、游戏、金融等领域被广泛采用。 云数据库MongoDB版(ApsaraDB for MongoDB)完全兼容MongoDB协议,基于飞天分布式系统和高可靠存储引擎,提供多节点高可用架构、弹性扩容、容灾、备份回滚、性能优化等解决方案。 产品详情: https://www.aliyun.com/product/mongodb
相关文章
|
17天前
|
机器学习/深度学习 数据采集 人工智能
【专栏】AI在软件测试中的应用,如自动执行测试用例、识别缺陷和优化测试设计
【4月更文挑战第27天】本文探讨了AI在软件测试中的应用,如自动执行测试用例、识别缺陷和优化测试设计。AI辅助工具利用机器学习、自然语言处理和图像识别提高效率,但面临数据质量、模型解释性、维护更新及安全性挑战。未来,AI将更注重用户体验,提升透明度,并在保护隐私的同时,通过联邦学习等技术共享知识。AI在软件测试领域的前景广阔,但需解决现有挑战。
|
2天前
|
运维 Kubernetes 测试技术
容器技术:优化软件测试流程的利器
本文介绍了容器技术的概念、优势和历史发展,对比了容器与虚拟机的区别,并提及了Docker和Kubernetes等常见容器技术。容器作为轻量级虚拟化工具,提供高效、灵活的应用部署方式,广泛应用于软件开发、云计算和微服务架构。随着技术演进,容器将在边缘计算、人工智能等领域发挥更大作用,推动行业变革。
12 3
|
3天前
|
缓存 监控 NoSQL
【MongoDB 专栏】MongoDB 的内存管理与优化
【5月更文挑战第11天】MongoDB的内存管理优化对性能至关重要,涉及数据缓存、索引及执行操作的内存使用。动态内存管理根据访问模式和负载调整,可通过配置参数优化,如设置合适缓存大小,调整内存分配参数。索引管理也很重要,需定期评估优化,避免内存占用过高。监控内存使用、数据清理压缩、架构规划也是优化手段。面对挑战,如高并发下的内存不足,需灵活调整策略,平衡系统资源。不断学习新方法,提升内存管理能力,以优化MongoDB性能。
【MongoDB 专栏】MongoDB 的内存管理与优化
|
3天前
|
存储 监控 NoSQL
【MongoDB 专栏】MongoDB 的存储引擎选择与优化
【5月更文挑战第11天】MongoDB 的存储引擎选择与优化至关重要,影响数据库性能、可靠性和可扩展性。常见引擎有默认的 WiredTiger(提供高性能读写、文档级并发控制和压缩)和较旧的 MMAPv1。选择引擎需考虑性能需求、数据规模、并发操作和压缩需求。WiredTiger 以其高性能和并发控制脱颖而出。优化策略包括配置参数、规划数据结构、监控性能和定期维护。案例显示,WiredTiger 对于并发访问频繁的电商平台尤为适合。未来,更高效、智能的存储引擎将应运而生,持续优化将是保持数据库系统竞争力的关键。
【MongoDB 专栏】MongoDB 的存储引擎选择与优化
|
4天前
|
NoSQL 测试技术 定位技术
【MongoDB 专栏】MongoDB 的地理空间索引与位置查询
【5月更文挑战第10天】MongoDB 支持地理空间数据处理,提供2dsphere(球面)和2d(平面)索引,适用于地图导航、物流、社交网络等领域。通过创建索引,可加速位置查询,如查询范围、最近邻及地理空间聚合。案例包括地图应用、物流追踪和社交网络。注意数据准确性、索引优化和性能测试,以发挥其在地理空间处理中的潜力。学习此功能,为应用开发解锁更多可能性!
【MongoDB 专栏】MongoDB 的地理空间索引与位置查询
|
4天前
|
存储 NoSQL MongoDB
【MongoDB 专栏】如何高效使用 MongoDB 的索引
【5月更文挑战第10天】MongoDB的索引是提升查询性能的关键,它基于B树结构,分为单字段、复合、多键和文本索引。创建索引可通过`createIndex()`或管理工具,适用于频繁查询、排序分组和连接操作。优化策略包括选择合适字段、避免过度索引和定期评估。注意索引影响写入性能、大小限制及可能的失效情况。通过案例分析,应根据业务需求合理创建和使用索引,以实现最佳性能。
【MongoDB 专栏】如何高效使用 MongoDB 的索引
|
8天前
|
敏捷开发 数据管理 测试技术
探索自动化测试在持续集成环境中的优化策略
【5月更文挑战第6天】 本文旨在深入剖析自动化测试在持续集成(CI)环境中所面临的挑战,并提出一系列创新的优化策略。通过对现代软件开发过程中自动化测试角色的分析,我们揭示了在快速迭代和部署的背景下,如何通过改进测试框架、选择合适的测试工具、以及实施数据驱动测试等手段来提高测试效率和准确性。文章不仅聚焦于技术层面的解决方案,还探讨了团队协作和流程管理对提升自动化测试效能的重要性。
|
10天前
|
机器学习/深度学习 人工智能 算法
深入探索软件自动化测试的优化策略
【5月更文挑战第4天】 随着软件开发周期的不断缩短和发布频率的增加,传统的手动测试方法已无法满足快速迭代的需求。因此,本文聚焦于自动化测试流程的优化,旨在提高测试效率和质量。文章首先回顾了自动化测试的基本概念与实施条件,随后分析了当前自动化测试面临的主要挑战,包括维护成本高、测试用例设计复杂等问题。在此基础上,提出了一系列优化策略:持续集成环境下的自动化测试、数据驱动测试、关键字驱动测试、以及基于人工智能的测试用例生成和维护等。通过案例分析和性能评估,验证了这些策略在提升测试覆盖率和减少人工干预方面的有效性。
|
13天前
|
存储 NoSQL 关系型数据库
【MongoDB系列笔记】索引
索引支持在MongoDB中高效地执行查询。如果没有索引,MongoDB必须执行全集合扫描,即扫描集合中的每个文档,以选择与查询语句匹配的文档。这种扫描全集合的查询效率是非常低的,特别在处理大量的数据时,查询可以要花费几十秒甚至几分钟,这对网站的性能是非常致命的。
23 1
|
14天前
|
监控 NoSQL MongoDB
MongoDB索引机制与优化策略详解
【4月更文挑战第30天】本文深入解析MongoDB的索引机制,包括单字段、复合、地理空间、全文及哈希索引。介绍了创建与查看索引的方法,并提出了优化策略:选择性创建、使用复合索引、定期审查优化、避免不必要的索引扫描、利用索引前缀与覆盖索引,以及监控索引使用。通过这些策略,可提升MongoDB查询性能。