mongodb分页优化

mongodb分页很简单,本文主要讲分页可能遇到的问题,以及优化方案

从传统web到移动端api,我们都面临一样的问题,比如ajax get有大小显示等,都会强迫你不得不分页

比如我的项目使用ratchet做h5框架,它的push.js里就是ajax get加载其他页面,页面太大就会报错。

分页说明

以典型的列表api来说:下拉刷新是获取最新信息,然后上拉加载下一页

常见api要写的2个接口

  • get_latest(model,count)
  • get_with_page(number,size)

get_latest一般是取最新的数据,比如我们常见的下拉刷新,一般都是这样的接口的。由于2次下拉之间,可能非常长的时间间隔,所以取到的数据会把当前列表的数据冲掉。

通常做法

  • 如果n(比如n=30s)分钟内有连续请求,提示最近已更新,没必要再刷,或者直接返回当前数据
  • 如果取到新数据,将当前列表的数据冲掉,保证数据一致性

如果判断我到最后一页了

常见的办法是取出总数,除以pagesize,然后判断当前页是否和总页数-1

n = all_count - 1

量少的时候,毫无感觉,如果量大了,你去查一下count(*)是啥后果呢?

所以比较好的做法是按照id去查,前端根据每次返回的数据条数,如果条数等于pagesize,你就可以取下一页数据,相反,如果取到的数据小于pagesize,你就知道没有那么多数据可以取了,即到了尾页。此时只要disable获取下一页的按钮即可。

使用 skip() 和 limit() 实现

//Page 1
db.users.find().limit (10)
//Page 2
db.users.find().skip(10).limit(10)
//Page 3
db.users.find().skip(20).limit(10)
........

抽象一下就是:检索第n页的代码应该是这样的

db.users.find().skip(pagesize*(n-1)).limit(pagesize)

当然,这是假定在你在2次查询之间没有任何数据插入或删除操作,你的系统能么?

当然大部分oltp系统无法确定不更新,所以skip只是个玩具,没太大用

而且skip+limit只适合小量数据,数据一多就卡死,哪怕你再怎么加索引,优化,它的缺陷都那么明显。

如果你要处理大量数据集,你需要考虑别的方案的。

使用 find() 和 limit() 实现

之前用skip()方法没办法更好的处理大规模数据,所以我们得找一个skip的替代方案。

为此我们想平衡查询,就考虑根据文档里有的时间戳或者id

在这个例子中,我们会通过‘_id’来处理(用时间戳也一样,看你设计的时候有没有类似created_at这样的字段)。

‘_id’是mongodb ObjectID类型的,ObjectID 使用12 字节的存储空间,每个字节两位十六进制数字,是一个24 位的字符串,包括timestamp, machined, processid, counter 等。下面会有一节单独讲它是怎么构成的,为啥它是唯一的。

使用_id实现分页的大致思路如下

  1. 在当前页内查出最后1条记录的_id,记为last_id
  2. 把记下来的last_id,作为查询条件,查出大于last_id的记录作为下一页的内容

这样来说,是不是很简单?

代码如下

//Page 1
db.users.find().limit(pageSize);
//Find the id of the last document in this page
last_id = ...

//Page 2
users = db.users.find({‘_id‘> last_id}). limit(10);
//Update the last id with the id of the last document in this page
last_id = ...

这只是示范代码,我们来看一下在Robomongo 0.8.4客户端里如何写

db.usermodels.find({‘_id‘ :{ "$gt" :ObjectId("55940ae59c39572851075bfd")} }).limit(20).sort({_id:-1})

根据上面接口说明,我们仍然要实现2个接口

  • get_latest(model,count)
  • get_next_page_with_last_id(last_id, size)

为了让大家更好的了解根据‘_id’分页原理,我们有必要去了解ObjectID的组成。

关于 ObjectID组成

前面说了:‘_id’是mongodb ObjectID类型的,它由12位结构组成,包括timestamp, machined, processid, counter 等。

![](http://images.blogjava.net/blogjava_net/dongbule/46046/o_111.PNG)

TimeStamp

前 4位是一个unix的时间戳,是一个int类别,我们将上面的例子中的objectid的前4位进行提取“4df2dcec”,然后再将他们安装十六进制 专为十进制:“1307761900”,这个数字就是一个时间戳,为了让效果更佳明显,我们将这个时间戳转换成我们习惯的时间格式

$ date -d ‘1970-01-01 UTC 1307761900 sec’ -u
2011年 06月 11日 星期六 03:11:40 UTC

前 4个字节其实隐藏了文档创建的时间,并且时间戳处在于字符的最前面,这就意味着ObjectId大致会按照插入进行排序,这对于某些方面起到很大作用,如 作为索引提高搜索效率等等。使用时间戳还有一个好处是,某些客户端驱动可以通过ObjectId解析出该记录是何时插入的,这也解答了我们平时快速连续创 建多个Objectid时,会发现前几位数字很少发现变化的现实,因为使用的是当前时间,很多用户担心要对服务器进行时间同步,其实这个时间戳的真实值并 不重要,只要其总不停增加就好。

Machine

接下来的三个字节,就是 2cdcd2 ,这三个字节是所在主机的唯一标识符,一般是机器主机名的散列值,这样就确保了不同主机生成不同的机器hash值,确保在分布式中不造成冲突,这也就是在同一台机器生成的objectid中间的字符串都是一模一样的原因。

pid

上面的Machine是为了确保在不同机器产生的objectid不冲突,而pid就是为了在同一台机器不同的mongodb进程产生了objectid不冲突,接下来的0936两位就是产生objectid的进程标识符。

increment

前面的九个字节是保证了一秒内不同机器不同进程生成objectid不冲突,这后面的三个字节a8b817,是一个自动增加的计数器,用来确保在同一秒内产生的objectid也不会发现冲突,允许256的3次方等于16777216条记录的唯一性。

客户端生成

mongodb产生objectid还有一个更大的优势,就是mongodb可以通过自身的服务来产生objectid,也可以通过客户端的驱动程序来产生,如果你仔细看文档你会感叹,mongodb的设计无处不在的使

用空间换时间的思想,比较objectid是轻量级,但服务端产生也必须开销时间,所以能从服务器转移到客户端驱动程序完成的就尽量的转移,必须将事务扔给客户端来完成,减低服务端的开销,另还有一点原因就是扩展应用层比扩展数据库层要变量得多。

总结

mongodb的ObejctId生产思想在很多方面挺值得我们借鉴的,特别是在大型分布式的开发,如何构建轻量级的生产,如何将生产的负载进行转移,如何以空间换取时间提高生产的最大优化等等。

说这么多的目的就是告诉你:mongodb的_id为啥是唯一的,单机如何唯一,集群中如何唯一,理解了这个就可以了。

性能优化

索引

按照自己的业务需求即可,参见官方文档 http://docs.mongodb.org/manual/core/indexes/

关于explain

rdbms里的执行计划,如果你不了解,那么mongo的explain估计你也不太熟,简单说几句

explain是mongodb提供的一个命令,用来查看查询的过程,以便进行性能优化。

http://docs.mongodb.org/manual/reference/method/cursor.explain/

db.usermodels.find({‘_id‘ :{ "$gt" :ObjectId("55940ae59c39572851075bfd")} }).explain()

/* 0 */
{
    "queryPlanner" : {
        "plannerVersion" : 1,
        "namespace" : "xbm-wechat-api.usermodels",
        "indexFilterSet" : false,
        "parsedQuery" : {
            "_id" : {
                "$gt" : ObjectId("55940ae59c39572851075bfd")
            }
        },
        "winningPlan" : {
            "stage" : "FETCH",
            "inputStage" : {
                "stage" : "IXSCAN",
                "keyPattern" : {
                    "_id" : 1
                },
                "indexName" : "_id_",
                "isMultiKey" : false,
                "direction" : "forward",
                "indexBounds" : {
                    "_id" : [
                        "(ObjectId(‘55940ae59c39572851075bfd‘), ObjectId(‘ffffffffffffffffffffffff‘)]"
                    ]
                }
            }
        },
        "rejectedPlans" : []
    },
    "executionStats" : {
        "executionSuccess" : true,
        "nReturned" : 5,
        "executionTimeMillis" : 0,
        "totalKeysExamined" : 5,
        "totalDocsExamined" : 5,
        "executionStages" : {
            "stage" : "FETCH",
            "nReturned" : 5,
            "executionTimeMillisEstimate" : 0,
            "works" : 6,
            "advanced" : 5,
            "needTime" : 0,
            "needFetch" : 0,
            "saveState" : 0,
            "restoreState" : 0,
            "isEOF" : 1,
            "invalidates" : 0,
            "docsExamined" : 5,
            "alreadyHasObj" : 0,
            "inputStage" : {
                "stage" : "IXSCAN",
                "nReturned" : 5,
                "executionTimeMillisEstimate" : 0,
                "works" : 5,
                "advanced" : 5,
                "needTime" : 0,
                "needFetch" : 0,
                "saveState" : 0,
                "restoreState" : 0,
                "isEOF" : 1,
                "invalidates" : 0,
                "keyPattern" : {
                    "_id" : 1
                },
                "indexName" : "_id_",
                "isMultiKey" : false,
                "direction" : "forward",
                "indexBounds" : {
                    "_id" : [
                        "(ObjectId(‘55940ae59c39572851075bfd‘), ObjectId(‘ffffffffffffffffffffffff‘)]"
                    ]
                },
                "keysExamined" : 5,
                "dupsTested" : 0,
                "dupsDropped" : 0,
                "seenInvalidated" : 0,
                "matchTested" : 0
            }
        },
        "allPlansExecution" : []
    },
    "serverInfo" : {
        "host" : "iZ251uvtr2b",
        "port" : 27017,
        "version" : "3.0.3",
        "gitVersion" : "b40106b36eecd1b4407eb1ad1af6bc60593c6105"
    }
}

字段说明:

queryPlanner.winningPlan.inputStage.stage列显示查询策略

  • IXSCAN表示使用Index 查询
  • COLLSCAN表示使用列查询,也就是一个一个对比过去

cursor中的索引名称移动到了queryPlanner.winningPlan.inputStage.indexName

3.0中使用executionStats.totalDocsExamined来显示总共需要检查的文档数,用以取而代之2.6里的nscanned,即扫描document的行数。

  • nReturned:返回的文档行数
  • needTime:耗时(毫秒)
  • indexBounds:所用的索引

Profiling

另外还有一个Profiling功能

db.setProfilingLevel(2, 20)

profile级别有三种:

  • 0:不开启
  • 1:记录慢命令,默认为大于100ms
  • 2:记录所有命令
  • 3、查询profiling记录

默认记录在system.profile中

db[‘system.profile‘].find()

总结一下

  • explain在写代码阶段就可以做性能分析,开发阶段用
  • profile检测性能慢的语句,便于线上产品问题定位

无论哪种你定位出来问题,解决办法

  • 根据业务,调整schema结构
  • 优化索引

有了上面这些知识,相信大家能够自己去给分页语句测试性能了。

全文完

欢迎关注我的公众号【node全栈】

时间: 2024-10-10 04:52:52

mongodb分页优化的相关文章

java MongoDB分页优化

最近项目在做网站用户数据新访客统计,数据存储在MongoDB中,统计的数据其实也并不是很大,1000W上下,但是公司只配给我4G内存的电脑,让我程序跑起来气喘吁吁...很是疲惫不堪. 最常见的问题莫过于查询MongoDB内存溢出,没办法只能分页查询.这种思想大家可能都会想到,但是如何分页,确实多有门道! 网上用的最多的,也是最常见的分页采用的是skip+limit这种组合方式,这种方式对付小数据倒也可以,但是对付上几百上千万的大数据,却只能望而兴叹... 经过网上各种查找资料,寻师问道的,发现了

MongoDB整理笔记のjava MongoDB分页优化

最近项目在做网站用户数据新访客统计,数据存储在MongoDB中,统计的数据其实也并不是很大,1000W上下,但是公司只配给我4G内存的电脑,让我程序跑起来气喘吁吁...很是疲惫不堪. 最常见的问题莫过于查询MongoDB内存溢出,没办法只能分页查询.这种思想大家可能都会想到,但是如何分页,确实多有门道! 网上用的最多的,也是最常见的分页采用的是skip+limit这种组合方式,这种方式对付小数据倒也可以,但是对付上几百上千万的大数据,却只能望而兴叹... 经过网上各种查找资料,寻师问道的,发现了

MongoDB分页处理方案(适用于一般数据库的分页方法)

MongoDB分页处理方案(适用于一般数据库的分页方法) (2012-11-06 17:59:55) 转载▼ 标签: mongodb 分页 数据库 跳转 分类: MongoDB 转载请注明出处:http://blog.sina.com.cn/s/blog_56545fd30101442b.html MongoDB的分页性能是广大使用者所诟病的大问题之一,在大数据量环境下,如果一次跳转的页数过多,如10W多页,可能用户要等上几十秒(瞎掰的数据),有兴趣的可以去看一下这篇文章Paging & Ran

MySQL分页优化中的“INNER JOIN方式优化分页算法”到底在什么情况下会生效?

本文出处:http://www.cnblogs.com/wy123/p/7003157.html 最近无意间看到一个MySQL分页优化的测试案例,并没有非常具体地说明测试场景的情况下,给出了一种经典的方案,因为现实中很多情况都不是固定不变的,能总结出来通用性的做法或者说是规律,是要考虑非常多的场景的,同时,面对能够达到优化的方式要追究其原因,同样的做法,换了个场景,达不到优化效果的,还要追究其原因.个人对此场景在不用情况表示怀疑,然后自己测试了一把,果然发现一些问题,同时也证实了一些预期的想法.

SQL通用优化方案(where优化、索引优化、分页优化、事务优化、临时表优化)

SQL通用优化方案:1. 使用参数化查询:防止SQL注入,预编译SQL命令提高效率2. 去掉不必要的查询和搜索字段:其实在项目的实际应用中,很多查询条件是可有可无的,能从源头上避免的多余功能尽量砍掉,这是最简单粗暴的解决方案.3. 选择最有效率的表名顺序: 数据库的解析器按照从右到左的顺序处理FROM子句中的表名,FROM子句中写在最后的表将被最先处理,在FROM子句中包含多个表的情况下,你必须选择记录条数最少的表放在最后,如果有3个以上的表连接查询,那就需要选择那个被其他表所引用的表放在最后.

MongoDB 分页查询的方法及性能

这篇文章着重的讲讲MongoDB的分页查询 传统的SQL分页 传统的sql分页,所有的方案几乎是绕不开 row_number的,对于需要各种排序,复杂查询的场景,row_number就是杀手锏.另外,针对现在的web很流行的poll/push加载分 页的方式,一般会利用时间戳来实现分页. 这两种分页可以说前者是通用的,连Linq生成的分页都是row_number,可想而知它多通用.后者是无论是性能和复杂程度都是最好的,因为只要简单 的一个时间戳即可. MongoDB分页 进入到Mongo的思路,

C#MongoDB 分页查询的方法及性能

传统的SQL分页 传统的sql分页,所有的方案几乎是绕不开row_number的,对于需要各种排序,复杂查询的场景,row_number就是杀手锏.另外,针对现在的web很流行的poll/push加载分页的方式,一般会利用时间戳来实现分页. 这两种分页可以说前者是通用的,连Linq生成的分页都是row_number,可想而知它多通用.后者是无论是性能和复杂程度都是最好的,因为只要简单的一个时间戳即可. MongoDB分页 进入到Mongo的思路,分页其实并不难,那难得是什么?其实倒也没啥,看明白

分页优化+表锁和库存优化+数据库的备份和导入

一.分页优化技术 代码参看: php/classic.php 把50331651记录进行分页,每页显示2条记录,于是我们用传统php编码方式,编写分页代码如下: 上传到/var/www/html下进行测试,结果如下: 如果访问第1页和第4页,返回语句: 使用explain执行计划查询比较靠前的页数,发觉速度很快因为可以使用上索引: 如果访问第4100000页,返回语句: 使用explain分析结果如下: 发觉这时如果分页到了中间的页数,这时我们既需要排序又要分页检索数据的时候,就会出现Using

MongoDB性能优化五个简单步骤

大家在使用MongoDB的时候有没有碰到过性能问题呢?这里总结了MongoDB性能优化的五个步骤,希望能够有所帮助. 第一步:找出慢语句 一般来说查询语句太慢和性能问题瓶颈有着直接的关系,所以可以用MongoDB的性能分析工具来找出这些慢语句: db.setProfilingLevel(1, 100); 第二步:使用explain分析 通过使用explain来对这些慢语句进行诊断.此外还可以mtools来分析日志. 第三步:创建索引 分析完之后需要创建新的索引(index)来提升查询的性能.别忘