【问题标题】:Entity Framework query caching实体框架查询缓存
【发布时间】:2016-10-08 00:08:37
【问题描述】:

这篇 MSDN 文章列出了一系列提高实体框架性能的方法:

https://msdn.microsoft.com/en-us/data/hh949853.aspx

它的一个建议(4.3)是将非映射对象的属性转换为局部变量,以便EF可以缓存其内部查询计划。

听起来是个好主意。因此,我使用一个简单的查询对其进行了测试,该查询将查询中间接属性引用的 10,000 次迭代的性能与局部变量进行了比较。像这样:

[Fact]
public void TestQueryCaching()
{
    const int iterations = 1000;

    var quote = new Quote();
    using (var ctx = new CoreContext())
    {
        quote.QuoteId = ctx.Quotes.First().Id;
    }

    double indirect = 0;
    double direct = 0;

    10.Times(it =>
    {
        indirect += PerformCoreDbTest(iterations, "IndirectValue", (ctx, i) =>
           {
               var dbQuote = ctx.Quotes.First(x => x.Id == quote.QuoteId);
           }).TotalSeconds;
        direct += PerformCoreDbTest(iterations, "DirectValue", (ctx, i) =>
            {
                var quoteId = quote.QuoteId;
                var dbQuote = ctx.Quotes.First(x => x.Id == quoteId);
            }).TotalSeconds;
    });

    _logger.Debug($"Indirect seconds: {indirect:0.00}, direct seconds:{direct:0.00}");
}

protected TimeSpan PerformCoreDbTest(int iterations, string descriptor, Action<ICoreContext, int> testAction)
{
    var sw = new Stopwatch();
    sw.Start();
    for (var i = 0; i < iterations; i++)
    {
        using (var ctx = new CoreContext())
        {
            testAction(ctx, i);
        }
    }
    sw.Stop();
    _logger.DebugFormat("{0}: Took {1} milliseconds for {2} iterations",
        descriptor, sw.Elapsed.TotalMilliseconds, iterations);
    return sw.Elapsed;
}

但我没有看到任何真正的性能优势。在两台不同的机器上,这些是 5 次迭代的结果:

Machine1 - Indirect seconds: 9.06, direct seconds:9.36
Machine1 - Indirect seconds: 9.98, direct seconds:9.84
Machine2 - Indirect seconds: 22.41, direct seconds:20.38
Machine2 - Indirect seconds: 17.27, direct seconds:16.93
Machine2 - Indirect seconds: 16.35, direct seconds:16.32

使用局部变量 - MSDN 文章推荐的“直接”方法 - 可能稍微快一点(4/5 倍),但不是始终如一,也不是很多。

我在测试中做错了吗?还是影响真的很小以至于没有太大区别?还是 MSDN 文章基本上是错误的,并且这种引用对象的方式对查询缓存没有任何影响?

** 2016 年 10 月 9 日编辑 ** 我将查询修改为 (a) 使其更复杂,并且 (b) 每次都传入不同的 quoteId。我怀疑后者很重要,否则查询实际上会被缓存 - 因为没有任何参数。请参阅下面@raderick 的答案。

这是更复杂的测试:

[Fact]
public void TestQueryCaching()
{
    const int iterations = 1000;

    List<EFQuote> quotes;
    using (var ctx = new CoreContext())
    {
        quotes = ctx.Quotes.Take(iterations).ToList();
    }

    double indirect = 0;
    double direct = 0;
    double iqueryable = 0;

    10.Times(it =>
    {
        indirect += PerformCoreDbTest(iterations, "IndirectValue", (ctx, i) =>
        {
            var quote = quotes[i];
            var dbQuote = ctx.Quotes
             .Include(x => x.QuoteGroup.QuoteGroupElements.Select(e => e.DefaultElement.DefaultChoices))
             .Include(x => x.QuoteElements.Select(e => e.DefaultElement.DefaultChoices))
             .Include(x => x.QuotePackage)
             .Include(x => x.QuoteDefinition)
             .Include(x => x.QuoteLines)
             .First(x => x.Id == quote.Id);
        }).TotalSeconds;
        direct += PerformCoreDbTest(iterations, "DirectValue", (ctx, i) =>
        {
            var quoteId = quotes[i].Id;
            var dbQuote = ctx.Quotes
                .Include(x => x.QuoteGroup.QuoteGroupElements.Select(e => e.DefaultElement.DefaultChoices))
                .Include(x => x.QuoteElements.Select(e => e.DefaultElement.DefaultChoices))
                .Include(x => x.QuotePackage)
                .Include(x => x.QuoteDefinition)
                .Include(x => x.QuoteLines)
                .First(x => x.Id == quoteId);
        }).TotalSeconds;
        iqueryable += PerformCoreDbTest(iterations, "IQueryable", (ctx, i) =>
        {
            var quoteId = quotes[i].Id;
            var dbQuote = ctx.Quotes
                    .Include(x => x.QuoteGroup.QuoteGroupElements.Select(e => e.DefaultElement.DefaultChoices))
                    .Include(x => x.QuoteElements.Select(e => e.DefaultElement.DefaultChoices))
                    .Include(x => x.QuotePackage)
                    .Include(x => x.QuoteDefinition)
                    .Include(x => x.QuoteLines)
                    .Where(x => x.Id == quoteId).First();
        }).TotalSeconds;
    });

    _logger.Debug($"Indirect seconds: {indirect:0.00}, direct seconds:{direct:0.00}, iqueryable seconds:{iqueryable:0.00}");
}

结果(超过 10,000 次总迭代)与上面的 MSDN 文章描述的更相似:

Indirect seconds: 141.32, direct seconds:91.95, iqueryable seconds:93.96

【问题讨论】:

    标签: c# performance entity-framework entity-framework-6


    【解决方案1】:

    我不能 100% 确定这篇文章是否可以描述 Entity Framework 版本 6 的当前行为,但这应该与 Entity Framework 中的查询编译成存储过程有关。

    当您第一次使用 Entity Framework 调用某个查询时,它必须由 EF 编译成 SQL 语句 - 可以是纯 SELECT 查询,也可以是使用 exec 及其参数的过程,例如:

    exec sp_executesql N'SELECT 
        [Extent1].[Id] AS [Id], 
        [Extent1].[Name] AS [Name], 
        [Extent1].[IssuedAt] AS [IssuedAt], 
        [Extent1].[Status] AS [Status], 
        [Extent1].[Foo_Id] AS [Foo_Id]
        FROM [dbo].[Activities] AS [Extent1]
        WHERE (N''Some Name'' = [Extent1].[Name]) AND ([Extent1].[Id] = @p__linq__0)',N'@p__linq__0 int',@p__linq__0=0
    

    @p__linq__0 是查询中的一个参数,因此每次您在查询代码中更改 Id 时,Entity Framework 都会从查询缓存中选择完全相同的语句并调用它,而无需再次尝试为其编译 SQL。另一方面,N''Some Name'' = [Extent1].[Name] 部分等于代码x.Name == "Some Name",我在这里使用了一个常量,所以它不是转换为查询参数,而是转换为查询语句的简单部分。

    每次您尝试进行查询时,Entity Framework 都会检查包含已编译 SQL 语句的缓存,以查看是否有已编译的语句可以重新使用参数。如果找不到该语句,Entity Framework 必须再次将 C# 查询编译为 Sql。因此,如果您的查询很小且编译速度快,您不会注意到任何内容,但如果您有很多包含、条件、转换和内置函数用法的“难以编译”的查询,您可以点击当您的查询未命中 Entity Framework 编译的查询缓存时将受到重罚。

    您可以在此处看到与当前分页工作的一些相似之处,而无需为 SkipTake 使用重载,更改页面时未命中已编译查询缓存:Force Entity Framework to use SQL parameterization for better SQL proc cache reuse

    在代码中使用常量时可能会遇到这种影响,其影响并不明显。让我们比较一下 EntityFramework 生成的这些代码片段和 SQL(为了简洁起见,我省略了类定义,应该很明显):

    查询 1

    示例代码:

    var result = context.Activities
                        .Where(x => x.IssuedAt >= DateTime.UtcNow && x.Id == iteration)    
                        .ToList(); 
    

    生产的 Sql:

    exec sp_executesql N'SELECT 
        [Extent1].[Id] AS [Id], 
        [Extent1].[Name] AS [Name], 
        [Extent1].[IssuedAt] AS [IssuedAt], 
        [Extent1].[Status] AS [Status], 
        [Extent1].[Foo_Id] AS [Foo_Id]
        FROM [dbo].[Activities] AS [Extent1]
        WHERE ([Extent1].[IssuedAt] >= (SysUtcDateTime())) AND ([Extent1].[Id] = @p__linq__0)',N'@p__linq__0 int',@p__linq__0=0
    

    您可以看到,在这种情况下,条件x.IssuedAt &gt;= DateTime.UtcNow 被转换为语句[Extent1].[IssuedAt] &gt;= (SysUtcDateTime())

    查询 2

    示例代码:

    var now = DateTime.UtcNow;
    
    var result = context.Activities
                        .Where(x => x.IssuedAt >= now && x.Id == iteration)
                        .ToList();
    

    生产的 Sql:

    exec sp_executesql N'SELECT 
        [Extent1].[Id] AS [Id], 
        [Extent1].[Name] AS [Name], 
        [Extent1].[IssuedAt] AS [IssuedAt], 
        [Extent1].[Status] AS [Status], 
        [Extent1].[Foo_Id] AS [Foo_Id]
        FROM [dbo].[Activities] AS [Extent1]
        WHERE ([Extent1].[IssuedAt] >= @p__linq__0) AND ([Extent1].[Id] = @p__linq__1)',N'@p__linq__0 datetime2(7),@p__linq__1 int',@p__linq__0='2016-10-09 15:27:37.3798971',@p__linq__1=0
    

    在这种情况下,您可以看到条件 x.IssuedAt &gt;= now 已转换为 [Extent1].[IssuedAt] &gt;= @p__linq__0 - 参数化语句,并且 DateTime 值作为过程参数传递。

    您可以清楚地看到这里与查询 1 的区别 - 条件是不带参数的查询代码的一部分,它使用内置函数来获取日期时间。

    这两个查询可能会给您一个提示,即在实体框架中使用常量会产生与仅使用字段、属性、参数等不同的查询。这是一个综合示例,让我们检查更接近真实查询的内容。

    查询 3

    在这里,我使用枚举 ActivityStatus 并想查询具有特定 ID 的活动,并且我希望能够仅获取状态为“活动”(无论这意味着什么)的活动。

    示例代码:

    var result = context.Activities
        .Where(x => x.Status == ActivityStatus.Active 
                    && x.Id == id)
        .ToList();
    

    生产的 Sql:

    exec sp_executesql N'SELECT 
        [Extent1].[Id] AS [Id], 
        [Extent1].[Name] AS [Name], 
        [Extent1].[IssuedAt] AS [IssuedAt], 
        [Extent1].[Status] AS [Status], 
        [Extent1].[Foo_Id] AS [Foo_Id]
        FROM [dbo].[Activities] AS [Extent1]
        WHERE (0 = [Extent1].[Status]) AND ([Extent1].[Id] = @p__linq__0)',N'@p__linq__0 int',@p__linq__0=0
    

    您可以看到,在条件x.Status == ActivityStatus.Active 中使用常量会产生 SQL 0 = [Extent1].[Status],这是正确的。这里的状态没有参数化,所以如果你在其他地方使用条件x.Status = ActivityStatus.Pending调用相同的查询,那将产生另一个查询,所以第一次调用它会导致实体框架查询编译。您可以同时使用 Query 4 来避免它。

    查询 4

    示例代码:

    var status = ActivityStatus.Active;
    
    var result = context.Activities
                        .Where(x => x.Status == status
                                    && x.Id == iteration)
                        .ToList();
    

    生产的 Sql:

    exec sp_executesql N'SELECT 
        [Extent1].[Id] AS [Id], 
        [Extent1].[Name] AS [Name], 
        [Extent1].[IssuedAt] AS [IssuedAt], 
        [Extent1].[Status] AS [Status], 
        [Extent1].[Foo_Id] AS [Foo_Id]
        FROM [dbo].[Activities] AS [Extent1]
        WHERE ([Extent1].[Status] = @p__linq__0) AND ([Extent1].[Id] = @p__linq__1)',N'@p__linq__0 int,@p__linq__1 int',@p__linq__0=0,@p__linq__1=0
    

    如您所见,此查询语句已完全参数化,因此将状态更改为 Pending、Active、Inactive 等仍将使用已编译查询缓存中的相同查询。

    根据您的编码风格,您可能会不时遇到此问题,当相同的 2 个仅具有不同常量值的查询将分别编译一个查询时。我可以为您提供尝试使用布尔值作为常量的相同查询,它应该产生相同的结果 - 条件未参数化。

    【讨论】:

    • 很好的解释 - 感谢您的辛勤工作。你关于更复杂查询的观点很有意义,所以我把它变得更复杂,我认为你基本上是对的。请参阅上面对我的问题的修改。
    • @KenSmith 很高兴为您解决问题!您是否检查了 SQL Server Profiler 中生成的查询?案例 1 和案例 2 有区别吗?
    • 有趣的是,不,不是在 SQL 级别。这三个(非常复杂的)查询在 SQL 级别上是完全相同的(kdiff 发音为这样),并且都被参数化了。我认为性能提升一定是完全在 EF 方面,底部两个能够在查询之间共享 EF 查询元数据,而顶部则不能。
    • @KenSmith 您能否也将您的 PerformCoreDbTest 代码添加到问题中?我有一种感觉,它可能会以某种方式影响结果。您还需要在执行之前预热查询以获得更精确的结果。由于查询缓存,将此测试分成 3 个不同的测试也可能产生不同的结果。
    • 我在上面添加了PerformCoreDbTest() 方法。同意预热有助于完美的精确度 - 但超过 10,000 次迭代,我认为它可能相当准确。
    猜你喜欢
    • 2014-03-09
    • 2014-11-22
    • 1970-01-01
    • 2012-12-13
    • 1970-01-01
    • 1970-01-01
    • 2018-10-29
    • 2021-04-13
    • 2015-08-21
    相关资源
    最近更新 更多