使用 LINQ 高效地仅加载嵌套集合的某些元素
Load only some elements of a nested collection efficiently with LINQ
我有以下 LINQ 查询(使用 EF Core 6 和 MS SQL 服务器):
var resultSet = dbContext.Systems
.Include(system => system.Project)
.Include(system => system.Template.Type)
.Select(system => new
{
System = system,
TemplateText = system.Template.TemplateTexts.FirstOrDefault(templateText => templateText.Language == locale.LanguageIdentifier),
TypeText = system.Template.Type.TypeTexts.FirstOrDefault(typeText => typeText.Language == locale.LanguageIdentifier)
})
.FirstOrDefault(x => x.System.Id == request.Id);
要求是检索与请求的 ID 匹配的系统并加载其项目、模板和模板的类型信息。该模板有多个模板文本(每种翻译语言一个),但我只想加载与请求的语言环境匹配的模板文本,同样处理模板类型的 TypeTexts 元素。
上面的 LINQ 查询在一个查询中执行此操作并将其转换为以下 SQL 查询(我编辑了 SELECT 语句以使用 * 而不是生成的一长串列):
SELECT [t1].*, [t2].*, [t5].*
FROM (
SELECT TOP(1) [p].*, [t].*, [t0].*
FROM [ParkerSystems] AS [p]
LEFT JOIN [Templates] AS [t] ON [p].[TemplateId] = [t].[Id]
LEFT JOIN [Types] AS [t0] ON [t].[TypeId] = [t0].[Id]
LEFT JOIN [Projects] AS [p0] ON [p].[Project_ProjectId] = [p0].[ProjectId]
WHERE [p].[SystemId] = @__request_Id_1
) AS [t1]
LEFT JOIN (
SELECT [t3].*
FROM (
SELECT [t4].*, ROW_NUMBER() OVER(PARTITION BY [t4].[ReferenceId] ORDER BY [t4].[Id]) AS [row]
FROM [TemplateTexts] AS [t4]
WHERE [t4].[Language] = @__locale_LanguageIdentifier_0
) AS [t3]
WHERE [t3].[row] <= 1
) AS [t2] ON [t1].[Id] = [t2].[ReferenceId]
LEFT JOIN (
SELECT [t6].*
FROM (
SELECT [t7].*, ROW_NUMBER() OVER(PARTITION BY [t7].[ReferenceId] ORDER BY [t7].[Id]) AS [row]
FROM [TypeTexts] AS [t7]
WHERE [t7].[Language] = @__locale_LanguageIdentifier_0
) AS [t6]
WHERE [t6].[row] <= 1
) AS [t5] ON [t1].[Id0] = [t5].[ReferenceId]
这还不错,它不是一个超级复杂的查询,但我觉得我的需求可以用更简单的 SQL 查询来解决:
SELECT *
FROM [Systems] AS [p]
JOIN [Templates] AS [t] ON [p].[TemplateId] = [t].[Id]
JOIN [TemplateTexts] AS [tt] ON [p].[TemplateId] = [tt].[ReferenceId]
JOIN [Types] AS [ty] ON [t].[TypeId] = [ty].[Id]
JOIN [TemplateTexts] AS [tyt] ON [ty].[Id] = [tyt].[ReferenceId]
WHERE [p].[SystemId] = @systemId and tt.[Language] = 2 and tyt.[Language] = 2
我的问题是:是否有一个 different/simpler LINQ 表达式(在方法语法或查询语法中)产生相同的结果(一次性获取所有信息),因为理想情况下我不希望有拥有一个匿名对象,其中聚合了过滤后的子集合。对于更多的布朗尼积分,如果生成的 SQL 是 simpler/closer 我认为是一个简单的查询,那就太好了。
此 LINQ 查询与您的 SQL 接近,但我担心结果的正确性:
var resultSet =
(from system in dbContext.Systems
from templateText in system.Template.TemplateTexts
where templateText.Language == locale.LanguageIdentifier
from typeText in system.Template.Type.TypeTexts
where typeText.Language == locale.LanguageIdentifier
select new
{
System = system,
TemplateText = templateText
TypeText = typeText
})
.FirstOrDefault(x => x.System.Id == request.Id);
Is there a different/simpler LINQ expression (...) that produces the same result
是(也许)但不是。
不,因为您正在查询 dbContext.Systems
,因此 EF 将 return 所有与您的过滤器匹配的系统,即使它们没有 TemplateTexts
等。这就是为什么必须生成外连接。 EF 不知道您明显打算跳过没有这些嵌套数据的系统,也不知道这些系统不会出现在数据库中。 (您似乎假设,看到第二个查询)。
这说明了左连接到子查询。
这些子查询是因为FirstOrDefault
而产生的。在 SQL 中,它总是需要某种子查询来获取一对多关系的“第一个”记录。这个 ROW_NUMBER() OVER
构造实际上是相当高效的。您的第二个查询没有任何“第一”记录的概念。它可能 return 不同的数据。
是的(也许),因为您还 Include
数据。我不确定为什么。有些人似乎认为 Include
是进行后续预测 (.Select
) 所必需的,但事实并非如此。如果这是您使用 Include
s 的原因,那么您可以删除它们,从而删除前几个连接。
OTOH 你也 Include
system.Project
不在投影中,所以你似乎故意添加了 Include
s。在这种情况下,它们会起作用,因为整个实体 system
都在投影中,否则 EF 会忽略它们。
如果您再次需要 Include
,出于上述原因,EF 必须生成外部联接。
EF 决定单独处理 Include
s 和预测,而手工制作 SQL,在数据先验知识的帮助下可以更有效地做到这一点。但是没有办法影响这种行为。
我有以下 LINQ 查询(使用 EF Core 6 和 MS SQL 服务器):
var resultSet = dbContext.Systems
.Include(system => system.Project)
.Include(system => system.Template.Type)
.Select(system => new
{
System = system,
TemplateText = system.Template.TemplateTexts.FirstOrDefault(templateText => templateText.Language == locale.LanguageIdentifier),
TypeText = system.Template.Type.TypeTexts.FirstOrDefault(typeText => typeText.Language == locale.LanguageIdentifier)
})
.FirstOrDefault(x => x.System.Id == request.Id);
要求是检索与请求的 ID 匹配的系统并加载其项目、模板和模板的类型信息。该模板有多个模板文本(每种翻译语言一个),但我只想加载与请求的语言环境匹配的模板文本,同样处理模板类型的 TypeTexts 元素。
上面的 LINQ 查询在一个查询中执行此操作并将其转换为以下 SQL 查询(我编辑了 SELECT 语句以使用 * 而不是生成的一长串列):
SELECT [t1].*, [t2].*, [t5].*
FROM (
SELECT TOP(1) [p].*, [t].*, [t0].*
FROM [ParkerSystems] AS [p]
LEFT JOIN [Templates] AS [t] ON [p].[TemplateId] = [t].[Id]
LEFT JOIN [Types] AS [t0] ON [t].[TypeId] = [t0].[Id]
LEFT JOIN [Projects] AS [p0] ON [p].[Project_ProjectId] = [p0].[ProjectId]
WHERE [p].[SystemId] = @__request_Id_1
) AS [t1]
LEFT JOIN (
SELECT [t3].*
FROM (
SELECT [t4].*, ROW_NUMBER() OVER(PARTITION BY [t4].[ReferenceId] ORDER BY [t4].[Id]) AS [row]
FROM [TemplateTexts] AS [t4]
WHERE [t4].[Language] = @__locale_LanguageIdentifier_0
) AS [t3]
WHERE [t3].[row] <= 1
) AS [t2] ON [t1].[Id] = [t2].[ReferenceId]
LEFT JOIN (
SELECT [t6].*
FROM (
SELECT [t7].*, ROW_NUMBER() OVER(PARTITION BY [t7].[ReferenceId] ORDER BY [t7].[Id]) AS [row]
FROM [TypeTexts] AS [t7]
WHERE [t7].[Language] = @__locale_LanguageIdentifier_0
) AS [t6]
WHERE [t6].[row] <= 1
) AS [t5] ON [t1].[Id0] = [t5].[ReferenceId]
这还不错,它不是一个超级复杂的查询,但我觉得我的需求可以用更简单的 SQL 查询来解决:
SELECT *
FROM [Systems] AS [p]
JOIN [Templates] AS [t] ON [p].[TemplateId] = [t].[Id]
JOIN [TemplateTexts] AS [tt] ON [p].[TemplateId] = [tt].[ReferenceId]
JOIN [Types] AS [ty] ON [t].[TypeId] = [ty].[Id]
JOIN [TemplateTexts] AS [tyt] ON [ty].[Id] = [tyt].[ReferenceId]
WHERE [p].[SystemId] = @systemId and tt.[Language] = 2 and tyt.[Language] = 2
我的问题是:是否有一个 different/simpler LINQ 表达式(在方法语法或查询语法中)产生相同的结果(一次性获取所有信息),因为理想情况下我不希望有拥有一个匿名对象,其中聚合了过滤后的子集合。对于更多的布朗尼积分,如果生成的 SQL 是 simpler/closer 我认为是一个简单的查询,那就太好了。
此 LINQ 查询与您的 SQL 接近,但我担心结果的正确性:
var resultSet =
(from system in dbContext.Systems
from templateText in system.Template.TemplateTexts
where templateText.Language == locale.LanguageIdentifier
from typeText in system.Template.Type.TypeTexts
where typeText.Language == locale.LanguageIdentifier
select new
{
System = system,
TemplateText = templateText
TypeText = typeText
})
.FirstOrDefault(x => x.System.Id == request.Id);
Is there a different/simpler LINQ expression (...) that produces the same result
是(也许)但不是。
不,因为您正在查询 dbContext.Systems
,因此 EF 将 return 所有与您的过滤器匹配的系统,即使它们没有 TemplateTexts
等。这就是为什么必须生成外连接。 EF 不知道您明显打算跳过没有这些嵌套数据的系统,也不知道这些系统不会出现在数据库中。 (您似乎假设,看到第二个查询)。
这说明了左连接到子查询。
这些子查询是因为FirstOrDefault
而产生的。在 SQL 中,它总是需要某种子查询来获取一对多关系的“第一个”记录。这个 ROW_NUMBER() OVER
构造实际上是相当高效的。您的第二个查询没有任何“第一”记录的概念。它可能 return 不同的数据。
是的(也许),因为您还 Include
数据。我不确定为什么。有些人似乎认为 Include
是进行后续预测 (.Select
) 所必需的,但事实并非如此。如果这是您使用 Include
s 的原因,那么您可以删除它们,从而删除前几个连接。
OTOH 你也 Include
system.Project
不在投影中,所以你似乎故意添加了 Include
s。在这种情况下,它们会起作用,因为整个实体 system
都在投影中,否则 EF 会忽略它们。
如果您再次需要 Include
,出于上述原因,EF 必须生成外部联接。
EF 决定单独处理 Include
s 和预测,而手工制作 SQL,在数据先验知识的帮助下可以更有效地做到这一点。但是没有办法影响这种行为。