zoukankan      html  css  js  c++  java
  • Entity Framework使用Linq实现一条语句多聚合查询



      Entity Framework完美支持Linq查询语法,使我们可以用优雅的Linq语句书写query语句,而不用再去写生疏的SQL语句,这对于我等编程业余虾绝对是福音。比如以Northwnd示例数据库为例,比如"查询所有类别ID==1的商品名称",我们可以这样写:

      var result = context.Products.Where(p=>p.CategoryID == 1).Select(p=>p.ProductName);
      //也可以这样写
      var result2 = from p in context.Products where p.CategoryID == 1 select p.ProductName;
    

      再比如”查询所有类别ID==1的商品的最高价“,我们可以这样写:  

      var result = Products.Where(p=>p.CategoryID == 1).Max(p => p.UnitPrice);
      //或者这样
      var result2 = (from p in context.Products where p.CategoryID == 1 select p.UnitPrice).Max();
    

      再比如要查询"所有类别ID==1的商品的最低价或者平均价",我们只需要将上面的.Max() 改为 .Min().Average() 就行了。还有其他一系列聚合函数,如 .Sum().Count() 等等,用法大同小异。

      那么,问题来了!现在需要查询"所有类别ID==1的商品的最高价、最低价和平均价",该怎么写呢?

      本人菜鸟,捣鼓了许久,得到了一些错误的打开方式,记录一下这些坑。

      错误的打开方式一

      Products.Max(p => p.UnitPrice).Min(p => p.UnitPrice);
      //压根儿就不能这么写
    

      错误的打开方式二

      var result = context.Products.Select(p => new
          {
              Max = context.Products.Where(pr => pr.CategoryID == 1).Max(pr => pr.UnitPrice),
              Min = context.Products.Where(pr => pr.CategoryID == 1).Min(pr => pr.UnitPrice),
              Avg = context.Products.Where(pr => pr.CategoryID == 1).Average(pr => pr.UnitPrice)
          });
    

    这货得到的结果是酱紫:

    Max Min Avg
    263.5000 2.5000 37.9791
    263.5000 2.5000 37.9791
    ... ... ...
    263.5000 2.5000 37.9791

      ...共77行,也是醉了。一看,恍然,对Products的每一项都执行一项.Select(new {}),刚好Product表有77条记录,不就正好是得到77条结果吗。
    打开ExpressProfiler查看实际执行的SQL语句如下:

      exec sp_reset_connection
      go
      exec sp_executesql N'SELECT (
          SELECT MAX([t1].[UnitPrice])
          FROM [Products] AS [t1]
          WHERE [t1].[CategoryID] = @p0
          ) AS [Max], (
          SELECT MIN([t2].[UnitPrice])
          FROM [Products] AS [t2]
          WHERE [t2].[CategoryID] = @p1
          ) AS [Min], (
          SELECT AVG([t3].[UnitPrice])
          FROM [Products] AS [t3]
          WHERE [t3].[CategoryID] = @p2
          ) AS [Avg]
      FROM [Products] AS [t0]',N'@p0 int,@p1 int,@p2   int',@p0=1,@p1=1,@p2=1
      go
    

      原来是Select的嵌套查询。再想其他的办法!!直接对Products表执行.Select()会生成多条记录,那我只Select一次呢?于是有了下面的巨坑。

      错误的打开方式三

      var result = from i in new[] {1}
          select new {
              Max = context.Products.Where(pr => pr.CategoryID == 1).Max(pr => pr.UnitPrice),
              Min = context.Products.Where(pr => pr.CategoryID == 1).Min(pr => pr.UnitPrice),
              Avg = context.Products.Where(pr => pr.CategoryID == 1).Average(pr => pr.UnitPrice)
          };
    

      一执行,结果就一条

    Max Min Avg
    263.5000 4.5000 37.9791

      正高兴呢,一看SQL语句,傻了:

      exec sp_reset_connection
      go
      exec sp_executesql N'SELECT MAX([t0].[UnitPrice]) AS [value]
      FROM [Products] AS [t0]
      WHERE [t0].[CategoryID] = @p0',N'@p0 int',@p0=1
      go
      exec sp_reset_connection
      go
      exec sp_executesql N'SELECT MIN([t0].[UnitPrice]) AS [value]
      FROM [Products] AS [t0]
      WHERE [t0].[CategoryID] = @p0',N'@p0 int',@p0=1
      go
      exec sp_reset_connection
      go
      exec sp_executesql N'SELECT AVG([t0].[UnitPrice]) AS [value]
      FROM [Products] AS [t0]
      WHERE [t0].[CategoryID] = @p0',N'@p0 int',@p0=1
      go
    

      这么简单的一个聚合查询,访问了三次数据库,执行三次查询。。。数据库要疯!!!跟直接写三条不同的.Max().Min().Average()查询语句有啥区别?
      难道没有更好的办法了吗?能不能像SQL语句一样一条语句查询出需要的结果,像酱紫:
      

      SELECT
    	  MAX (UnitPrice) AS Max,
    	  MIN (UnitPrice) AS Min,
    	  AVG (UnitPrice) AS Avg
      FROM
    	  Products
      WHERE
    	  CategoryID = 1
    

      业余水平玩儿编程果然坑,被这个问题困扰了许久,无奈只得焚香询问google大神,四级水平扒英文许久,找到两种方法,分享一下,也作为学习笔记。如有更好的办法,请不吝赐教。



      正确的打开方式

      //通过分组group by
      var res1 = context.Products.Where(product => product.CategoryID == 1)
          .GroupBy(gKey => true)
          .Select(gp => new {
                  Min = gp.Min(p => p.UnitPrice),
                  Max = gp.Max(p => p.UnitPrice),
                  Avg = gp.Average(p => p.UnitPrice)
              });
      //或者写成这样,一样一样的
      var ress2 = from p in context.Products
              where p.CategoryID == 1
              group p by true into gp
              select new {
                  Min = gp.Min(p => p.UnitPrice),
                  Max = gp.Max(p => p.UnitPrice),
                  Avg = gp.Average(p => p.UnitPrice)
              };
    

    生成的SQL语句为:

      exec sp_reset_connection
      go
      exec sp_executesql N'SELECT MIN([t1].[UnitPrice]) AS [Min], MAX([t1].[UnitPrice]) AS [Max], AVG([t1].[UnitPrice]) AS [Avg]
      FROM (
          SELECT @p0 AS [value], [t0].[CategoryID], [t0].[UnitPrice]
          FROM [Products] AS [t0]
          ) AS [t1]
      WHERE [t1].[CategoryID] = @p1
      GROUP BY [t1].[value]',N'@p0 int,@p1 int',@p0=1,@p1=1
      go
    

      其原理是:先对Products根据键Key(即此例中的gKey)进行.GroupBy()分组,得到一个IQueryable<gp>(https://msdn.microsoft.com/library/system.linq.queryable.groupby(v=vs.100).aspx) 变量;每一个不同的Key生成一个不同的gp,其中typeof(gp)=IGrouping<bool, Product>(由于本例中Key为常值true,所以只有一个gp);再对IQueryable<gp>中的每个gp执行.Select(new {})

      由于IGrouping<TKey, TElement>继承自IEnumerable<TElement>, IEnumerable,所以可以对gp可以执行.Max(),.Min()等扩展方法。由于有了局部命名变量gp,就可以在new的匿名类中直接使用gp.Max(),等同于Products.Max(),因为从SQL语句可以看出gp仅仅是取出Products表中的CategoryIDUnitPrice两列再加上一个常值的value列而已。

      这样一来,就可以在一个Linq语句中执行多个聚合函数了,而且从生成的SQL语句来看,应该也是比较好的了,比最理想的情况多了一个嵌套查询。(其实感觉这种方式跟错误的打开方式三中的方式差不多,GroupBy方式只是把Products包装成了一个独立的对象,使得.Select(new{})方法只执行一次。只是错误的打开方式三中定义一个new[] {1}的新列表来进行Select查询的方式没法正确翻译为SQL语句。)

      不推荐的打开方式二

      //采用.Aggregate()调用累加器进行计算,相当于foreach了
      decimal sum = 0M;
      int num = 0;
      var result = context.Products.Where(p => p.CategoryID == 1)
          .Select(p => new {UnitPrice = p.UnitPrice}).AsEnumerable()
              .Aggregate( new {
                  Min = decimal.MaxValue, 
                  Max = decimal.MinusOne, 
                  Avg = 0M
                  },
                  (a, temp) => {
                      if (temp.UnitPrice.HasValue)
                      { 
                          sum+= temp.UnitPrice.GetValueOrDefault();
                          checked { ++num; }
                      }
                      return new
                          {
                              Min = Math.Min(temp.UnitPrice ?? 0M, a.Min),
                              Max = Math.Max(temp.UnitPrice ?? 0M, a.Max),
                              Avg = num > 0 ? sum/(decimal) num : 0M
                          };
                  });
    

      说明
      必须在调用.Aggregate()方法前调用.AsEnumerable(), .ToList(), .ToArray()几种方法之一立即执行查询。否则会报错:

      不能将具有语句体的 lambda 表达式转换为表达式树。
    

      原因应该是带有语句体的lambda表达式无法翻译成SQL(如果是查询内存数据,比如List<T>等,则不需要加这一句)。所以,为了尽量减少查询时传输的数据量,只查询了p.UnitPrice这一个字段。
      执行的SQL语句:

      exec sp_reset_connection
      go
      exec sp_executesql N'SELECT [t0].[UnitPrice]
      FROM [Products] AS [t0]
      WHERE [t0].[CategoryID] = @p0',N'@p0 int',@p0=1
      go
    

      由于这种方式会立即执行查询,且会传输多余的字段,所以不推荐采用这种方法,应尽量采用正确的打开方式



      利用业余时间自学编程一段时间了,开始写博客,作为自己的学习笔记,也将自己的学习经验分享给网友。个人水平有限,文中如有错漏,敬请斧正。

  • 相关阅读:
    linux终端发送邮件
    ubuntu交换Caps 和 ESC
    pycharm快捷键
    python catch socket timeout
    pgsql restart
    python re.sub
    文件写入与缓存
    HTTP协议再分析
    leetcode-45
    Java的锁
  • 原文地址:https://www.cnblogs.com/ToddleLobster/p/5027083.html
Copyright © 2011-2022 走看看