Ken*_*ith 5 c# performance entity-framework entity-framework-6
此MSDN文章列出了改善实体框架性能的方法:
https://msdn.microsoft.com/zh-CN/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;
}
Run Code Online (Sandbox Code Playgroud)
但是我没有看到任何真正的性能优势。在两台不同的计算机上,这些是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
Run Code Online (Sandbox Code Playgroud)
使用局部变量-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}");
}
Run Code Online (Sandbox Code Playgroud)
结果(超过10,000次迭代)更类似于上面的MSDN文章所述:
Indirect seconds: 141.32, direct seconds:91.95, iqueryable seconds:93.96
我不是 100% 确定本文可以描述实体框架版本 6 的当前行为,但这应该与实体框架中的查询编译为存储过程有关。
当您第一次使用 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
Run Code Online (Sandbox Code Playgroud)
@p__linq__0
是查询中的一个参数,因此每次更改查询代码中的 Id 时,实体框架都会从查询缓存中选择完全相同的语句并调用它,而不会再次尝试为其编译 SQL。另一方面N''Some Name'' = [Extent1].[Name]
部分等于 code x.Name == "Some Name"
,我在这里使用了一个常量,因此它不是转换为查询参数,而是转换为查询语句的简单部分。
每次尝试进行查询时,实体框架都会检查包含已编译 SQL 语句的缓存,以查看是否存在可以与参数一起重用的已编译语句。如果未找到该语句,则实体框架必须再次将 C# 查询编译为 Sql。因此,如果您的查询很小且编译速度很快,您将不会注意到任何事情,但是如果您有包含大量包含、条件、转换和内置函数使用的“难以编译”的查询,您可以点击当您的查询未命中实体框架编译的查询缓存时,将受到重罚。
您可以在此处看到与当前分页工作的一些相似之处,而不使用Skip
和 的重载,Take
在更改页面时不命中已编译的查询缓存:强制实体框架使用 SQL 参数化以获得更好的 SQL proc 缓存重用
在您的代码中使用常量时,您可能会遇到这种影响,而且其影响并不明显。让我们比较这些代码片段和 EntityFramework 生成的 SQL(为了简洁,我省略了类定义,应该很明显):
查询 1
示例代码:
var result = context.Activities
.Where(x => x.IssuedAt >= DateTime.UtcNow && x.Id == iteration)
.ToList();
Run Code Online (Sandbox Code Playgroud)
生成的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
Run Code Online (Sandbox Code Playgroud)
您可以看到,在这种情况下, conditionx.IssuedAt >= DateTime.UtcNow
被转换为 statement [Extent1].[IssuedAt] >= (SysUtcDateTime())
。
查询 2
示例代码:
var now = DateTime.UtcNow;
var result = context.Activities
.Where(x => x.IssuedAt >= now && x.Id == iteration)
.ToList();
Run Code Online (Sandbox Code Playgroud)
生成的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
Run Code Online (Sandbox Code Playgroud)
在这种情况下,您可以看到条件x.IssuedAt >= now
已转换为[Extent1].[IssuedAt] >= @p__linq__0
- 参数化语句,并且 DateTime 值作为过程参数传递。
您可以清楚地看到这里与查询 1 的区别 - 条件是没有参数的查询代码的一部分,它使用内置函数获取日期时间。
这两个查询可能会给你一个提示,实体框架中常量的使用会产生与仅使用字段、属性、参数等不同的查询。这是一个合成示例,让我们检查一些更接近真实查询的内容。
查询 3
在这里,我使用枚举ActivityStatus
并希望查询具有特定 Id 的活动,并且我希望能够仅获取状态为“活动”(无论这意味着什么)的活动。
示例代码:
var result = context.Activities
.Where(x => x.Status == ActivityStatus.Active
&& x.Id == id)
.ToList();
Run Code Online (Sandbox Code Playgroud)
生成的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
Run Code Online (Sandbox Code Playgroud)
您可以看到,在条件中使用 constantx.Status == ActivityStatus.Active
会产生 SQL 0 = [Extent1].[Status]
,这是正确的。这里的状态没有参数化,所以如果你在其他地方使用 condition 调用相同的查询x.Status = ActivityStatus.Pending
,那将产生另一个查询,所以第一次调用它会导致实体框架查询编译。您可以对两者使用查询 4来避免它。
查询 4
示例代码:
var status = ActivityStatus.Active;
var result = context.Activities
.Where(x => x.Status == status
&& x.Id == iteration)
.ToList();
Run Code Online (Sandbox Code Playgroud)
生成的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
Run Code Online (Sandbox Code Playgroud)
如您所见,此查询语句已完全参数化,因此将状态更改为 Pending、Active、Inactive 等仍将使用来自已编译查询缓存的相同查询。
根据您的编码风格,您可能会不时遇到这个问题,当相同的 2 个只有不同常量值的查询将分别编译一个查询时。我可以为您提供使用布尔值作为常量尝试相同的查询,它应该产生相同的结果 - 条件未参数化。
归档时间: |
|
查看次数: |
3261 次 |
最近记录: |