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
表中的CategoryID
、UnitPrice
两列再加上一个常值的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
由于这种方式会立即执行查询,且会传输多余的字段,所以不推荐采用这种方法,应尽量采用正确的打开方式。
利用业余时间自学编程一段时间了,开始写博客,作为自己的学习笔记,也将自己的学习经验分享给网友。个人水平有限,文中如有错漏,敬请斧正。