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

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



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

posted on 2015-12-08 01:00  吐泡泡的虾  阅读(894)  评论(0编辑  收藏  举报