我需要加快以下Linq查询

Hum*_*ble 3 c# sql linq entity-framework

我有一个旧的存储过程我正在重写为EF Linq查询但是proc几乎快了3倍!

这是查询语法的示例:

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

这是我正在使用的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
Run Code Online (Sandbox Code Playgroud)

我把这个叫做proc:

string myString = context.MyQuickerProc(127).FirstOrDefault();
Run Code Online (Sandbox Code Playgroud)

我已经使用了单元测试并且停止观察发现Linq呼叫需要1.3秒而且sproc呼叫需要0.5秒,令人震惊的长!我正在调查失踪的FK,因为我只能假设这就是这些电话花了这么长时间的原因.

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

任何有关这方面的帮助将非常感激.提前致谢 :)

Iai*_*way 10

第1步:建立业务案例

我们需要做的第一件事就是问" 它需要多快? ",因为如果我们不知道它需要多快,我们就不知道什么时候完成.这不是技术决定,而是业务决策.您需要一个以利益相关者为中心的"快速足够"的衡量标准,并且您需要牢记快速足够快.我们不是在寻找"尽可能快",除非有商业原因.即便如此,我们通常也在寻找"尽可能快地在预算范围内".

由于您是我的利益相关者,并且您似乎对存储过程的性能不太感到沮丧,让我们将其作为基准!

第2步:测量

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

谢天谢地你已经测量过了(虽然我们稍后会详细讨论).您的存储过程在0.5秒内运行!这够快吗?是的!任务完成!

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


还在?那好吧.我不是时间,人们喜欢糟糕的技术,优化实体框架查询很有趣.接受挑战!

第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; }
}
Run Code Online (Sandbox Code Playgroud)

既然我们已经这样做了,我们可以看一下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;
}
Run Code Online (Sandbox Code Playgroud)

我可以从日志中看到正在运行两个查询: -

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
Run Code Online (Sandbox Code Playgroud)

等等,什么?为什么当我们需要的是一个字符串时,愚蠢的实体框架会对数据库进行两次往返?

第4步:分析

让我们退一步再看看我们的查询: -

var query = context.Foos.FirstOrDefault(x => x.Id == 1).Bar.Value;
Run Code Online (Sandbox Code Playgroud)

鉴于我们对延期执行的了解,我们可以推断出什么?

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

我们的查询仅在我们请求除了an之外的其他内容时运行IQueryable,例如with .AsEnumerable(),.ToList()或者特别是.GetEnumerator()等等.在这种情况下.FirstOrDefault()不返回a IQueryable,因此这比我们想要的更早地触发数据库调用.

我们所做的查询基本上是说: -

  • 获得第一个FooId == 1(或null如果没有任何)
  • 现在延迟加载FooBar
  • 现在告诉我BarValue

哇!所以,我们不仅做两个往返到数据库中,我们也发送整个FooBar跨线!当我们的实体像这里的人为实体一样微小时,这并不是那么糟糕,但如果它们是更大的现实实体呢?

第5步:优化

正如你希望从上面得到的,前两个优化规则是1)" 不要 "和2)" 先测量 "优化的第三个规则是" 避免不必要的工作 ".额外的往返和一大堆虚假数据肯定算是"不必要的",所以让我们做点什么: -

尝试1

我们要做的第一件事是尝试声明式方法."找到第Bar一个Foo带有Id == 1" 的价值".

从可维护性的角度来看,这通常是最明智的选择; 程序员的意图显然是被捕获的.但是,记住我们想要尽可能延迟执行,让我们在以下.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])
)
Run Code Online (Sandbox Code Playgroud)

尝试2

在SQL和大多数O/RM中,一个有用的技巧是确保从任何给定关系的正确"结束"查询.当然,我们正在寻找一个Bar,但我们已经拿到IdFoo,所以我们可以用为起点重新编写查询:"查找我ValueBarFooId == 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]
Run Code Online (Sandbox Code Playgroud)

好多了.Prima Facie看起来比原始的Entity-Framework生成的混乱原始存储过程都要好.完成!

第6步:测量

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

我们衡量!

不幸的是,你必须自己做这一点.我可以告诉你,在我的机器上,在我的网络上,模拟我的应用程序的实际负载,INNER JOIN是最快的,然后是两个往返版本(!!),然后是WHERE EXISTS版本,接着是存储过程.我不能告诉你这将是最快的在你的硬件,在你的网络中,一个现实的负载下您的应用程序.

可以告诉你,我做了这个确切的性能优化十几次,并根据网络,数据库服务器的特点和模式我已经看到了所有三个INNER JOIN,WHERE EXISTS和两个往返提供最佳性能.

但是,我甚至不能告诉你,如果任何这些都是足够快.根据您的需要,您可能需要手动滚动一些超级优化的SQL并调用存储过程.您甚至可能需要进一步使用非规范化读取优化读取存储.为数据库结果使用内存缓存怎么样?如何为您的网络服务器使用输出缓存?如果这个查询甚至不是瓶颈怎么办?

良好的性能不是关于加速实体框架查询.良好的性能,就像我们行业中的任何事情一样,是关于了解对客户重要的事情,并找出获得它的最佳方式.