实体框架查询缓存

本文关键字:缓存 查询 框架 实体 | 更新日期: 2023-09-27 18:12:10

这篇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文章基本上是错误的,引用对象的方式对查询缓存没有任何影响?

**编辑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

实体框架查询缓存

我不能100%确定这篇文章可以描述当前的行为作为实体框架版本6,但这应该是有关查询在实体框架编译成存储过程。

当你第一次使用实体框架调用一些查询时,它必须被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时,实体框架将从查询缓存中选择这个完全相同的语句并调用它,而不需要再次为它编译SQL。另一方面,N''Some Name'' = [Extent1].[Name]部分等于代码x.Name == "Some Name",我在这里使用了一个常量,所以它不是转换为查询参数,而是转换为查询语句的简单部分。

每次你尝试进行查询,实体框架检查缓存包含编译SQL语句,看看是否有一个已经编译的语句,它可以与参数重用。如果没有找到该语句,则实体框架必须再次将c#查询编译为Sql。因此,如果你的查询小而快速编译,你不会注意到任何东西,但是如果你有"难以编译"的查询,有很多包含,条件,转换和内置函数使用,当你的查询没有命中实体框架编译的查询缓存时,你可能会受到严重的惩罚。

SkipTake中,你可以看到当前的分页工作在没有使用过载的情况下有一些相似之处,在更改页面时不会撞击已编译的查询缓存:强制实体框架使用SQL参数化以更好地重用SQL进程缓存

在代码中使用常量时,您可能会遇到这种影响,而且它的影响相当不明显。让我们比较一下这些代码片段和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的区别- condition是查询代码的一部分,没有参数,它使用内置函数来获取日期时间。

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

查询

3

在这里,我使用enum 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中使用constant会生成SQL 0 = [Extent1].[Status],这是正确的。这里的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等,仍然会使用编译查询缓存中的相同查询。

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