我需要加速下面的Linq查询。

3

我正在将一个旧的存储过程重写为 EF Linq 查询,然而这个存储过程的执行速度几乎是新查询的三倍!

以下是查询语法的示例:

public string GetStringByID(long ID)
    {
        return dataContext.Table2.FirstOrDefault(x => x.Table2ID == ID).Table1.StringValue;
    }

这是我使用的存储过程代码,以及调用它的方法。

存储过程为:

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:

我这样调用proc:

string myString = context.MyQuickerProc(127).FirstOrDefault();

我使用了单元测试和计时器发现Linq调用需要1.3秒,而sproc调用只需要0.5秒,这太久了! 我正在调查缺失的FK,因为我只能假设这是这些调用如此耗时的原因。

无论如何,我需要加速这个Linq查询,并添加当前Linq查询不包含的缺失功能(if/else逻辑)。

如果有任何帮助将不胜感激。谢谢提前!


4
如果进程更快,为什么要重写它?您可以在实体框架中调用进程。 - HLGEM
HLGEM - 我希望在这个项目中更多地使用Linq,因为我发现它开发、阅读和维护起来都更快。因此,我希望重写并加速这个过程,部分是为了练习未来项目的开发,部分是为了在整个项目中实现统一。 - Humble Rumble
1
@LukeRumbelow - 通常来说,编写维护性代码是一件好事,但请记住,在某些情况下,封装(例如此示例中的proc方向)是更好的模式。 - Gregory A Beamer
从Entity Framework 5.0开始(如果使用.Net 4.5),EF具有编译查询缓存功能,该功能在第一次执行查询时激活。确保在任何给定AppDomain的生命周期内第二或第三次执行查询时对其进行基准测试。 - Federico Berasategui
2
@LukeRumbelow:我认为HighCore指的不是数据缓存,而是将表达式树编译成SQL进行缓存。这是EF的开销,每个应用程序域只需支付一次。如果您的分析代码每次运行查询一次,则会导致您得到不正确的数字。 - Iain Galloway
显示剩余4条评论
3个回答

10

步骤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 == 1Foo(如果没有则返回 null
  • 现在Lazy Load这个FooBar
  • 现在告诉我BarValue

哇!我们不仅要向数据库发送两次查询,还要传输整个FooBar!当我们的实体像这里构造的虚假实体一样小的时候,这并不算太糟糕,但如果它们是更大的实际实体呢?

步骤5:优化

正如你从上面所了解到的,优化的第一条规则是“不要”,第二条规则是“先衡量”。优化的第三条规则是“避免不必要的工作”。额外的查询和大量的无用数据肯定是“不必要”的,因此让我们采取一些措施:-

尝试1

我们首先要尝试的是声明式方法。"找到第一个具有Id == 1FooBar的值"。

从可维护性的角度来看,这通常是最清晰的选择;程序员的意图显然被捕捉到了。但是,请记住我们希望尽可能地延迟执行时间,让我们在.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,但我们已经拥有了一个FooId,所以我们可以以此为起点重写查询:“找到Id == 1FooBarValue”:

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 查询。像我们行业中的任何事情一样,良好的性能在于知道客户关心什么,并找到最佳方法来满足他们的需求。

谢谢您的这篇文章,非常有趣。我特别喜欢您使用database.Log提取执行的SQL的方式。不幸的是,所有优化尝试都在1.3秒左右。我的备选方案始终是像您所说的那样,在我的缓存容器中缓存结果,现在我正在走这条路(因为完整的结果集相当小,需要非常经常地访问)。感谢您的帮助,非常感激。 - Humble Rumble
没有听从自己的建议:你在基准测试中运行查询多次了吗?只运行一次并使用Stopwatch计时不是一个真实的测试。特别是,如评论中所述,EF必须每个应用程序域编译一次表达式树到SQL,这会导致开销。存储过程确实可能更快,但对我来说看起来有些可疑;实际查询在我的机器上运行时间小于10ms。如果您感兴趣,可以发布您的分析代码,但无论如何,只要您足够快就可以了。 - Iain Galloway
好久不见,艾恩 - 很好的问题分解和非常有启发性的逐步分析 - 金星 :) - jim tollan

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{....}

注意,我还添加了一些逻辑检查,如果结果为空。您正在使用FirstOrDefault,如果未找到结果,则会导致.Table1抛出异常。如果您从不期望结果为空,则应将调用更改为First(),或处理空情况。

另一件事情是,您应该查看EF如何配置以匹配NULL情况,这可能会减慢查询速度。查看此帖子(不是链接到我的帖子,但它很相关): EntityFramework LINQToEntities generate weird slow TSQL Where-Clause


明天早上我会尝试这段代码,对其进行分析并告诉您结果,谢谢。 - Humble Rumble

0

这应该会产生正确的结果,但我无法确定它的效率如何;您需要进行性能分析。请注意,查询实际上只会从数据库中获取单个字符串,并且不需要Entity Framework进行任何客户端处理。

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语句,但我没有尝试过Entity Framework是否会生成相同的查询。但由于这是一个单一的查询,甚至有一点机会,Entity Framework可以通过其条件第二个查询优于存储过程,但显然只是因为重新构造的查询。
 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

明天早上我会尝试这段代码,对其进行分析并告诉您结果,谢谢。 - Humble Rumble

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接