近日在这篇博文中我和那位博主进行了一些探讨,焦点在于这样一种查询的写法:

return collection.FindAs<Log>(query).Where(l => l.CreateTime < lasttime).OrderByDescending(l => l.CreateTime).Take(pagesize).ToList();

我承认起先我甚至都不知道MongoDB官方驱动有对Linq的部分支持,貌似我之前看的教程都是针对官方驱动的早期版本的,从没有看到一篇文章提过官方驱动的Linq功能~至多有写过第三方驱动支持Linq的……

不过官方Linq例子中也并不是这样书写的,以下为官方示例:

var result =
collection.AsQueryable<C>()
.Where(c => c.X == 1)
.Count();

注意第二行,官方是先对集合进行AsQueryable<C>()转换后才使用Linq查询的。

而那位博主是在用官方的标准查询方法之后再继续通过Linq实现排序和限制传回数据量(以及另一处的跳过N条数据命令)。

我担心这种混合型的特殊编码方式并不受支持,故而会导致性能问题,即:程序会先将所有数据从服务器取回,再进行排序、跳过、限量取出。

另外由于从未尝试过用Linq查询MongoDB,我也想测试一下其性能是否有损耗,于是便进行了实验。

首先是测试用的数据类型代码:

    public class 数据
    {
        public Guid Id { get; set; }
        public string 名称 { get; set; }
        public bool 性别 { get; set; }
        public double? 身高 { get; set; }
        public DateTime? 生日 { get; set; }
        public 状态 状态 { get; set; }
    }

    public enum 状态
    {
        单身 = 0, 交往 = 1, 已婚 = 2, 离异 = 3
    }

测试类的必要字段:

        readonly MongoDatabase _db = new MongoClient("mongodb://localhost/").GetServer().GetDatabase("MongoDBLab");
        private readonly Random _ran = new Random();

现在来插入10万条随机的初始数据到数据库:

        [TestMethod]
        public void 插入10万条数据测试()
        {
            var c = _db.GetCollection<数据>("十万数据集合");
            for (int i = 0; i < 100000; i++)
            {
                c.Insert(new 数据
                {
                    Id = Guid.NewGuid(),
                    身高 = _ran.NextDouble() * 0.6 + 1.3,
                    名称 = String.Format("{0}{1}{2}", (char)(_ran.Next(0x9fa0 - 0x4e00) + 0x4e00), (char)(_ran.Next(0x9fa0 - 0x4e00) + 0x4e00), (char)(_ran.Next(0x9fa0 - 0x4e00) + 0x4e00)),
                    生日 = DateTime.Today.AddDays(0 - (_ran.Next(80) + 15) * 365),
                    性别 = _ran.Next() % 2 == 0,
                    状态 = (状态)_ran.Next(4)
                });
            }
        }

这时数据库中的内容是这样的:

image

我不打算对其做任何索引。

下面测试正式开始。

标准查询方式:

        [TestMethod]
        public void 在10万数据中常规查询测试()
        {
            var c = _db.GetCollection<数据>("十万数据集合");
            var data = c.Find(Query.And(
                Query<数据>.EQ(q => q.性别, false),
                Query.Or(Query<数据>.EQ(q => q.状态, 状态.单身), Query<数据>.EQ(q => q.状态, 状态.离异), Query<数据>.EQ(q => q.状态, 状态.交往)),
                Query<数据>.GTE(q => q.身高, 1.5),
                Query<数据>.LTE(q => q.身高, 1.9),
                Query<数据>.GTE(q => q.生日, new DateTime(1960, 1, 1)),
                Query<数据>.LTE(q => q.生日, new DateTime(2000, 1, 1))
                )).SetSortOrder(SortBy<数据>.Descending(q => q.生日));
            data.Skip = 7500;
            data.Limit = 30;

            Console.WriteLine("共有{0}条数据", data.Count());
            foreach (var f in data)
            {
                Console.WriteLine("{0} {1}岁 {2:0.00}米", f.名称, DateTime.Today.Year - f.生日.Value.Year, f.身高);
            }
        }

这里查询条件是:

  • 女性
  • 状态为单身或离异或交往
  • 身高大于1.5并小于1.9
  • 生年大于1960并小于2000

然后对于匹配到的结果按生年倒序排列,并且跳过7500条数据,取回30条。

后面的所有查询也都将遵循此条件。

测试结果:

image

总共查到12615条数据。

下面是第一种Linq查询方式:

        [TestMethod]
        public void 在10万数据中Linq查询测试1()
        {
            var c = _db.GetCollection<数据>("十万数据集合");
            var data = c.AsQueryable()
                .Where(
                    q => !q.性别 &&
                         (q.状态 == 状态.单身 || q.状态 == 状态.离异 || q.状态 == 状态.交往) &&
                         q.身高 >= 1.5 &&
                         q.身高 <= 1.9 &&
                         q.生日 >= new DateTime(1960, 1, 1) &&
                         q.生日 <= new DateTime(2000, 1, 1)
                )
                .OrderByDescending(q => q.生日)
                .Skip(7500)
                .Take(30);


            Console.WriteLine("共有{0}条数据", data.Count());
            foreach (var f in data)
            {
                Console.WriteLine("{0} {1}岁 {2:0.00}米", f.名称, DateTime.Today.Year - f.生日.Value.Year, f.身高);
            }
        }

注意第89行代码中的Count(),这里因为使用的Linq,所以Count()实际调用的也是Linq的Api中的扩展方法,而不是前面标准查询中的MongoCursor类原生方法,区别看结果:

image

看到了吧,显示只有30条结果,因为这个集合已经被Take方法限制过了,所以这时获得的不是查询到的匹配数量。

所以就有了第二种Linq查询方式:

        [TestMethod]
        public void 在10万数据中Linq查询测试2()
        {
            var c = _db.GetCollection<数据>("十万数据集合");
            var data = c.AsQueryable()
                .Where(
                    q => !q.性别 &&
                         (q.状态 == 状态.单身 || q.状态 == 状态.离异 || q.状态 == 状态.交往) &&
                         q.身高 >= 1.5 &&
                         q.身高 <= 1.9 &&
                         q.生日 >= new DateTime(1960, 1, 1) &&
                         q.生日 <= new DateTime(2000, 1, 1)
                )
                .OrderByDescending(q => q.生日);

            Console.WriteLine("共有{0}条数据", data.Count());
            foreach (var f in data.Skip(7500).Take(30))
            {
                Console.WriteLine("{0} {1}岁 {2:0.00}米", f.名称, DateTime.Today.Year - f.生日.Value.Year, f.身高);
            }
        }

在这种方式中我们先执行Count获取总匹配数量,后面在112行中才执行Skip和Take方法进行取回数量限制,这样的结果就对了:

image

最后一种就是那位博主使用的混搭查询方式:

        [TestMethod]
        public void 在10万数据中混合查询测试()
        {
            var c = _db.GetCollection<数据>("十万数据集合");
            var data = c.Find(Query.And(
                Query<数据>.EQ(q => q.性别, false),
                Query.Or(Query<数据>.EQ(q => q.状态, 状态.单身), Query<数据>.EQ(q => q.状态, 状态.离异), Query<数据>.EQ(q => q.状态, 状态.交往)),
                Query<数据>.GTE(q => q.身高, 1.5),
                Query<数据>.LTE(q => q.身高, 1.9),
                Query<数据>.GTE(q => q.生日, new DateTime(1960, 1, 1)),
                Query<数据>.LTE(q => q.生日, new DateTime(2000, 1, 1))
                ));

            Console.WriteLine("共有{0}条数据", data.Count());
            foreach (var f in data.OrderByDescending(q => q.生日).Skip(7500).Take(30))
            {
                Console.WriteLine("{0} {1}岁 {2:0.00}米", f.名称, DateTime.Today.Year - f.生日.Value.Year, f.身高);
            }
        }

查询结果与前面一致。

接下来就是性能比拼了:

image

可以说四种方法基本上没多大性能差别。

Linq的第一种方法稍快是因为它没有查询到全部匹配数据总数。Linq的第二种方法优势就很小了,但据我多次测试感觉Linq的平均用时也确实是比另外两个少那么点点的。

虽然如此,得出这个结果还是挺令人惊讶的,Linq的表达式解析不但没有损耗性能,反而可能还比标准的那套稍微蹩脚的Api快那么点点,并且还能与标准Api无缝衔接,这做的真的很到位。

看来我是有点鲁莽了,自己没搞清之前就去指点那位博主~好在我们俩因为这次交流都学到了些更深入的知识,所以说交流、探讨还是很重要的呵。

鉴于此结果,我个人觉得以后可能会大力使用Linq了,又方便又快,关键通用性极高,以Linq作为标准的话,切换数据库都不发愁。不过当然如果遇到复杂查询条件拼接或非面向对象的查询时,标准查询仍然是必要的。

另外我顺手测试了一下Skip对查询的性能影响,之前是跳过7500条数据,现在改成仅跳过55条,重跑:

image

第一种Linq查询方式飞速提升了300毫秒……,第二种Linq方式速度微量提升(基本可忽略~),标准方式和混合方式都没有任何改变。

看来Skip对于本地执行的效率影响大于在数据库中执行的影响。(难道.Net空跑7000多次循环就会浪费300毫秒吗??)

注:所使用的官方驱动版本为1.10.0-rc0 (预发行版)

image

PS:NuGet被我昨天骂了一天之后今天终于神奇般地原地复活了~~~

转载此文章时须注明转载自”SkyD(斯克迪亚)开发者博客“,并保留此文章的Url链接

作者信息

昵称
斯克迪亚

查看其所发布的所有文章

总积分
2420
注册时间
(2018年5月4日 19:06)

评论

目前还没有任何评论。

[切换到移动版页面]