如何让EF高效调用聚合函数?
How to make EF efficiently call an aggregate function?
我正在尝试编写一个 LINQ-to-entities 查询,它将对我的主要对象进行 ICollection
导航 属性 并将一些元数据附加到它们中的每一个,这些元数据是通过加入确定的他们每个人都到另一个数据库 table 并使用聚合函数。所以主要对象是这样的:
public class Plan
{
...
public virtual ICollection<Room> Rooms { get; set; }
}
我的查询是这样的:
var roomData = (
from rm in plan.Rooms
join conf in context.Conferences on rm.Id equals conf.RoomId into cjConf
select new {
RoomId = rm.Id,
LastUsedDate = cjConf.Count() == 0 ? (DateTime?)null : cjConf.Max(conf => conf.EndTime)
}
).ToList();
我想要的是生成一些高效的 SQL,使用聚合函数 MAX
来计算 LastUsedDate
,如下所示:
SELECT
rm.Id, MAX(conf.EndTime) AS LastUsedDate
FROM
Room rm
LEFT OUTER JOIN
Conference conf ON rm.Id = conf.RoomId
WHERE
rm.Id IN ('a967c9ce-5608-40d0-a586-e3297135d847', '2dd6a82d-3e76-4441-9a40-133663343d2b', 'bb302bdb-6db6-4470-a24c-f1546d3e6191')
GROUP BY
rm.id
但是当我分析 SQL 服务器时,它显示来自 EF 的查询:
SELECT
[Extent1].[Id] AS [Id],
[Extent1].[RoomId] AS [RoomId],
[Extent1].[ProviderId] AS [ProviderId],
[Extent1].[StartTime] AS [StartTime],
[Extent1].[EndTime] AS [EndTime],
[Extent1].[Duration] AS [Duration],
[Extent1].[ParticipantCount] AS [ParticipantCount],
[Extent1].[Name] AS [Name],
[Extent1].[ServiceType] AS [ServiceType],
[Extent1].[Tag] AS [Tag],
[Extent1].[InstantMessageCount] AS [InstantMessageCount]
FROM [dbo].[Conference] AS [Extent1]
所以它是从Conference
中选择所有然后在内存中进行Max()
计算,这是非常低效的。我怎样才能让 EF 使用聚合函数生成正确的 SQL 查询?
由于 plan.Rooms
未 IQueryable
附加查询提供程序,因此连接语句被编译为 Enumarable.Join
。这意味着 context.Conferences
被隐式转换为 IEumerable
并且其内容在其他运算符应用于它之前被拉入内存。
您可以通过不使用 join
来解决此问题:
var roomIds = plan.Rooms.Select(r => r.Id).ToList();
var maxPerRoom = context.Conferences
.Where(conf => roomIds.Contains(conf.RoomId))
.GroupBy(conf => conf.RoomId)
.Select(g => new
{
RoomId = g.Key,
LastUsedDate = g.Select(conf => conf.EndTime)
.DefaultIfEmpty()
.Max()
}
).ToList();
var roomData = (
from rm in plan.Rooms
join mx in maxPerRoom on rm.Id equals mx.RoomId
select new
{
RoomId = rm.Id,
LastUsedDate = mx.LastUsedDate
}
).ToList();
第一步从上下文中收集 LastUsedDate
数据,然后与内存中的 plan.Rooms
集合相结合。如果您对 returning/displaying 除了房间 ID 以外的任何其他内容都不感兴趣,则最后一步甚至没有必要,但这取决于您。
与您所追求的 SQL 查询密切相关的等效 LINQ to Entities 查询如下所示:
var roomIds = plan.Rooms.Select(rm => rm.Id);
var query =
from rm in context.Rooms
join conf in context.Conferences on rm.Id equals conf.RoomId
into rmConf from rm in rmConf.DefaultIfEmpty() // left join
where roomIds.Contains(rm.Id)
group conf by rm.Id into g
select new
{
RoomId = g.Key,
LastUsedDate = g.Max(conf => (DateTime?)conf.EndTime)
};
诀窍是从 EF IQueryable
开始查询,从而允许它完全转换为 SQL,而不是从 plan.Rooms
开始查询IEnumerable
并使整个查询在内存中执行(context.Conferences
被视为 IEnumerable
并导致在内存中加载整个 table)。
SQLIN
子句是通过内存IEnumerable<Guid>
和Contains
方法实现的。
终于不用再查计数了。 SQL 自然会处理 null
,您只需确保调用可为 null 的 Max
重载,这是通过 (DateTime?)conf.EndTime
转换实现的。无需像在 LINQ to Objects 中那样检查 conf
是否为 null
,因为 LINQ to Entities/SQL 也自然地处理了(只要接收方变量可为空)。
我正在尝试编写一个 LINQ-to-entities 查询,它将对我的主要对象进行 ICollection
导航 属性 并将一些元数据附加到它们中的每一个,这些元数据是通过加入确定的他们每个人都到另一个数据库 table 并使用聚合函数。所以主要对象是这样的:
public class Plan
{
...
public virtual ICollection<Room> Rooms { get; set; }
}
我的查询是这样的:
var roomData = (
from rm in plan.Rooms
join conf in context.Conferences on rm.Id equals conf.RoomId into cjConf
select new {
RoomId = rm.Id,
LastUsedDate = cjConf.Count() == 0 ? (DateTime?)null : cjConf.Max(conf => conf.EndTime)
}
).ToList();
我想要的是生成一些高效的 SQL,使用聚合函数 MAX
来计算 LastUsedDate
,如下所示:
SELECT
rm.Id, MAX(conf.EndTime) AS LastUsedDate
FROM
Room rm
LEFT OUTER JOIN
Conference conf ON rm.Id = conf.RoomId
WHERE
rm.Id IN ('a967c9ce-5608-40d0-a586-e3297135d847', '2dd6a82d-3e76-4441-9a40-133663343d2b', 'bb302bdb-6db6-4470-a24c-f1546d3e6191')
GROUP BY
rm.id
但是当我分析 SQL 服务器时,它显示来自 EF 的查询:
SELECT
[Extent1].[Id] AS [Id],
[Extent1].[RoomId] AS [RoomId],
[Extent1].[ProviderId] AS [ProviderId],
[Extent1].[StartTime] AS [StartTime],
[Extent1].[EndTime] AS [EndTime],
[Extent1].[Duration] AS [Duration],
[Extent1].[ParticipantCount] AS [ParticipantCount],
[Extent1].[Name] AS [Name],
[Extent1].[ServiceType] AS [ServiceType],
[Extent1].[Tag] AS [Tag],
[Extent1].[InstantMessageCount] AS [InstantMessageCount]
FROM [dbo].[Conference] AS [Extent1]
所以它是从Conference
中选择所有然后在内存中进行Max()
计算,这是非常低效的。我怎样才能让 EF 使用聚合函数生成正确的 SQL 查询?
由于 plan.Rooms
未 IQueryable
附加查询提供程序,因此连接语句被编译为 Enumarable.Join
。这意味着 context.Conferences
被隐式转换为 IEumerable
并且其内容在其他运算符应用于它之前被拉入内存。
您可以通过不使用 join
来解决此问题:
var roomIds = plan.Rooms.Select(r => r.Id).ToList();
var maxPerRoom = context.Conferences
.Where(conf => roomIds.Contains(conf.RoomId))
.GroupBy(conf => conf.RoomId)
.Select(g => new
{
RoomId = g.Key,
LastUsedDate = g.Select(conf => conf.EndTime)
.DefaultIfEmpty()
.Max()
}
).ToList();
var roomData = (
from rm in plan.Rooms
join mx in maxPerRoom on rm.Id equals mx.RoomId
select new
{
RoomId = rm.Id,
LastUsedDate = mx.LastUsedDate
}
).ToList();
第一步从上下文中收集 LastUsedDate
数据,然后与内存中的 plan.Rooms
集合相结合。如果您对 returning/displaying 除了房间 ID 以外的任何其他内容都不感兴趣,则最后一步甚至没有必要,但这取决于您。
与您所追求的 SQL 查询密切相关的等效 LINQ to Entities 查询如下所示:
var roomIds = plan.Rooms.Select(rm => rm.Id);
var query =
from rm in context.Rooms
join conf in context.Conferences on rm.Id equals conf.RoomId
into rmConf from rm in rmConf.DefaultIfEmpty() // left join
where roomIds.Contains(rm.Id)
group conf by rm.Id into g
select new
{
RoomId = g.Key,
LastUsedDate = g.Max(conf => (DateTime?)conf.EndTime)
};
诀窍是从 EF IQueryable
开始查询,从而允许它完全转换为 SQL,而不是从 plan.Rooms
开始查询IEnumerable
并使整个查询在内存中执行(context.Conferences
被视为 IEnumerable
并导致在内存中加载整个 table)。
SQLIN
子句是通过内存IEnumerable<Guid>
和Contains
方法实现的。
终于不用再查计数了。 SQL 自然会处理 null
,您只需确保调用可为 null 的 Max
重载,这是通过 (DateTime?)conf.EndTime
转换实现的。无需像在 LINQ to Objects 中那样检查 conf
是否为 null
,因为 LINQ to Entities/SQL 也自然地处理了(只要接收方变量可为空)。