今天我想进一步谈下SQL Server里的计划缓存和它的副作用。我们都知道,每个提交到SQL Server的逻辑查询,会编译为物理执行计划。这个执行计划然后会缓存为所谓的计划缓存,用于后期重用。现在我们首先来谈下即席SQL语句和它的负作用,还有它们带来的性能问题。
即席SQL语句(Adhoc SQL Statements)
每次当你提交一个即席SQL语句到SQL Server,对于每个特定查询,都会生成一个执行计划。“特定查询”是什么意思?答案很简单:SQL Server对每个完整的SQL语句(包括你的参数值)生成一个哈希值,并使用这个哈希值作为计划缓存的查找值。如果使用这个哈希值找到一个执行计划,计划就会重用,否则在计划缓存里会编译一个新的执行计划。假设你提交下列3个查询到SQL Server:
SELECT * FROM Sales.SalesOrderHeader WHERE CustomerID = 11000 GO SELECT * FROM Sales.SalesOrderHeader WHERE CustomerID = 30052 GO SELECT * FROM Sales.SalesOrderHeader WHERE CustomerID = 11223 GO
对于这3个查询,SQL Server编译3个不同的执行计划,因为你提供了硬编码的参数值。因此3个查询间会计算不同的的哈希值,不会找到已缓存的计划。作为副作用,在计划缓存里,现在你有近3个近乎一样的查询有3个不同的计划。这个特定问题被称为计划缓存污染。
你刚用不同的执行计划污染了你的计划缓存,这很难重用(因为硬编码的参数值),而且你在浪费可以被SQL Server里其它组件使用的有用内存。缓存的目的应该提高重用数,但使用即席SQL语句就做不到。
计划稳定性
假设你为你的SQL语句使用参数值,或者你甚至使用存储过程。在这个情况下,SQL Server非常容易重用缓存的执行计划。但即使重用缓存的执行计划,你回引入性能问题。例如SQL Server为一个查询编译了一个执行计划,它回进行书签查找,因为非聚集索引没有覆盖你的查询:
我们提过,书签查找只有在从表里获取一些数据才有意义。如果你越过了临界点,使用全表扫描或聚集索引扫描更高效。但如果SQL Server冲了缓存的执行计划,这个选项就不会考虑太多——SQL Server会盲目重用你的计划——这时你的性能就会很差!可以看下面的例子:
这里SQL盲目重用了有书签查找的缓存计划。你会看到估计和实际行数有很大差别!SQL Server在假设从查询里只返回一条记录来编译和缓存计划。但实际上从SQL Server我们拿回1499条记录。你看到的执行计划,是假设只有一条记录返回的情况下优化——考虑下这个情况。
这里潜在的根源是你的计划不稳定。基于估计行数,你得到有书签查找的缓存计划,如果你越过临界点,会是表/聚集索引扫描。这是我们经常碰到的常见SQL Server性能问题。
你如何解决这个问题?简单:通过覆盖非聚集索引来避免书签查找。使用这个方法你会获得计划稳定性,不管你的输入参数,你会得到同样的性能。
小结
今天你学到了SQL Server里,计划缓存的双刃剑:在一方面,计划缓存非常强大,因为你可以重用计划缓存避免编译资源占用。在另一方面,他非常危险,使用定型的执行计划,你的计划不再稳定,这就意味着你不能再保证性能。
感谢关注!