我有一个旧的存储过程我正在重写为EF Linq查询但是proc几乎快了3倍!
这是查询语法的一个示例:
public string GetStringByID(long ID)
{
return dataContext.Table2.FirstOrDefault(x => x.Table2ID == ID).Table1.StringValue;
}
这里是我正在使用的sproc代码以及调用它的方法。
sproc是:
PROCEDURE [dbo].[MyQuickerProc]
@ID bigint
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS(SELECT TOP 1 ID FROM Table2 WHERE Table2ID = @Id)
BEGIN
SELECT TOP 1 t1.StringValue
FROM Table2 t2
INNER JOIN Table1 t1 ON t1.Table1ID= Table2.Table1ID
WHERE Table2ID = @ID
END
ELSE
BEGIN
SELECT TOP 1 t1.StringValue
FROM Table2 t2
INNER JOIN Table1 t1 ON t1.Table1Id = Table2.Table1ID
WHERE Table2ID IS NULL
END
END
我这样叫proc:
string myString = context.MyQuickerProc(127).FirstOrDefault();
我已经使用了单元测试并停止观察发现Linq呼叫需要1.3秒,而sproc呼叫需要0.5秒,令人震惊的长!我正在调查失踪的FK,因为我只能假设这就是这些电话花了这么长时间的原因。
在任何情况下,我都需要加速这个Linq查询并添加sproc所缺少的功能,并且当前的Linq查询不包含(if / else逻辑)。
对此的任何帮助将不胜感激。提前谢谢:)
答案 0 :(得分:10)
我们需要做的第一件事是询问" 它需要多快?",因为如果我们不知道它需要多快我们不能知道我们何时完成。这不是一个技术决定,它是一个商业决策。您需要一个以利益相关者为中心的衡量标准" Fast Enough"瞄准,你需要记住Fast Fast足够快。我们不是在寻找"尽可能快"除非有商业原因。即便如此,我们通常也会在预算范围内尽快寻找"。
既然您是我的利益相关者,并且您似乎对存储过程的性能感到不满,那就让我们将其作为基准!
接下来我们需要做的是衡量我们的系统,看看我们是否足够快。
谢天谢地,你已经测量过了(虽然我们稍后会详细讨论这个问题)。 您的存储过程在0.5秒内运行!这够快吗?是的! Job done! 强>
没有理由继续花时间(和老板的钱)修复一些没有破坏的东西。你可能有更好的事情去做,所以去做吧! :d
还在吗?那好吧。 I'm not on the clock, people are badmouthing tech I like, and optimising Entity Framework queries is fun。 接受挑战!
那是怎么回事?为什么我们的查询这么慢?
要回答这个问题,我需要对你的模型做一些假设: -
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
等等,什么?为什么当我们需要的是一个字符串时,愚蠢的实体框架会对数据库进行两次往返?
让我们退后一步,再次查看我们的查询: -
var query = context.Foos.FirstOrDefault(x => x.Id == 1).Bar.Value;
鉴于我们对Deferred Execution的了解,我们可以推断出这是什么?
延迟执行基本上意味着只要您使用IQueryable
,实际上什么也没发生 - 查询是在内存中构建的,直到实际执行才会执行后来。这有很多原因 - 特别是它允许我们以模块化方式构建查询,然后运行组合查询一次。如果context.Foos
立即将整个Foo
表加载到内存中,实体框架将毫无用处!
我们的查询仅在我们要求IQueryable
之外的其他内容时运行,例如使用.AsEnumerable()
,.ToList()
或特别.GetEnumerator()
等。在这种情况下,.FirstOrDefault()
不会返回IQueryable
,因此这会触发数据库调用很多比我们想象的更早。
我们提出的问题基本上是: -
Foo
获取第一个Id == 1
(如果没有,请null
Foo
&#39; s Bar
Bar
&#39; s Value
哇!因此,我们不仅要对数据库进行两次往返,我们还要通过电汇发送整个Foo
和Bar
!当我们的实体像这里的人为实体一样微小时,这不是那么,但如果它们是更大的现实实体呢?
正如您希望从上面收集到的那样,前两个优化规则是1)&#34; Don&#tt &#34;和2)&#34; 先测量&#34;优化的第三条规则是&#34; 避免不必要的工作&#34;。额外的往返和一大堆虚假数据肯定算作“不必要的”,所以让我们做点什么: -
尝试1
我们要做的第一件事是尝试声明式方法。 &#34;找到Bar
Foo
与Id == 1
&#34;的第一个.FirstOrDefault()
的值。
从可维护性的角度来看,这通常是最明智的选择;程序员的意图显然是被捕获的。但是,记住我们希望尽可能延迟执行,让我们在.Select()
之后弹出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])
)
: -
Bar
尝试2
在SQL和大多数O / RM中,一个有用的技巧是确保您从正确的&#34;结束&#34;任何给定的关系。当然,我们正在寻找Id
,但我们已 Foo
Value
,因此我们可以重写查询以此作为起点:&#34;使用Bar
&#34;找到Foo
Id == 1
的{{1}}: -
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生成的混乱和原始存储过程更好。完成!
没有!等一下!我们怎么知道我们是否足够快?我们怎么知道我们是否更快?
我们测量!
不幸的是,你必须自己做这件事。我可以告诉你,在我的机器上,在我的网络上,模拟我的应用程序的实际负载,INNER JOIN
是最快,然后是两个往返版本 (!!) ,然后是WHERE EXISTS
版本,然后是存储过程。 我无法告诉您 应用
我可以告诉您,我已经进行了十几次完全性能优化,具体取决于网络,数据库服务器和架构的特性我已经看到所有三个INNER JOIN
,WHERE EXISTS
和两次往返都能带来最佳效果。
然而, 我甚至无法告诉您 根据您的需要,您可能需要手动滚动一些超级优化的SQL并调用存储过程。您甚至可能需要进一步使用非规范化读取优化读取存储。为数据库结果使用内存缓存怎么样?如何为您的网络服务器使用输出缓存?如果这个查询甚至不是瓶颈怎么办?
良好的性能不是加快实体框架查询的速度。 良好的表现,就像我们行业中的任何事情一样,是关于了解客户的重要信息,并找出最佳方式。
答案 1 :(得分:0)
我建议做的第一件事是在你的linq查询上调用ToString()来查看正在生成的SQL。根据您的查询和配置,您可能会两次访问数据库,一次获取Table2,然后再次通过延迟加载获取关联的Table1实体。您应该尝试使用SQL事件探查器验证是否是这种情况,或者逐步调试调试器。看看是否重写您的查询,如下所示添加了任何热切加载相关实体的性能增强:
var result = dataContext.Table2.
.include("Table1")
.FirstOrDefault(x => x.Table2ID == ID);
if(result != null){
return result..Table1.StringValue;
}else{....}
注意我还在一些逻辑检查中添加了result是否为null。您正在使用FirstOrDefault,如果找不到结果,将导致.Table1抛出异常。如果您从未期望结果为null,或者处理null情况,我会将调用更改为First()。
您应该关注的另一件事是如何配置EF以匹配NULL情况,这可能会降低您的查询速度。看看这篇文章(不是链接到我自己的帖子,但它的相关): EntityFramework LINQToEntities generate weird slow TSQL Where-Clause
答案 2 :(得分:0)
这应该产生正确的结果,但我不知道它的效率如何;你将不得不剖析它。请注意,查询实际上只会从数据库中获取单个字符串,而不需要实体框架进行任何客户端处理。
dataContext.Table2
.Where(x => (x.Table2ID == ID) || (x.Table2ID == null))
.OrderByDescending(x => x.Table2ID) // This will place ID before NULL.
.Select(x => x.Table1.StringValue)
.First()
使用LINQPad我或多或少得到了预期的SQL语句,但我没有尝试实体框架会产生相同的查询。但是因为这是一个单一的查询,实体框架甚至可能会通过条件化的第二个查询来超越存储过程,但显然只是因为重新构造的查询。
SELECT TOP (1) [t1].[StringValue]
FROM [Table2] AS [t2]
LEFT OUTER JOIN [Table1] AS [t1]
ON [t1].[Table1ID] = [t2].[Table1ID]
WHERE ([t2].[Table2ID] = @ID) OR ([t2].[Table2ID] IS NULL)
ORDER BY [t2].[Table2ID] DESC