步骤1:确立商业案例
我们需要做的第一件事是问“需要多快?”,因为如果我们不知道它需要多快,我们就无法知道何时完成。这不是技术决定,而是商业决策。您需要一个以利益相关者为中心的“足够快”的度量标准,并且需要记住“足够快”就足够了。除非有商业原因,否则我们不会寻求“尽可能快”。即使在这种情况下,我们通常也在寻找“在预算范围内尽可能快”。
由于您是我的利益相关者,并且您似乎对存储过程的性能并不太担心,让我们将其用作基准!
步骤2:测量
接下来,我们需要测量系统以查看是否足够快。
谢天谢地,您已经进行了测量(尽管我们稍后会再谈)。您的存储过程运行时间为0.5秒!这足够快吗?是的!任务完成!
没有理由继续花费你的时间(和你老板的钱)去修复一个本来没有问题的东西。你可能有更好的事情要做,所以去做吧!:D
你还在吗?好的。我现在不在工作时间,但是有人在说我喜欢的技术不好,并且优化Entity Framework查询非常有趣。接受挑战!
第三步:检查
那么出了什么问题?为什么我们的查询如此缓慢?
为了回答这个问题,我需要对你的模型做出一些假设:-
public class Foo
{
public int Id { get; set; }
public int BarId { get; set; }
public virtual Bar Bar { get; set; }
}
public class Bar
{
public int Id { get; set; }
public string Value { get; set; }
public virtual ICollection<Foo> Foos { get; set; }
}
现在我们已经完成了这一步,可以看一下Entity Framework为我们生成的可怕查询语句:-
using (var context = new FooContext())
{
context.Database.Log = s => Console.WriteLine(s);
var query = context.Foos.FirstOrDefault(x => x.Id == 1).Bar.Value;
}
我可以从日志中看到有两个查询正在运行:
SELECT TOP (1)
[Extent1].[Id] AS [Id],
[Extent1].[BarId] AS [BarId]
FROM [dbo].[Foos] AS [Extent1]
WHERE 1 = [Extent1].[Id]
SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Value] AS [Value]
FROM [dbo].[Bars] AS [Extent1]
WHERE [Extent1].[Id] = @EntityKeyValue1
等等,什么?为什么愚蠢的实体框架在我们只需要一个字符串时进行两次数据库往返?
步骤4:分析
让我们退后一步,再次查看我们的查询:
var query = context.Foos.FirstOrDefault(x => x.Id == 1).Bar.Value
鉴于我们所知道的延迟执行,我们能推断出这里正在发生什么吗?
延迟执行基本上意味着只要您使用IQueryable
,就不会实际发生任何事情-查询在内存中构建而不实际执行直到以后。这有很多用处-特别是它让我们以模块化方式构建查询,然后运行组合的查询。如果context.Foos
立即加载整个Foo
表格到内存中,Entity Framework将非常无用!
我们的查询只有在我们请求除IQueryable
之外的内容时才运行,例如使用.AsEnumerable()
、.ToList()
或特别是.GetEnumerator()
等。在这种情况下,.FirstOrDefault()
不返回IQueryable
,因此这比我们预期的早触发了数据库调用。
我们所做的查询基本上是在说:
- 获取第一个
Id == 1
的 Foo
(如果没有则返回 null
)
- 现在Lazy Load这个
Foo
的Bar
- 现在告诉我
Bar
的Value
哇!我们不仅要向数据库发送两次查询,还要传输整个Foo
和Bar
!当我们的实体像这里构造的虚假实体一样小的时候,这并不算太糟糕,但如果它们是更大的实际实体呢?
步骤5:优化
正如你从上面所了解到的,优化的第一条规则是“不要”,第二条规则是“先衡量”。优化的第三条规则是“避免不必要的工作”。额外的查询和大量的无用数据肯定是“不必要”的,因此让我们采取一些措施:-
尝试1
我们首先要尝试的是声明式方法。"找到第一个具有Id == 1
的Foo
的Bar
的值"。
从可维护性的角度来看,这通常是最清晰的选择;程序员的意图显然被捕捉到了。但是,请记住我们希望尽可能地延迟执行时间,让我们在.Select()
之后加上.FirstOrDefault()
:
var query = context.Bars.Where(x => x.Foos.Any(y => y.Id == 1))
.Select(x => x.Value)
.FirstOrDefault();
SELECT TOP (1)
[Extent1].[Value] AS [Value]
FROM [dbo].[Bars] AS [Extent1]
WHERE EXISTS (SELECT
1 AS [C1]
FROM [dbo].[Foos] AS [Extent2]
WHERE ([Extent1].[Id] = [Extent2].[BarId]) AND (1 = [Extent2].[Id])
)
尝试2
在SQL和大多数O/RMs中,一个有用的技巧是确保你从任何给定关系的正确“端点”查询。当然,我们正在寻找一个Bar
,但我们已经拥有了一个Foo
的Id
,所以我们可以以此为起点重写查询:“找到Id == 1
的Foo
的Bar
的Value
”:
var query = context.Foos.Where(x => x.Id == 1)
.Select(x => x.Bar.Value)
.FirstOrDefault();
SELECT TOP (1)
[Extent2].[Value] AS [Value]
FROM [dbo].[Foos] AS [Extent1]
INNER JOIN [dbo].[Bars] AS [Extent2] ON [Extent1].[BarId] = [Extent2].[Id]
WHERE 1 = [Extent1].[Id]
非常好。Prima Facie这些看起来比原始的Entity-Framework生成的混乱代码和原始存储过程更可取。完成!
步骤6:测量
不!等一分钟!我们怎么知道我们足够快?我们怎么知道我们更快了吗?
我们测量!
不幸的是,您将不得不自己完成这部分。我可以告诉您,在我的机器上,在我的网络上,模拟我的应用程序的实际负载,INNER JOIN
是最快的,其次是两个往返版本(!!),其次是WHERE EXISTS
版本,最后是存储过程。我无法告诉您在您的硬件、网络下,在您的应用程序的实际负载下哪个最快。
我可以告诉你,我已经进行了十多次这种精确的性能优化,根据网络、数据库服务器和模式的特点,我发现三个中的所有内容,即 INNER JOIN、WHERE EXISTS 和两个往返都可以提供最佳性能。然而,我甚至不能告诉你,其中任何一个是否足够快。根据您的需求,您可能需要手动编写一些超级优化的 SQL 并调用存储过程。您甚至可能需要进一步使用去正规化的读取优化的读取存储。您考虑过为数据库结果使用内存缓存吗?您考虑过为 Web 服务器使用输出缓存吗?如果这个查询甚至不是瓶颈呢?良好的性能并不是加速 Entity Framework 查询。像我们行业中的任何事情一样,良好的性能在于知道客户关心什么,并找到最佳方法来满足他们的需求。