分享

MongoDB 分页查询的方法及性能

xioaxu790 发表于 2014-11-30 19:20:42 [显示全部楼层] 回帖奖励 阅读模式 关闭右栏 1 19436
问题导读
1、什么是MongoDB分页?
2、传统的SQL分页和MongoDB分页有何不同?
3、MongoDB根据什么来加载文档的索引和元数据到内存里?






传统的SQL分页

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

MongoDB分页

进入到Mongo的思路,分页其实并不难,那难得是什么?其实倒也没啥,看明白了也就那样,和SQL分页的思路是一致的。
先说明下这篇文章使用的用例,我在数据库里导入了如下的实体数据,其中cus_id、amount我生成为有序的数字,倒入的记录数是200w:
  1. public class Test
  2. {
  3.         /// <summary>
  4.         /// 主键 ObjectId 是MongoDB自带的主键类型
  5.         /// </summary>
  6.         public ObjectId Id { get; set; }
  7.         /// <summary>
  8.         /// 客户编号
  9.         /// </summary>
  10.         [BsonElement("cust_id")]
  11.         public string CustomerId { get; set; }
  12.         /// <summary>
  13.         /// 总数
  14.         /// </summary>
  15.         [BsonElement("amount")]
  16.         public int Amount { get; set; }
  17.         /// <summary>
  18.         /// 状态
  19.         /// </summary>
  20.         [BsonElement("status")]
  21.         public string Status { get; set; }
  22. }
复制代码

以下的操作基于MongoDB GUI 工具见参考资料3

首先来看看分页需要的参数以及结果,一般的分页需要的参数是:
  1. PageIndex    当前页
  2. PageSize      每页记录数
  3. QueryParam[]  其他的查询字段
复制代码


所以按照row_number的分页思想,也就是说取第(pageIndex*pageSize)到第(pageIndex*pageSize + pageSize),我们用Linq表达就是:
  1. query.Where(xxx...xxx).Skip(pageIndex*pageSize).Take(pageSize)
复制代码


查找了资料,还真有skip函数,而且还有Limit函数 见参考资料1、2,于是轻易地实现了这样的分页查询:
  1. 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,我们来写两条语句分别如下:
  1. db.test.sort({"amount":1}).skip(100000).limit(10)  //183ms
  2. db.test.find({amount:{$gt:2399927}}).sort({"amount":1}).limit(10)  //53ms
复制代码

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

C#实现

上面已经谈过了MongoDB分页的语句和效率,那么我们来实现C#驱动版本。
本篇文章里使用的是官方的BSON驱动,详见参考资料4。Mongo驱动附带了另种方式一种是类似ADO.NET的原生query,一种是Linq,这里我们两种都实现
方案一:条件查询 原生Query实现
  1. var query = Query<Test>.GT(item => item.Amount, 2399927);               
  2. var result = collection.Find(query).SetLimit(100)
  3.                        .SetSortOrder(SortBy.Ascending("amount")).ToList();              
  4. Console.WriteLine(result.First().ToJson());//BSON自带的ToJson
复制代码


方案二:Skip原生Query实现
  1. var result = collection.FindAll().SetSkip(100000).SetLimit(100)
  2.              .SetSortOrder(SortBy.Ascending("amount"));
  3. Console.WriteLine(result.ToList().First().ToJson());
复制代码


方案三:Linq 条件查询
  1. var result = collection.AsQueryable<Test>().OrderBy(item => item.Amount)
  2.          .Where(item => item.Amount > 2399927).Take(100);
  3. Console.WriteLine(result.First().ToJson());
复制代码


方案四:Linq Skip版本
  1. var result = collection.AsQueryable<Test>().OrderBy(item => item.Amount).Skip(100000).Take(100);
  2. Console.WriteLine(result.First().ToJson());
复制代码


性能比较参考
这里的测试代码稍后我上传一下,具体的实现是利用了老赵(我的偶像啊~)的CodeTimer来计算性能。另外我跑代码都是用TestDriven插件来跑的。

方案一:
  1. pagination GT-Limit
  2. { "_id" : ObjectId("5472e383fc46de17c45d4682"), "cust_id" : "A12399997", "amount" : 2399928, "status" : "B" }
  3. Time Elapsed:    1,322ms
  4. CPU Cycles:    4,442,427,252
  5. Gen 0:         0
  6. Gen 1:         0
  7. Gen 2:         0
复制代码


方案二:
  1. pagination Skip-limit
  2. { "_id" : ObjectId("5472e383fc46de17c45d4682"), "cust_id" : "A12399997", "amount" : 2399928, "status" : "B" }
  3. Time Elapsed:    95ms
  4. CPU Cycles:    18,280,728
  5. Gen 0:         0
  6. Gen 1:         0
  7. Gen 2:         0
复制代码




方案三:
  1. paginatiLinq on Linq where
  2. { "_id" : ObjectId("5472e383fc46de17c45d4682"), "cust_id" : "A12399997", "amount" : 2399928, "status" : "B" }
  3. Time Elapsed: 76ms
  4.      CPU Cycles:     268,734,988
  5.      Gen 0:          0
  6.      Gen 1:          0
  7.      Gen 2:          0
复制代码


方案四:
  1. pagination Linq Skip
  2. { "_id" : ObjectId("5472e383fc46de17c45d4682"), "cust_id" : "A12399997", "amount" : 2399928, "status" : "B" }
  3. Time Elapsed:    97ms
  4. CPU Cycles:    30,834,648
  5. Gen 0:         0
  6. Gen 1:         0
  7. Gen 2:         0
复制代码


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

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

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

方案一的修正结果:
  1. pagination GT-Limit
  2. { "_id" : ObjectId("5472e383fc46de17c45d4682"), "cust_id" : "A12399997", "amount
  3. " : 2399928, "status" : "B" }
  4. Time Elapsed:   18ms
  5. CPU Cycles:     54,753,796
  6. Gen 0:          0
  7. Gen 1:          0
  8. Gen 2:          0
复制代码


总结

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

已有(1)人评论

跳转到指定楼层
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关闭

推荐上一条 /2 下一条