我需要加快以下Linq查询

时间:2014-05-13 15:30:03

标签: c# sql linq entity-framework

我有一个旧的存储过程我正在重写为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逻辑)。

对此的任何帮助将不胜感激。提前谢谢:)

3 个答案:

答案 0 :(得分:10)

第1步:建立业务案例

我们需要做的第一件事是询问" 它需要多快?",因为如果我们不知道它需要多快我们不能知道我们何时完成。这不是一个技术决定,它是一个商业决策。您需要一个以利益相关者为中心的衡量标准" Fast Enough"瞄准,你需要记住Fast Fast足够快。我们不是在寻找"尽可能快"除非有商业原因。即便如此,我们通常也会在预算范围内尽快寻找"。

既然您是我的利益相关者,并且您似乎对存储过程的性能感到不满,那就让我们将其作为基准!

第2步:测量

接下来我们需要做的是衡量我们的系统,看看我们是否足够快。

谢天谢地,你已经测量过了(虽然我们稍后会详细讨论这个问题)。 您的存储过程在0.5秒内运行!这够快吗?是的! Job done!

没有理由继续花时间(和老板的钱)修复一些没有破坏的东西。你可能有更好的事情去做,所以去做吧! :d


还在吗?那好吧。 I'm not on the clock, people are badmouthing tech I like, and optimising Entity Framework queries is fun接受挑战!

第3步:检查

那是怎么回事?为什么我们的查询这么慢?

要回答这个问题,我需要对你的模型做一些假设: -

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;

鉴于我们对Deferred Execution的了解,我们可以推断出这是什么?

延迟执行基本上意味着只要您使用IQueryable,实际上什么也没发生 - 查询是在内存中构建的,直到实际执行才会执行后来。这有很多原因 - 特别是它允许我们以模块化方式构建查询,然后运行组合查询一次。如果context.Foos立即将整个Foo表加载到内存中,实体框架将毫无用处!

我们的查询仅在我们要求IQueryable之外的其他内容时运行,例如使用.AsEnumerable().ToList()或特别.GetEnumerator()等。在这种情况下,.FirstOrDefault()不会返回IQueryable,因此这会触发数据库调用很多比我们想象的更早。

我们提出的问题基本上是: -

  • 使用Foo获取第一个Id == 1(如果没有,请null
  • Foo&#39; s Bar
  • 现在Lazy Load
  • 现在告诉我Bar&#39; s Value

哇!因此,我们不仅要对数据库进行两次往返,我们还要通过电汇发送整个FooBar!当我们的实体像这里的人为实体一样微小时,这不是那么,但如果它们是更大的现实实体呢?

第5步:优化

正如您希望从上面收集到的那样,前两个优化规则是1)&#34; Don&#tt &#34;和2)&#34; 先测量&#34;优化的第三条规则是&#34; 避免不必要的工作&#34;。额外的往返和一大堆虚假数据肯定算作“不必要的”,所以让我们做点什么: -

尝试1

我们要做的第一件事是尝试声明式方法。 &#34;找到Bar FooId == 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生成的混乱原始存储过程更好。完成!

步骤6:测量

没有!等一下!我们怎么知道我们是否足够快?我们怎么知道我们是否更快?

我们测量!

不幸的是,你必须自己做这件事。我可以告诉你,在我的机器上,在我的网络上,模拟我的应用程序的实际负载,INNER JOIN是最快,然后是两个往返版本 (!!) ,然后是WHERE EXISTS版本,然后是存储过程。 我无法告诉您 应用

可以告诉您,我已经进行了十几次完全性能优化,具体取决于网络,数据库服务器和架构的特性我已经看到所有三个INNER JOINWHERE 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