SELECT * FROM A INNER JOIN B ON a.a=b.bINNER JOIN C ON c.c=a.a代码清单1.
图1.被缓存的执行计划
那究竟这几类对象缓存所占用的内存相关信息该怎么看呢?我们可以通过dm_os_memory_cache_counters这个DMV看到,上述几类被缓存的对象如图2所示。
图2.在内存中这几类对象缓存所占用的内存
另外,执行计划缓存是一种缓存。而缓存中的对象会根据算法被替换掉。对于执行计划缓存来说,被替换的算法主要是基于内存压力。而内存压力会被分为两种,既内部压力和外部压力。外部压力是由于Buffer Pool的可用空间降到某一临界值(该临界值会根据物理内存的大小而不同,如果设置了最大内存则根据最大内存来)。内部压力是由于执行计划缓存中的对象超过某一个阈值,比如说32位的SQL Server该阈值为40000,而64位中该值被提升到了160000。
这里重点说一下,缓存的标识符是查询语句本身,因此select * from SchemaName.TableName和Select * from TableName虽然效果一致,但需要缓存两份执行计划,所以一个Best Practice是在引用表名称和以及其他对象的名称时,请带上架构名称。
基于被缓存的执行计划对语句进行调优
被缓存的执行计划所存储的内容非常丰富,不仅仅包括被缓存的执行计划、语句,还包括被缓存执行计划的统计信息,比如说CPU的使用、等待时间等。但这里值得注意的是,这里的统计只算执行时间,而不算编译时间。比如说我们可以利用代码清单2中的代码根据被缓存的执行计划找到数据库中耗时最长的20个查询语句。
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTEDSELECT TOP 20CAST(qs.total_elapsed_time / 1000000.0 AS DECIMAL(28, 2))AS [Total Duration (s)], CAST(qs.total_worker_time * 100.0 / qs.total_elapsed_time AS DECIMAL(28, 2)) AS [% CPU], CAST((qs.total_elapsed_time - qs.total_worker_time)* 100.0 / qs.total_elapsed_time AS DECIMAL(28, 2)) AS [% Waiting], qs.execution_count, CAST(qs.total_elapsed_time / 1000000.0 / qs.execution_count AS DECIMAL(28, 2)) AS [Average Duration (s)], SUBSTRING (qt.text,(qs.statement_start_offset/2) + 1, ((CASE WHEN qs.statement_end_offset = -1THEN LEN(CONVERT(NVARCHAR(MAX), qt.text)) * 2ELSE qs.statement_end_offsetEND - qs.statement_start_offset)/2) + 1) AS [Individual Query, qt.text AS [Parent Query], DB_NAME(qt.dbid) AS DatabaseName, qp.query_plan FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) as qt CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp WHERE qs.total_elapsed_time > 0 ORDER BY qs.total_elapsed_time DESC代码清单2.通过执行计划缓存找到数据库总耗时最长的20个查询语句
图3.仅仅是由于不同的参数,查询优化器选择不同的执行计划
大家可能会觉得,这不是挺好的嘛,根据参数产生不同的执行计划。那让我们再考虑一个问题,如果将上面的查询放到一个存储过程中,参数不能被直接嗅探到,当第一个执行计划被缓存后,第二次执行会复用第一次的执行计划!虽然免去了编译时间,但不好的执行计划所消耗的成本会更高!让我们来看这个例子,如图4所示。
图4.不同的参数,却是完全一样的执行计划!
再让我们看同一个例子,把执行顺序颠倒后,如图5所示。
图5.执行计划完全变了
我们看到,第二次执行的语句,完全复用了第一次的执行计划。那总会有一个查询牺牲。比如说当参数为4时会有5000多条,此时索引扫描应该最高效,但图4却复用了上一个执行计划,使用了5000多次查找!!!这无疑是低效率的。而且这种情况出现会非常让DBA迷茫,因为在缓存中的执行计划不可控,缓存中的对象随时可能被删除,谁先执行谁后执行产生的性能问题往往也让DBA头疼。
由这个例子我们看出,查询优化器希望尽可能选择高效的执行计划,而执行计划缓存却希望尽可能的重用缓存,这两种机制在某些情况会产生冲突。
在下篇文章中,我们将会继续来看由于执行计划缓存和查询分析器的冲突,以及编译执行计划所带来的常见问题和解决方案。
小结
本篇文章中,我们简单讲述了查询优化器生成执行计划的过程,以及执行计划缓存的机制。当查询优化器和执行计划缓存以某种不好的情况交汇时,将产生一些问题。在下篇文章中,我们会继续探索SQL Server中的执行计划缓存。
以上内容是小编给大家介绍的SQL Server中的执行计划缓存(上)的全部叙述,希望大家喜欢。