实体框架查询缓存

时间:2016-10-08 00:08:37

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

此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

使用本地变量 - &#34;直接&#34; MSDN文章推荐的方法 - 可能是最快的(4/5次),但不是一致的,而且实际上并不多。

我在测试中做错了什么?或者效果真的很轻微,它没有太大的区别?或者MSDN文章基本上是错误的,这种引用对象的方式对查询缓存没有任何影响?

**编辑10/9/16 ** 我将查询修改为(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

1 个答案:

答案 0 :(得分:3)

我并非100%确定本文可以描述Entity Framework第6版的当前行为,但是这一点应该与Entity Framework中的查询编译相关联到存储过程。

当您第一次使用Entity Framework调用某个查询时,必须将它编译为一个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 >= DateTime.UtcNow会转换为语句[Extent1].[IssuedAt] >= (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 >= now已转换为[Extent1].[IssuedAt] >= @p__linq__0 - 参数化语句,并且DateTime值作为过程参数传递。

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

这两个查询可能会给你一个提示,实体框架中常量的使用会产生不同的查询,只使用字段,属性,参数等。这是一个合成的例子,让我们检查更接近真实查询的东西。

查询3

这里我使用enum ActivityStatus并想要查询具有特定ID的Activity,并且我希望只能获得状态为“Active”的活动(无论这意味着什么)。

示例代码:

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在其他地方调用相同的查询,则会生成另一个查询,因此第一次调用它将导致实体框架查询编译。您可以使用查询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个查询只有不同的常量值时,每个查询都会编译一个查询。我可以提供你使用布尔值作为常量来尝试相同的查询,它应该产生相同的结果 - 条件没有参数化。