DefaultIfEmpty mess ordered items in EF and LINQ to Entities? - entity-framework

Given this entity (and this records as examples)
Discount
Amount Percentage
1000 2
5000 4
10000 8
I want to get the percentage to apply to one P.O. Amount
I.E.: Having a P.O. Amount of 15000
if I use
db.Discount
.Where(d => d.Amount <= PO.Amount)
.OrderByDescending(o => o.Amount)
.Select(s => s.Percentage)
.ToList()
.DefaultIfEmpty(0)
.FirstOrDefault();
I get 8 (correct)
but if I use
db.Discount
.Where(d => d.Amount <= PO.Amount)
.OrderByDescending(o => o.Amount)
.Select(s => s.Percentage)
.DefaultIfEmpty(0)
.FirstOrDefault();
I get 2 (incorrect) and items are not ordered any more.
Am I doing an incorrect use of DefaultIfEmpty?

If you are using Entity Framework 6.*, then it is a known bug:
the workaround is to move the DefaultIfEmpty call to after the ToList, which is arguably better as there is no need for the replacement of an empty result set to be done in the database.
Following examples generated with EF 6.1.2 (and "captured" with Microsoft SQL Profiler on a Microsoft SQL Server 2016).
Now... Your "wrong" query:
var res = db.Discounts
.Where(d => d.Amount <= PO.Amount)
.OrderByDescending(o => o.Amount)
.Select(s => s.Percentage)
.DefaultIfEmpty(0)
.FirstOrDefault();
"removes" the OrderBy:
exec sp_executesql N'SELECT
[Limit1].[C1] AS [C1]
FROM ( SELECT TOP (1)
CASE WHEN ([Project1].[C1] IS NULL) THEN cast(0 as bigint) ELSE [Project1].[Percentage] END AS [C1]
FROM ( SELECT 1 AS X ) AS [SingleRowTable1]
LEFT OUTER JOIN (SELECT
[Extent1].[Percentage] AS [Percentage],
cast(1 as tinyint) AS [C1]
FROM [dbo].[Discount] AS [Extent1]
WHERE [Extent1].[Amount] <= #p__linq__0 ) AS [Project1] ON 1 = 1
) AS [Limit1]',N'#p__linq__0 bigint',#p__linq__0=15000
"best" query would be:
var res = db.Discounts
.Where(d => d.Amount <= PO.Amount)
.OrderByDescending(o => o.Amount)
.Select(s => s.Percentage)
.Take(1)
.ToArray()
.DefaultIfEmpty(0)
.First(); // Or Single(), same result but clearer that there is always *one* element
See the Take(1)? It generates a TOP (1):
exec sp_executesql N'SELECT TOP (1)
[Project1].[Percentage] AS [Percentage]
FROM ( SELECT
[Extent1].[Amount] AS [Amount],
[Extent1].[Percentage] AS [Percentage]
FROM [dbo].[Discount] AS [Extent1]
WHERE [Extent1].[Amount] <= #p__linq__0
) AS [Project1]
ORDER BY [Project1].[Amount] DESC',N'#p__linq__0 bigint',#p__linq__0=15000
Then the ToArray() will move the elaboration to C#. You could use .FirstOrDefault() with ?? instead of using DefaultIfEmpty(), but the result would be different if Amount is already nullable (the null returned by FirstOrDefault() is because there are no rows or because the only row found has Amount == null? Who knows :-) ). To solve this problem it becomes a little more complex (in the most general case):
var res = (db.Discounts
.Where(d => d.Amount <= PO.Amount)
.OrderByDescending(o => o.Amount)
.Select(s => new { s.Percentage })
.FirstOrDefault() ?? new { Percentage = (long)0 }
).Percentage;
Here the (long) in (long)0 should be the data type of Percentage. This query gives:
exec sp_executesql N'SELECT TOP (1)
[Project1].[C1] AS [C1],
[Project1].[Percentage] AS [Percentage]
FROM ( SELECT
[Extent1].[Amount] AS [Amount],
[Extent1].[Percentage] AS [Percentage],
1 AS [C1]
FROM [dbo].[Discount] AS [Extent1]
WHERE [Extent1].[Amount] <= #p__linq__0
) AS [Project1]
ORDER BY [Project1].[Amount] DESC',N'#p__linq__0 bigint',#p__linq__0=15000
Other "worse" variant:
var res = db.Discounts
.Where(d => d.Amount <= PO.Amount)
.OrderByDescending(o => o.Amount)
.Select(s => s.Percentage)
.Take(1)
.DefaultIfEmpty(0)
.First();
that gives an overcomplicated query with two TOP (1):
exec sp_executesql N'SELECT
[Limit2].[C1] AS [C1]
FROM ( SELECT TOP (1)
CASE WHEN ([Project2].[C1] IS NULL) THEN cast(0 as bigint) ELSE [Project2].[Percentage] END AS [C1]
FROM ( SELECT 1 AS X ) AS [SingleRowTable1]
LEFT OUTER JOIN (SELECT TOP (1)
[Project1].[Percentage] AS [Percentage],
cast(1 as tinyint) AS [C1]
FROM ( SELECT
[Extent1].[Amount] AS [Amount],
[Extent1].[Percentage] AS [Percentage]
FROM [dbo].[Discount] AS [Extent1]
WHERE [Extent1].[Amount] <= #p__linq__0
) AS [Project1]
ORDER BY [Project1].[Amount] DESC ) AS [Project2] ON 1 = 1
) AS [Limit2]',N'#p__linq__0 bigint',#p__linq__0=15000

this is normal because
for first statment
db.Discount.Where(d => d.Amount <= PO.Amount).OrderByDescending(o => o.Amount).Select(s => s.Percentage).ToList().DefaultIfEmpty(0).FirstOrDefault();
you call .ToList() before DefaultIfEmpty(0) which means when you call .ToList() statment translated to sql as following
DECLARE #p0 Int = 15000
SELECT [t0].[Percentage]
FROM [AppScreens] AS [t0]
WHERE [t0].[Amount] <= #p0
ORDER BY [t0].[Amount] DESC
then executed and loaded in memory after that these two function run on data in memory .DefaultIfEmpty(0).FirstOrDefault(); so the result is as you expected
but for second statment
db.Discount.Where(d => d.Amount <= PO.Amount).OrderByDescending(o => o.Amount).Select(s => s.Percentage).DefaultIfEmpty(0).FirstOrDefault();
you don't call .ToList() which mean that statement won't be executed until it reach FirstOrDefault() because DefaultIfEmpty(0) function is implemented by using deferred execution and you can read its documentation from this reference of MSDN
When it reach .FirstOrDefault() statment translated to sql as following
DECLARE #p0 Int = 15000
SELECT case when [t2].[test] = 1 then [t2].[Percentage] else [t0].[EMPTY] end AS [value]
FROM (SELECT 0 AS [EMPTY] ) AS [t0]
LEFT OUTER JOIN ( SELECT TOP (1) 1 AS [test], [t1].[Percentage] FROM [Discount] AS [t1] WHERE [t1].[Amount] <= #p0 ) AS [t2] ON 1=1
ORDER BY [t2].[Amount] DESC
then executed and loaded in memory after that so the result isn't as you expected
because it get top 1 first before order so it get first item.

Related

Querying for any entries after traversing multiple conditional includes in EF Core 5

Consider the following data model:
A Principal has a number of Roles (many-to-many);
Roles grant multiple Permissions (many-to-many);
Now i want to use LINQ to determine whether a Principle has a permission, i.e. whether he is in any Roles that possess this permission.
Usually I would go for AnyAsync on the join table for cases like this , but since I am traversing more than one join table I am really struggling. I initially came up with this:
var query = context.Principals
.Where(p => p.Id == "SomeUUID")
.Include(p => p.Roles)
.ThenInclude(r => r.Permissions
.Where(p => p.Name == "SomePermission"));
But now I would have to traverse, probably in-memory, the attached Roles through the Principal again to search for Any Permission.
Is there a way I can smoothly apply this check within the same query?
EDIT: Here is the generated SQL to complement Parks'(bottom) answer in comparison:
SELECT CASE
WHEN EXISTS (
SELECT 1
FROM [Principals] AS [a]
INNER JOIN (
SELECT [a1].[Id], [a1].[Description], [a1].[DisplayName], [a0].[RoleId], [a0].[PrincipalId]
FROM [PrincipalRoles] AS [a0]
INNER JOIN [Roles] AS [a1] ON [a0].[RoleId] = [a1].[Id]
) AS [t] ON [a].[PrincipalId] = [t].[PrincipalId]
INNER JOIN (
SELECT [a3].[Id], [a3].[Name], [a2].[RoleId], [a2].[PermissionId]
FROM [RolePermissions] AS [a2]
INNER JOIN [Permissions] AS [a3] ON [a2].[PermissionId] = [a3].[Id]
) AS [t0] ON [t].[Id] = [t0].[RoleId]
WHERE ([a].[PrincipalId] = "SomePrincipal") AND ([t0].[Name] = "SomePermission")) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
SELECT CASE
WHEN EXISTS (
SELECT 1
FROM [Principals] AS [a]
WHERE ([a].[PrincipalId] = "SomePrincipal") AND EXISTS (
SELECT 1
FROM [PrincipalRoles] AS [a0]
INNER JOIN [Roles] AS [a1] ON [a0].[RoleId] = [a1].[Id]
WHERE ([a].[PrincipalId] = [a0].[PrincipalId]) AND EXISTS (
SELECT 1
FROM [RolePermissions] AS [a2]
INNER JOIN [Permissions] AS [a3] ON [a2].[PermissionId] = [a3].[Id]
WHERE ([a1].[Id] = [a2].[RoleId]) AND ([a3].[Name] = "SomePermission")))) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
To see if a principal has a Role in Roles that has a Permission in Permissions you can use the following Where condition:
var result = await context.Principals
.Where(p => p.Id == "SomeUUID" && p.Roles.Any(r => r.Permissions.Any(x => x.Name == "SomePermission")))
.FirstOrDefaultAsync();
If result is null then either the Principal did not exist or did not have the permission you checked for. If result still needs to have the navigation properties then you can add the include statements originally included in your question.
Probably you are looking for this query:
var hasPermssion = context.Principals
.Where(p => p.Id == "SomeUUID")
.SelectMany(p => p.Roles)
.SelectMany(r => r.Permissions)
.Any(p => p.Name == "SomePermission");
Here is the generated SQL statement:
SELECT CASE
WHEN EXISTS (
SELECT 1
FROM [Principals] AS [a]
INNER JOIN (
SELECT [a1].[Id], [a1].[Description], [a1].[DisplayName], [a0].[RoleId], [a0].[PrincipalId]
FROM [PrincipalRoles] AS [a0]
INNER JOIN [Roles] AS [a1] ON [a0].[RoleId] = [a1].[Id]
) AS [t] ON [a].[PrincipalId] = [t].[PrincipalId]
INNER JOIN (
SELECT [a3].[Id], [a3].[Name], [a2].[RoleId], [a2].[PermissionId]
FROM [RolePermissions] AS [a2]
INNER JOIN [Permissions] AS [a3] ON [a2].[PermissionId] = [a3].[Id]
) AS [t0] ON [t].[Id] = [t0].[RoleId]
WHERE ([a].[PrincipalId] = "SomePrincipal") AND ([t0].[Name] = "SomePermission")) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END

EF LINQ query to SQL OUTER APPLY

How to generate such SQL code:
SELECT TOP(10) c.Id AS CarId, r.FranceAccessoriesCount, r.GermanyAccessoriesCount, r.ItalyAccessoriesCount
FROM [Cars] AS c
OUTER APPLY (SELECT
SUM(CASE WHEN a.ManufacturerCountry = 'France' then 1 ELSE 0 END) as FranceAccessoriesCount,
SUM(CASE WHEN a.ManufacturerCountry = 'Germany' then 1 ELSE 0 END) as GermanyAccessoriesCount,
SUM(CASE WHEN a.ManufacturerCountry = 'Italy' then 1 ELSE 0 END) as ItalyAccessoriesCount
FROM [Accessories] a
WHERE a.CarId = c.Id AND a.[Year] > 1999) r
using LINQ query (EF Core)?
I've tried:
await this.context.Cars
.Take(10)
.Select(c => new
{
CarId = c.Id,
Accessories = this.context.Accessories.Where(a => a.CarId == c.Id && a.Year > 1999)
})
.Select(c => new
{
CarId = c.CarId,
FranceAccessoriesCount = c.Accessories.Count(a => a.ManufacturerCountry == "France"),
GermanAccessoriesCount = c.Accessories.Count(a => a.ManufacturerCountry == "Germany"),
ItalyAccessoriesCount = c.Accessories.Count(a => a.ManufacturerCountry == "Italy")
})
.ToListAsync();
But this doesn't generate OUTER APPLY. Instead it translates to:
SELECT TOP(10) [c].[Id] AS CarId (
SELECT COUNT(*)
FROM [Accessories] AS [a]
WHERE (([a].[CarId] = [c].[Id]) AND [a].[Year] > 1999) AND ([a].[ManufacturerCountry ] = N'France')) AS [FranceAccessoriesCount ], (
SELECT COUNT(*)
FROM [Accessories] AS [a1]
WHERE (([a].[CarId] = [c].[Id]) AND [a1].[Year] > 1999) AND ([a1].[ManufacturerCountry ] = N'Germany')) AS [GermanyAccessoriesCount ], (
SELECT COUNT(*)
FROM [Accessories] AS [a2]
WHERE (([a2].[CarId] = [c].[Id]) AND [a2].[Year] > 1999) AND ([a2].[ManufacturerCountry ] = N'Italy')) AS [ItalyAccessoriesCount]
FROM [Cars] AS [c]
How to write LINQ query which will translate in OUTER APPLY?
EF is a mapper. You tell it what you want using entities as you have them defined and it generates an SQL statement to retrieve and project that data. It may not be optimal in all circumstances but generally it is quite good from a performance perspective.
If the EF query is not returning the data you expect to see, then approach it from the desired data you want to project to and express a question from what projection you want vs. what projection you get.
If the EF query is returning the data you expect, but simply isn't using a CROSS APPLY like you expect, then the answer is that if this is important, don't rely on the EF generated query and instead have it execute a hand-crafted SQL statement.

How to union with a scalar value

In this pseudo-code, the last line won't work because the scalar value isn't IQueryable.
var scalarValue = 7;
var qry = (from t in db.Table
where t.Filter == 5 //Arbitrary
select t.ID);
var unioned = qry.Union(scalarValue);
It's straightforward in SQL:
select t.ID
from Table
union
select 7;
In my scenario I do need to select a valid Table.ID, so the workaround is:
var scalarValue = 7;
var qry = (from t in db.Table
where t.Filter == 5 //Arbitrary
select t.ID);
var scalarQry = (from t in db.Table
where t.ID == scalarValue
select t.ID);
var unioned = qry.Union(scalarQry);
However, if there is a way to output "select 7;" for example, then that's what I'm after for this question.
Writing
MyContext.MyClass.Select(t => t.Id).Union(new List<long>() { 42 }).ToList();
produces the following SQL query (MyClass is a DbSet<MyClass>) :
SELECT
[Distinct1].[C1] AS [C1]
FROM ( SELECT DISTINCT
[UnionAll1].[MyClassId] AS [C1]
FROM (SELECT
[Extent1].[MyClassId] AS [MyClassId]
FROM [dbo].[MyClasss] AS [Extent1]
UNION ALL
SELECT
cast(42 as bigint) AS [C1]
FROM ( SELECT 1 AS X ) AS [SingleRowTable1]) AS [UnionAll1]
) AS [Distinct1]
The overload of Union which gets called is :
public static IQueryable<TSource> Union<TSource>(this IQueryable<TSource> source1, IEnumerable<TSource> source2)
in System.Linq.Queryable
I hope this will match your needs

Entity Framework ... Four queries into one

I am using EF5 and finding questions as follows:
context.Questions
.OrderBy(x => Guid.NewGuid())
.Where(x => x.Difficulty == difficulty && x.Format == format);
Each Question has a field named duration with values (2, 4, 6 and 8) in minutes.
I have the following int array: { 4, 0, 1, 2 } which means:
Get 4 questions of 2 minutes, 0 of 4 minutes, 1 of 6 minutes and 2 of 8 minutes.
After .OrderBy and .Where I need to get 7 questions as mentioned.
Is there a way to do this without loading all questions or using 4 queries?
Thank You,
Miguel
This is kind of hardcoded - but you get the idea, and solves your problem explicitly. You should be able to convert it to whatever you need from here.
var questions = context.Questions .OrderBy(x => Guid.NewGuid())
.Where(x => x.Difficulty == difficulty && x.Format == format);
var selectedQuestions = questions.Where(q => q.Duration == 2).Take(questionArray[0])
.Union(questions.Where(q => q.Duration == 4).Take(questionArray[1]))
.Union(questions.Where(q => q.Duration == 6).Take(questionArray[2]))
.Union(questions.Where(q => q.Duration == 8).Take(questionArray[3]));
Since you're never enumerating the queryable, EF will do all of these unions in sql, and get all the data in a single call.
Produced SQL:
SELECT [Distinct3].[C1] AS [C1],
[Distinct3].[C2] AS [C2],
[Distinct3].[C3] AS [C3],
[Distinct3].[C4] AS [C4]
FROM (SELECT DISTINCT [UnionAll3].[C1] AS [C1],
[UnionAll3].[C2] AS [C2],
[UnionAll3].[C3] AS [C3],
[UnionAll3].[C4] AS [C4]
FROM (SELECT [Distinct2].[C1] AS [C1],
[Distinct2].[C2] AS [C2],
[Distinct2].[C3] AS [C3],
[Distinct2].[C4] AS [C4]
FROM (SELECT DISTINCT [UnionAll2].[C1] AS [C1],
[UnionAll2].[C2] AS [C2],
[UnionAll2].[C3] AS [C3],
[UnionAll2].[C4] AS [C4]
FROM (SELECT [Distinct1].[C1] AS [C1],
[Distinct1].[C2] AS [C2],
[Distinct1].[C3] AS [C3],
[Distinct1].[C4] AS [C4]
FROM (SELECT DISTINCT [UnionAll1].[Id] AS [C1],
[UnionAll1].[Duration] AS [C2],
[UnionAll1].[Difficulty] AS [C3],
[UnionAll1].[Format] AS [C4]
FROM (SELECT TOP (4) [Project1].[Id] AS [Id],
[Project1].[Duration] AS [Duration],
[Project1].[Difficulty] AS [Difficulty],
[Project1].[Format] AS [Format]
FROM (SELECT NEWID() AS [C1],
[Extent1].[Id] AS [Id],
[Extent1].[Duration] AS [Duration],
[Extent1].[Difficulty] AS [Difficulty],
[Extent1].[Format] AS [Format]
FROM [dbo].[Questions] AS [Extent1]
WHERE ([Extent1].[Difficulty] = #p__linq__0)
AND ([Extent1].[Format] = #p__linq__1)
AND (2 = [Extent1].[Duration])) AS [Project1]
ORDER BY [Project1].[C1] ASC
UNION ALL
SELECT TOP (0) [Project3].[Id] AS [Id],
[Project3].[Duration] AS [Duration],
[Project3].[Difficulty] AS [Difficulty],
[Project3].[Format] AS [Format]
FROM (SELECT NEWID() AS [C1],
[Extent2].[Id] AS [Id],
[Extent2].[Duration] AS [Duration],
[Extent2].[Difficulty] AS [Difficulty],
[Extent2].[Format] AS [Format]
FROM [dbo].[Questions] AS [Extent2]
WHERE ([Extent2].[Difficulty] = #p__linq__2)
AND ([Extent2].[Format] = #p__linq__3)
AND (4 = [Extent2].[Duration])) AS [Project3]
ORDER BY [Project3].[C1] ASC) AS [UnionAll1]) AS [Distinct1]
UNION ALL
SELECT TOP (1) [Project7].[Id] AS [Id],
[Project7].[Duration] AS [Duration],
[Project7].[Difficulty] AS [Difficulty],
[Project7].[Format] AS [Format]
FROM (SELECT NEWID() AS [C1],
[Extent3].[Id] AS [Id],
[Extent3].[Duration] AS [Duration],
[Extent3].[Difficulty] AS [Difficulty],
[Extent3].[Format] AS [Format]
FROM [dbo].[Questions] AS [Extent3]
WHERE ([Extent3].[Difficulty] = #p__linq__4)
AND ([Extent3].[Format] = #p__linq__5)
AND (6 = [Extent3].[Duration])) AS [Project7]
ORDER BY [Project7].[C1] ASC) AS [UnionAll2]) AS [Distinct2]
UNION ALL
SELECT TOP (2) [Project11].[Id] AS [Id],
[Project11].[Duration] AS [Duration],
[Project11].[Difficulty] AS [Difficulty],
[Project11].[Format] AS [Format]
FROM (SELECT NEWID() AS [C1],
[Extent4].[Id] AS [Id],
[Extent4].[Duration] AS [Duration],
[Extent4].[Difficulty] AS [Difficulty],
[Extent4].[Format] AS [Format]
FROM [dbo].[Questions] AS [Extent4]
WHERE ([Extent4].[Difficulty] = #p__linq__6)
AND ([Extent4].[Format] = #p__linq__7)
AND (8 = [Extent4].[Duration])) AS [Project11]
ORDER BY [Project11].[C1] ASC) AS [UnionAll3]) AS [Distinct3];
Why not just get 4 (or max of the array) questions of each type and do the filtering on the client? With 16 results it should be cheap - you can use linq grouping to group them by minutes and then it should be easy. Another interesting point here is ordering... If you have more than 4 questions but you always order them when selecting would you ever see any other questions that just first 4?
My query is a little bit more complex. Basically, I ended up with the following:
context.Questions
.OrderBy(x => Guid.NewGuid())
.Where(x =>
x.AccessLevel >= accessLevel &&
x.Enabled == true &&
x.QuestionFormat == questionFormat
x.Theme.Id == themeId
)
.Select(x => new {
Answers = x.Answers.Select(y => new {
Correct = y.Correct,
Packs = y.Packs.SelectMany(z => z.Files, (z, v) => new {
Id = z.Id,
File = new { Key = v.Key, Mime = v.Mime }
}),
Text = y.Text
}),
Duration = x.Duration,
Note = x.Note,
Text = x.Text,
Packs = x.Packs.SelectMany(y => y.Files, (y, z) => new {
Id = y.Id,
File = new { Key = z.Key, Mime = z.Mime }
})
})
.GroupBy(x => x.Duration);
So I am ordering the questions randomly and filtering them.
Then I get, for each question, its answers ...
Each question and answer have some files associated with them.
I have all files in one table. This is why I have Packs and Files in my query.
I am only loading the Files Keys and Mimes. Not the data.
So you suggestion is to get all questions grouped by time, right?
In fact, after do the AccessLevel / Enabled / QuestionFormat / Theme filtering I will never have so many questions ...
So I can group them by Duration as I did in the end of this query.
What would be a fast way to take the number of questions of each duration according to the array I posted in the beginning?
Thank You,
Miguel

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