object88
object88

Reputation: 760

Optimizing LINQ To Entities query

OK, let's say that I have two entities, A and B. For each user, there may be 0 or more A, with a unique combination of UserId (foreign key), GroupName, and Name properties. Entity A also has an "Value" property. Also for each user, there may be 0 or more B, with a UserID (again, a foreign key) and an "Occurred" property.

My task is to find all the B which have an Occurred property of more than 7 days ago OR have an Occurred property more than now - the number of hours in a particular A property. This code seems to work perfectly:

DateTime now = DateTime.UtcNow;
DateTime expired = now - TimeSpan.FromDays(7d);

using (DatabaseContext context = DatabaseContext.Create())
{
  IQueryable<A> aQ = context.As.Where(a => a.GroupName == "Notifications" && s.Name == "Retention");

  IQueryable<B> bQ = context.Bs.Where(
    n => aQ.Any(a => a.UserId == b.UserId) ?
      b.Occurred < EntityFunctions.AddHours(now, -aQ.FirstOrDefault(a => a.UserId == b.UserId).Value) : 
      b.Occurred < expired);

  IList<B> bs = bQ.ToList();
  // ...
}

This produces a SQL query along these lines:

SELECT 
[Extent1].[ID] AS [ID], 
[Extent1].[OCCURRED] AS [OCCURRED], 
[Extent1].[USERID] AS [USERID], 
FROM [dbo].[B] AS [Extent1]
OUTER APPLY  (SELECT TOP (1) 
  [Extent2].[GROUPNAME] AS [GROUPNAME], 
  [Extent2].[NAME] AS [NAME], 
  [Extent2].[USERID] AS [USERID], 
  [Extent2].[VALUE] AS [VALUE], 
  FROM [dbo].[A] AS [Extent2]
  WHERE (N'Notifications' = [Extent2].[GROUPNAME]) AND (N'Retention' = [Extent2].[NAME]) AND ([Extent2].[USERID] = [Extent1].[USERID]) ) AS [Element1]
  OUTER APPLY  (SELECT TOP (1) 
    [Extent3].[GROUPNAME] AS [GROUPNAME], 
    [Extent3].[NAME] AS [NAME], 
    [Extent3].[USERID] AS [USERID], 
    [Extent3].[VALUE] AS [VALUE], 
    FROM [dbo].[A] AS [Extent3]
    WHERE (N'Notifications' = [Extent3].[GROUPNAME]) AND (N'Retention' = [Extent3].[NAME]) AND ([Extent3].[USERID] = [Extent1].[USERID]) ) AS [Element2]
  WHERE (CASE WHEN ( EXISTS (SELECT 
    1 AS [C1]
    FROM [dbo].[A] AS [Extent4]
    WHERE (N'Notifications' = [Extent4].[GROUPNAME]) AND (N'Retention' = [Extent4].[NAME]) AND ([Extent4].[USERID] = [Extent1].[USERID])
  )) THEN CASE WHEN ( CAST( [Extent1].[OCCURRED] AS datetime2) < (DATEADD (hours,  -([Element1].[VALUE]), @p__linq__0))) THEN cast(1 as bit) WHEN ( NOT ( CAST( [Extent1].[OCCURRED] AS datetime2) < (DATEADD (hours,  -([Element2].[VALUE]), @p__linq__0)))) THEN cast(0 as bit) END WHEN ([Extent1].[OCCURRED] < @p__linq__1) THEN cast(1 as bit) WHEN ( NOT ([Extent1].[OCCURRED] < @p__linq__1)) THEN cast(0 as bit) END) = 1

Please note that I've hacked the code and SQL query down from the actual stuff, so this may not be the perfect representation. But I hope it gets the point across: this looks a little hairy, at least in the way that the query is repeatedly checking A for matching groupname, name, and user. But I'm no Database expert. What I do know is that one observed execution ran roughly 2.5 seconds.

Is there a better way of going about this?

Thanks for any input!

---- SOLUTION ----

Thanks to Gert for this.

The code's query ended up like this:

var bQ =
  from b in context.Bs
  let offset = aQ.FirstOrDefault(a => a.UserId == b.UserId)
  let expiration = (null != offset) ? EntityFunctions.AddHours(now, -offset.Value) : expired
  where b.Occurred < expiration
  select b;

Which is almost exactly what Gert suggested. The new SQL query looks like this:

SELECT 
[Extent1].[ID] AS [ID], 
[Extent1].[OCCURRED] AS [OCCURRED], 
[Extent1].[USERID] AS [USERID]
FROM  [dbo].[B] AS [Extent1]
OUTER APPLY  (SELECT TOP (1)
  [Extent2].[GROUPNAME] AS [GROUPNAME], 
  [Extent2].[NAME] AS [NAME], 
  [Extent2].[USERID] AS [USERID], 
  [Extent2].[VALUE] AS [VALUE]
  FROM [dbo].[A] AS [Extent2]
  WHERE (N'Notifications' = [Extent2].[GROUPNAME]) AND (N'Retention' = [Extent2].[NAME]) AND ([Extent2].[USERID] = [Extent1].[USERID]) ) AS [Element1]
  WHERE CAST( [Extent1].[OCCURRED] AS datetime2) < (CASE WHEN ([Element1].[ID] IS NOT NULL) THEN DATEADD (hour, -([Element1].[VALUE]), @p__linq__0) ELSE @p__linq__1 END)

Upvotes: 0

Views: 573

Answers (1)

Gert Arnold
Gert Arnold

Reputation: 109079

I think that this will query the A table only once:

from b in context.Bs
    let offset = aQ.FirstOrDefault(a => a.UserId == b.UserId).Value
    let dd = offset.HasValue
        ? EntityFunctions.AddHours(now, -offset)
        : expired
where b.Occurred < dd

Upvotes: 2

Related Questions