zoukankan      html  css  js  c++  java
  • MongoDB 分页查询的方法及性能

    最近有点忙,本来有好多东西可以总结,Redis系列其实还应该有四、五、六...不过《Redis in Action》还没读完,等读完再来总结,不然太水,对不起读者。

    自从上次Redis之后呢,算是对Nosql类型的产品有些入门了,这会换个方向,研究下真正的NoSql数据库——MongoDB。说起MongoDB,确实是用完了之后颠覆了我的数据管和程序观。怎么说呢?如果用在OO设计的程序里那真的太棒了,像我这种数据驱动、表驱动思想根深蒂固的人,思路很难一下子跟上MongoDB的节奏。当然并不是调用个api,写几句query那些思路,而是程序设计思路,业务领域的设计,如果OO,如何适合展现,适合查询,适合聚合运算等等。总之MongoDB重要的是程序的设计,设计好了,其实完全就忽略了Mongo的存储,因为mongodb实在是太方便了。

    废话不多说,关于入门的资料、安装以及其他请拉到文章末尾,我附上了一些资料,以后如有必要再来分享。这篇文章着重的讲讲MongoDB的分页查询,为啥?分页可是常见的头号杀手,弄不好了,客户骂,经理骂。

     

    传统的SQL分页

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

    MongoDB分页

    进入到Mongo的思路,分页其实并不难,那难得是什么?其实倒也没啥,看明白了也就那样,和SQL分页的思路是一致的。

    先说明下这篇文章使用的用例,我在数据库里导入了如下的实体数据,其中cus_id、amount我生成为有序的数字,倒入的记录数是200w:

    public class Test
    {
            /// <summary>
            /// 主键 ObjectId 是MongoDB自带的主键类型
            /// </summary>
            public ObjectId Id { get; set; }
            /// <summary>
            /// 客户编号
            /// </summary>
            [BsonElement("cust_id")]
            public string CustomerId { get; set; }
            /// <summary>
            /// 总数
            /// </summary>
            [BsonElement("amount")]
            public int Amount { get; set; }
            /// <summary>
            /// 状态
            /// </summary>
            [BsonElement("status")]
            public string Status { get; set; }
    }
    以下的操作基于MongoDB GUI 工具见参考资料3

    首先来看看分页需要的参数以及结果,一般的分页需要的参数是:

    • PageIndex    当前页
    • PageSize      每页记录数
    • QueryParam[]  其他的查询字段

    所以按照row_number的分页思想,也就是说取第(pageIndex*pageSize)到第(pageIndex*pageSize + pageSize),我们用Linq表达就是:

    query.Where(xxx...xxx).Skip(pageIndex*pageSize).Take(pageSize)

    查找了资料,还真有skip函数,而且还有Limit函数 见参考资料1、2,于是轻易地实现了这样的分页查询:

    db.test.find({xxx...xxx}).sort({"amount":1}).skip(10).limit(10)//这里忽略掉查询语句

     相当的高效,几乎是几毫秒就出来了结果,果然是NoSql效率一流。但是慢,我这里使用的数据只是10条而已,并没有很多数据。我把数据加到100000,效率大概是20ms。如果这么简单就研究结束了的话,那真的是太辜负了程序猿要钻研的精神了。sql分页的方案,方案可是能有一大把,效率也是不一的,那Mongo难道就这一种,答案显然不是这样的。另外是否效率上,性能上会有问题呢?Redis篇里,就吃过这样的亏,乱用Keys。

    在查看了一些资料之后,发现所有的资料都是这样说的:

    不要轻易使用Skip来做查询,否则数据量大了就会导致性能急剧下降,这是因为Skip是一条一条的数过来的,多了自然就慢了。

     这么说Skip就要避免使用了,那么如何避免呢?首先来回顾SQL分页的后一种时间戳分页方案,这种利用字段的有序性质,利用查询来取数据的方式,可以直接避免掉了大量的数数。也就是说,如果能附带上这样的条件那查询效率就会提高,事实上是这样的么?我们来验证一下:

    这里我们假设查询第100001条数据,这条数据的Amount值是:2399927,我们来写两条语句分别如下:

    db.test.sort({"amount":1}).skip(100000).limit(10)  //183ms
    
    
    db.test.find({amount:{$gt:2399927}}).sort({"amount":1}).limit(10)  //53ms

    结果已经附带到注释了,很明显后者的性能是前者的三分之一,差距是非常大的。也印证了Skip效率差的理论。

    C#实现

    上面已经谈过了MongoDB分页的语句和效率,那么我们来实现C#驱动版本。

    本篇文章里使用的是官方的BSON驱动,详见参考资料4。Mongo驱动附带了另种方式一种是类似ADO.NET的原生query,一种是Linq,这里我们两种都实现

    方案一:条件查询 原生Query实现

    var query = Query<Test>.GT(item => item.Amount, 2399927);                
    var result = collection.Find(query).SetLimit(100)
                           .SetSortOrder(SortBy.Ascending("amount")).ToList();              
    Console.WriteLine(result.First().ToJson());//BSON自带的ToJson

    方案二:Skip原生Query实现

    var result = collection.FindAll().SetSkip(100000).SetLimit(100)
                 .SetSortOrder(SortBy.Ascending("amount"));
    Console.WriteLine(result.ToList().First().ToJson());

    方案三:Linq 条件查询

    var result = collection.AsQueryable<Test>().OrderBy(item => item.Amount)
             .Where(item => item.Amount > 2399927).Take(100);
    Console.WriteLine(result.First().ToJson());

    方案四:Linq Skip版本

     var result = collection.AsQueryable<Test>().OrderBy(item => item.Amount).Skip(100000).Take(100);
    Console.WriteLine(result.First().ToJson());

    性能比较参考

    这里的测试代码稍后我上传一下,具体的实现是利用了老赵(我的偶像啊~)的CodeTimer来计算性能。另外我跑代码都是用TestDriven插件来跑的。
    方案一:
    pagination GT-Limit
    { "_id" : ObjectId("5472e383fc46de17c45d4682"), "cust_id" : "A12399997", "amount" : 2399928, "status" : "B" }
    Time Elapsed:    1,322ms
    CPU Cycles:    4,442,427,252
    Gen 0:         0
    Gen 1:         0
    Gen 2:         0
    方案二:
    pagination Skip-limit
    { "_id" : ObjectId("5472e383fc46de17c45d4682"), "cust_id" : "A12399997", "amount" : 2399928, "status" : "B" }
    Time Elapsed:    95ms
    CPU Cycles:    18,280,728
    Gen 0:         0
    Gen 1:         0
    Gen 2:         0
    方案三:
    paginatiLinq on Linq where
    { "_id" : ObjectId("5472e383fc46de17c45d4682"), "cust_id" : "A12399997", "amount" : 2399928, "status" : "B" }

    Time Elapsed: 76ms
    CPU Cycles: 268,734,988
    Gen 0: 0
    Gen 1: 0
    Gen 2: 0

    方案四:
    pagination Linq Skip
    { "_id" : ObjectId("5472e383fc46de17c45d4682"), "cust_id" : "A12399997", "amount" : 2399928, "status" : "B" }
    Time Elapsed:    97ms
    CPU Cycles:    30,834,648
    Gen 0:         0
    Gen 1:         0
    Gen 2:         0

    上面结果是不是大跌眼镜,这和理论实在相差太大,第一次为什么和后面的差距如此大?刚开始我以为是C# Mongo的驱动问题,尝试了换驱动也差不多。这几天我在看《MongoDB in Action》的时候,发现文章里提到:

    MongoDB会根据查询,来加载文档的索引和元数据到内存里,并且建议文档元数据的大小始终要保持小于机器内存,否则性能会下降。

     注意到了上面的理论之后,我替换了我的测试方案,第一次执行排除下,然后再比较,发现确实结果正常了。

    方案一的修正结果:

    pagination GT-Limit
    { "_id" : ObjectId("5472e383fc46de17c45d4682"), "cust_id" : "A12399997", "amount
    " : 2399928, "status" : "B" }
    Time Elapsed:   18ms
    CPU Cycles:     54,753,796
    Gen 0:          0
    Gen 1:          0
    Gen 2:          0

    总结

    这篇文章,基于Skip分页和有序字段查询分页两种方案进行的对比。后者说白了只是利用查询结果不用依次数数来提高了性能。Skip虽然效率低一些但是通用一些,有序字段的查询,需要在设计分页的时候对这个字段做一些处理,起码要点了页码能获取到这个字段。这里我附加一个方式,就是两者的结合,我们可以拿每次展示的那页数据上的最后一个,结合Skip来处理分页,这样的话,相对来说更好一些。这里就不具体实现了。其他方式的性能比较和实现,欢迎大牛们来分享,十分感谢。另外本篇中如有纰漏和不足请留言指教。

      忘记打个小广告,我们公司招人哦,详情见我博客的副标题!!

    参考资料

    1. MongoDB Skip函数:http://docs.mongodb.org/manual/reference/operator/aggregation/skip/

    2. MongoDB Limit函数:http://docs.mongodb.org/manual/reference/operator/aggregation/limit/

    3. MongoVUE Windows客户端管理工具(有收费版本):http://www.mongovue.com/ 

    4. C#官方驱动:http://docs.mongodb.org/manual/applications/drivers/

  • 相关阅读:
    December 23rd 2016 Week 52nd Friday
    December 22nd 2016 Week 52nd Thursday
    December 21st 2016 Week 52nd Wednesday
    December 20th 2016 Week 52nd Tuesday
    December 19th 2016 Week 52nd Sunday
    December 18th 2016 Week 52nd Sunday
    uva294(唯一分解定理)
    uva11624Fire!(bfs)
    fzu2150Fire Game(双起点bfs)
    poj3276Face The Right Way
  • 原文地址:https://www.cnblogs.com/capqueen/p/MongoDBPagination.html
Copyright © 2011-2022 走看看