Entity Framework ... Four queries into one - entity-framework

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

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

DefaultIfEmpty mess ordered items in EF and LINQ to Entities?

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.

OData is generating wrong URL or is it just me using wrong keyword

I have a query like this,
https://example.com/_vti_bin/exampleService/exampleService.svc/Categories?
$filter=Products/any(x:x/Status eq toupper('DELETED'))&
$select=ID,Products/Status,Products/Title&
$expand=Products
but it's not filtering dataset based on status = deleted and returns products which has status not deleted etc..
I looked at SQL trace and it is generating something like this,
exec sp_executesql N'SELECT
[Project2].[C1] AS [C1],
[Project2].[C2] AS [C2],
[Project2].[C3] AS [C3],
[Project2].[ID] AS [ID],
[Project2].[C4] AS [C4],
[Project2].[C5] AS [C5],
[Project2].[C8] AS [C6],
[Project2].[ID1] AS [ID1],
[Project2].[C6] AS [C7],
[Project2].[C7] AS [C8],
[Project2].[Title] AS [Title],
[Project2].[Status] AS [Status]
FROM ( SELECT
[Extent1].[ID] AS [ID],
1 AS [C1],
N''DataAccess.Product'' AS [C2],
N''ID'' AS [C3],
N''Products'' AS [C4],
N'''' AS [C5],
[Extent2].[ID] AS [ID1],
[Extent2].[Title] AS [Title],
[Extent2].[Status] AS [Status],
CASE WHEN ([Extent2].[ID] IS NULL) THEN CAST(NULL AS varchar(1)) ELSE N''DataAccess.Product'' END AS [C6],
CASE WHEN ([Extent2].[ID] IS NULL) THEN CAST(NULL AS varchar(1)) ELSE N''Title,Status,ID'' END AS [C7],
CASE WHEN ([Extent2].[ID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C8]
FROM [dbo].[Categories] AS [Extent1]
LEFT OUTER JOIN [dbo].[Products] AS [Extent2] ON [Extent1].[ID] = [Extent2].[ProductID]
WHERE ([Extent1].[ClientID] = #p__linq__0) AND ( EXISTS (SELECT
1 AS [C1]
FROM [dbo].[Products] AS [Extent3]
WHERE ([Extent1].[ID] = [Extent3].[ProductID]) AND (([Extent3].[Status] = (UPPER(N''DELETED''))) OR (([Extent3].[Status] IS NULL) AND (UPPER(N''DELETED'') IS NULL)))
))
) AS [Project2]
ORDER BY [Project2].[ID] ASC, [Project2].[C8] ASC',N'#p__linq__0 int',#p__linq__0=23
Is it correct to use "eq" if I only want products whose status is "deleted" and nothing else ?
Edit
I am using OData V3, using WCF Data services with EF
I believe the problem is in the query.
Form the url you are saying something like ...
// get me categories
https://example.com/_vti_bin/exampleService/exampleService.svc/Categories?
// where any product is deleted
$filter=Products/any(x:x/Status eq toupper('DELETED'))&
// return the category id, product status and title
$select=ID,Products/Status,Products/Title&
$expand=Products
In other words you are filtering categories on the deleted status not products within them.
You could add a second filter to handle the product filtering and only return categories and their filtered set of products.
Try something like this instead ...
https://example.com/_vti_bin/exampleService/exampleService.svc/Categories?
$filter=Products/any(x:x/Status eq toupper('DELETED'))&
$select=ID,Products/Status,Products/Title&
$expand=Products/any(p:p/Status eq toupper('DELETED'))
Depending on your situation it may be best turn the query around ...
https://example.com/_vti_bin/exampleService/exampleService.svc/Products?
$filter=Status eq toupper('DELETED')&
$select=Category/ID,Status,Title
... by pulling a set of products and their related category Id's you get the same result but gain the ability to filter those products directly on the base query instead of a more complex child collection filter.
As discussed in chat though, this does require a valid OData model where the relationship between Products and Categories is properly defined.

Why EF 6.13 left outer join with Lambda DefaultIfEmpty is Invalid?

Sorry, my English is very poor, I hope you understand.
If I use Linq it's correct, but now I want to use Lambda.
TABLE 1
ExamQuestion
TABLE 2
PaperQuestion
No foreign key;
var l = db.ExamQuestions
.Join(db.PaperQuestions, s => s.Id, p => p.QuestionId, (s, p) => new
ExamQuestionList()
{
Id = s.Id,
Question = s.Question,
CateTitle = "aaa",
Option = s.Option,
IsPass = s.IsPass,
Answer = s.Answer,
Difficulty = s.Difficulty,
IsDelete = s.IsDelete,
IsExist = true,
Score = p.Score,
CreateTime = s.CreateTime
})
SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Question] AS [Question],
N'aaa' AS [C1],
[Extent1].[Option] AS [Option],
[Extent1].[IsPass] AS [IsPass],
[Extent1].[Answer] AS [Answer],
[Extent1].[Difficulty] AS [Difficulty],
[Extent1].[IsDelete] AS [IsDelete],
cast(1 as bit) AS [C2],
[Extent2].[Score] AS [Score],
[Extent1].[CreateTime] AS [CreateTime]
FROM [dbo].[ExamQuestions] AS [Extent1]
INNER JOIN [dbo].[PaperQuestions] AS [Extent2] ON [Extent1].[Id] = [Extent2].[QuestionId]
I user DefaulifEmpty but invalid, left outer join nest Outermost layer
var l = db.ExamQuestions
.Join(db.PaperQuestions, s => s.Id, p => p.QuestionId, (s, p) => new
ExamQuestionList()
{
Id = s.Id,
Question = s.Question,
CateTitle = "aaa",
Option = s.Option,
IsPass = s.IsPass,
Answer = s.Answer,
Difficulty = s.Difficulty,
IsDelete = s.IsDelete,
IsExist = true,
Score = p.Score,
CreateTime = s.CreateTime
}).DefaultIfEmpty();
SELECT
[Project1].[Id] AS [Id],
[Project1].[Question] AS [Question],
[Project1].[C1] AS [C1],
[Project1].[Option] AS [Option],
[Project1].[IsPass] AS [IsPass],
[Project1].[Answer] AS [Answer],
[Project1].[Difficulty] AS [Difficulty],
[Project1].[IsDelete] AS [IsDelete],
[Project1].[C2] AS [C2],
[Project1].[Score] AS [Score],
[Project1].[CreateTime] AS [CreateTime]
FROM ( SELECT 1 AS X ) AS [SingleRowTable1]
LEFT OUTER JOIN (SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Question] AS [Question],
[Extent1].[Option] AS [Option],
[Extent1].[Answer] AS [Answer],
[Extent1].[CreateTime] AS [CreateTime],
[Extent1].[Difficulty] AS [Difficulty],
[Extent1].[IsPass] AS [IsPass],
[Extent1].[IsDelete] AS [IsDelete],
[Extent2].[Score] AS [Score],
N'aaa' AS [C1],
cast(1 as bit) AS [C2]
FROM [dbo].[ExamQuestions] AS [Extent1]
INNER JOIN [dbo].[PaperQuestions] AS [Extent2] ON [Extent1].[Id] = [Extent2].[QuestionId] ) AS [Project1] ON 1 = 1
It is a valid sql statement that it was generated by the linq provider and posted by you in the OP..
The most outer table [SingleRowTable1] allows to get always at least a single empty record composed by null fields named with the aliases specified in the statement by the column names prefixed with [Project1] table alias name.
The final join condition AS [Project1] ON 1 = 1 clause bind all rows getted by the inner most projection to the outer most fictitious table ( SELECT 1 AS X ) that get always 1 value annd attach it to each row getted by the innermost projection.
I hope it is a clear explanation..

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