问题
你想为多次用到的查询提高性能,而且你不想添加额外的编码或配置.
解决方案
假设你有如Figure 13-8 所示的模型
Figure 13-8. A model with an Associate and its related Paycheck
在这个模型里,每个Associate(同事)有0到多个Paychecks(薪水),你有一个LINQ查询,它在你的整个应用程序中重复使用,你想仅编译一次,然后复用这个已编译的版本,通过这种方式来提高这个查询性能。
当针对数据库执行时,EF必须把你的强类型的LINQ查询转换成对应的SQL查询(基于你的数据库引擎,SqlServer,Oracle等等),在EF5时,每个查询转换在默认情况下会被缓存,这个过程与“自动缓存”相关,后面的每次LINQ查询,都直接从“查询计划缓存”里重新取回,这样就绕过了转换的步骤.对于包含参数的查询,改变参数值,仍然会重新获取相同的查询.有趣地是,这个”查询计划缓存”在同一个应用程序域里的上下文对象里共享,也就是说,一旦缓存了,在同一个应用程序域里的任何一个上下文对象都访问它.
在Listing 13-10,我们比较了启用和禁用缓存的性能.为说明带来的性能,我们把LinQ查询的编译版本和非编译版本迭代10次的时间打印出来.在这个查询里,我们可以看到大致有2倍的性能提升.多数情况下是由于编译需要相对高的成本,而执行查询却只需要低的成本.
Listing 13-20. Comparing the Performance of a Simple Compiled LINQ Query
private static void RunUncompiledQuery()
{
using (var context = new EFRecipesEntities())
{
// Explicitly disable query plan caching
var objectContext = ((IObjectContextAdapter)context).ObjectContext;
var associateNoCache = objectContext.CreateObjectSet<Associate>();
associateNoCache.EnablePlanCaching = false;
var watch = new Stopwatch();
long totalTicks = 0;
// warm things up
associateNoCache.Include(x => x.Paychecks).Where(a => a.Name.StartsWith("Karen")).ToList();
// query gets compiled each time
for (var i = 0; i < 10; i++)
{
watch.Restart();
associateNoCache.Include(x => x.Paychecks).Where(a => a.Name.StartsWith("Karen")).ToList();
watch.Stop();
totalTicks += watch.ElapsedTicks;
Console.WriteLine("Not Compiled #{0}: {1}", i, watch.ElapsedTicks);
}
Console.WriteLine("Average ticks without compiling: {0}", (totalTicks / 10));
Console.WriteLine("");
}
}
private static void RunCompiledQuery()
{
using (var context = new EFRecipesEntities())
{
var watch = new Stopwatch();
long totalTicks = 0;
// warm things up
context.Associates.Include(x => x.Paychecks).Where(a => a.Name.StartsWith("Karen")).ToList();
totalTicks = 0;
for (var i = 0; i < 10; i++)
{
watch.Restart();
context.Associates.Include(x => x.Paychecks).Where(a => a.Name.StartsWith("Karen")).ToList();
watch.Stop();
totalTicks += watch.ElapsedTicks;
Console.WriteLine("Compiled #{0}: {1}", i, watch.ElapsedTicks);
}
Console.WriteLine("Average ticks with compiling: {0}", (totalTicks / 10));
}
}
输出结果如下:
Not Compiled #0: 10014
Not Compiled #1: 5004
Not Compiled #2: 5178
Not Compiled #3: 7624
Not Compiled #4: 4839
Not Compiled #5: 5017
Not Compiled #6: 4864
Not Compiled #7: 5090
Not Compiled #8: 4499
Not Compiled #9: 6942
Average ticks without compiling: 5907
Compiled #0: 3458
Compiled #1: 1524
Compiled #2: 1320
Compiled #3: 1283
Compiled #4: 1202
Compiled #5: 1145
Compiled #6: 1075
Compiled #7: 1104
Compiled #8: 1081
Compiled #9: 1084
Average ticks with compiling: 1427
它是如何工作的
当你运行一个LINQ查询时,EF为该查询创建一个表达式树对象,然后该对象转换或编译入一个内部命令树.该内部命令树会被传递给数据库提供者并被转换为相应的数据库命令(通常是SQL).转换一个表达式树的代价可能相当高,主要取决于查询复杂度和底层的模型.模型如果有很深层的继承或是很多的水平方向上的引入,会使得转换处理过程相当复杂,这样编译的所花的时间要比执行查询所花的时间多得多.然后在EF5为LINQ查询引入了查询自动缓存技术.你可以通过查看Listing 13-20 的执行结果里看出它所提高的性能.
另外,如Listing 13-20 所示,你也能禁用”自动编译”特性,通过DbContext对象的底层对象ObjectContext,得到一个实体对象的引用,并设置它的EnablePlanCaching属性为false.
为了跟踪每个已编译的查询,EF遍历查询表达式树节点,并创建一个哈希表,用它作为已编译查询的索引,为后面的每个调用,EF会先尝试从缓存查找哈希表的主键,以节省查询转换处理带来的成本.需要注意的是,”查询缓存计划”不依赖上下文对象,它是被绑定到应用程序的应用程序域,也就意味着,缓存的查询对于所有的上下文实例都是可用的.
当底层的查询缓存包含800或更多缓存计划时,每一分钟,一个清除处理会根据LFRU(least frequently/recently used使用次数最少,最近不用)算法(根据查询被命中的次数和它的时限)来移除一个缓存.
已编译的查询对Asp.net的分页查询尤其有用,分页查询的参数可能会改变,但是查询是一致的,也是能复用到每一页的展示上,这是因为一个已编译的查询是”被参数化的”,也就是说能接受不同的参数值.