【问题标题】:How to make EF efficiently call an aggregate function?如何让 EF 高效调用聚合函数?
【发布时间】:2018-07-31 11:43:47
【问题描述】:

我正在尝试编写一个 LINQ-to-entities 查询,该查询将采用我的主要对象的 ICollection 导航属性并将一些元数据附加到它们中的每一个,这是通过将它们中的每一个连接到另一个数据库表来确定的,并且使用聚合函数。所以主要对象是这样的:

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();

我想要的是它生成一些使用聚合函数MAX 来计算LastUsedDate 的高效SQL,如下所示:

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 Server 时,它会显示来自 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 中选择 everything 并在内存中进行 Max() 计算,这是非常低效的。如何让 EF 使用聚合函数生成正确的 SQL 查询?

【问题讨论】:

  • 您的查询源自物化对象 (plan),因此整个查询将在 LINQ to Objects 上下文中执行。这意味着context.Conferences 被视为IEnumerable,从而将整个表加载到内存中。例如,将查询根更改为context.Rooms
  • @mjwills EF 6.2.0.
  • @IvanStoev 但是如果我使用context.Rooms,我该如何过滤到与Plan关联的房间?
  • 如果您愿意,您可以从context.Plans 开始,或者从context.Rooms 开始并使用.Where(room =&gt; room.PlanId == plan.Id) 之类的名称。重点是从EFIQueryable开始。
  • 那你为什么不用where rm.PlanId == plan.Id 作为过滤器呢? plan.Rooms 是否被额外过滤?但你总是可以做var roomIds = plan.Rooms.Select(rm =&gt; rn.Id); var query = from rm in context.Rooms where roomsIds.Contains(rm.Id) …

标签: c# sql-server entity-framework linq linq-to-entities


【解决方案1】:

与您所追求的 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 并导致将整个表加载到内存中)。

SQLIN子句是通过内存中的IEnumerable&lt;Guid&gt;Contains方法实现的。

最后,无需检查计数。 SQL 自然地处理nulls,您只需要确保调用可空的Max 重载,这是通过(DateTime?)conf.EndTime 强制转换实现的。无需像在 LINQ to Objects 中那样检查 conf 中的 null,因为 LINQ to Entities/SQL 也会自然地处理它(只要接收器变量可以为空)。

【讨论】:

  • 不用查询context.Rooms是吗?我认为 context.Conferences 包含所需的一切,除非 OP 想要查看更多 Room 数据。顺便说一句,Contains(room.Id) 应该是 Contains(rm.Id)
  • @Gert 我想你是对的(像往常一样:) 对于具体场景(因此是紫外线)。我只是想展示如何“移植”所需的 SQL 查询。顺便说一句,感谢您指出错字:)
【解决方案2】:

由于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 集合。如果您不想返回/显示除房间 ID 之外的任何其他内容,则最后一步甚至都不是必需的,但这取决于您。

【讨论】:

  • 这不好 - 每个房间仍然需要去数据库一次。这可能会导致 1000 次查询!
  • 查询重写。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-31
  • 2021-12-22
  • 2011-10-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多