本文属于《理解性能的奥秘——应用程序中慢,SSMS中快》系列
接上文:理解性能的奥秘——应用程序中慢,SSMS中快(4)——收集解决参数嗅探问题的信息
- 查询使用的参数嗅探完全不合适。也就是说,查询计划对于这次执行是合适的,但是对于下一次执行就可能不合适。
- 应用程序中存在特定的调用模式,而且与其他大部分调用模式差异很大。通常这种调用是针对初次启动或者新一天的开始时调用。
- 一个或多个表上的索引结构不能很好地支持查询,但是又有某些次优的索引,导致优化过程中,优化器会无计划选择这些索引。
没有办法的办法:
基于输入的最优索引:
CREATE PROCEDURE List_orders_12 @custid nchar(5), @fromdate datetime, @todate datetime AS SELECT * FROM Orders WHERE CustomerID = @custid AND OrderDate BETWEEN @fromdate AND @todate
use Northwind go EXEC List_orders_12 'SAVEA', '19970811', '19970811' go sp_recompile List_orders_12 go EXEC List_orders_12 'CENTC', '19960101', '19961231'
注意,对于ASVEA,我们仅查询一天的订单,但是对于CENTC,我们查询一年的数据,你可能想到,这两个调用应该使用不同的索引,下面是实际执行计划:

CREATE PROCEDURE List_orders_12 @custid nchar(5), @fromdate datetime, @todate datetime AS SELECT * FROM Orders WHERE CustomerID = @custid AND OrderDate BETWEEN @fromdate AND @todate OPTION (RECOMPILE)
CREATE PROCEDURE List_orders_12 @custid nchar(5), @fromdate datetime, @todate datetime WITH RECOMPILE AS
对于这个存储过程,使用哪种方式都无所谓,因为它是单语句的。但是对于代码很长的存储过程,使用WITH RECOMPILE不是最好的方式,因为这样会引起整个存储过程重编译,增加重编译开销。另外关于WITH RECOMPILE的一个特性,就是计划不会存入缓存,但是使用OPTION (RECOMPILE)就会存入计划缓存中。
- 存储过程被调用的频率很高,重编译会明显影响系统性能。
- 查询非常复杂并且编译时间已经明显影响了响应时间。
动态查询条件:
WHERE (CustomerID = @custid OR @custid IS NULL) AND (OrderDate = @orderdate OR @orderdate IS NULL) ...
正如你想象的,参数嗅探对这类存储过程没有好处。我(作者)不打算在这里浪费太多篇幅,因为:1)因为前面提到过,这种情况更多是应用程序的问题。2)我写了一系列独立的文章:T-SQL动态查询(1)——简介 、T-SQL动态查询(2)——关键字查询、T-SQL动态查询(3)——静态SQL、T-SQL动态查询(4)——动态SQL
评估索引:
SELECT DISTINCT c.* FROM Table_C c JOIN Table_B b ON c.Col1 = b.Col2 JOIN Table_A a ON a.Col4 = b.Col1 WHERE a.Col1 = @p1 AND a.Col2 = @p2 AND a.Col3 = @p3
- 非聚集、不唯一索引Comb0_ix ,覆盖Col1,Col2,Col5,Col4 列。
- 非聚集、不唯一索引Col2_ix,覆盖Col2列。
- 非聚集、不唯一索引Col3_ix ,覆盖Col3列。
DBCC SHOW_STATISTICS (Table_A, Col3_ix)

SELECT Col3, COUNT(*) FROM Table_A GROUP BY Col3 ORDER BY Col3
然后重新研究慢查询的查询计划,检查优化器嗅探@p3的参数值是什么。然后发现是“APPLE”,一个不存在于表中的值。也就是说,第一次存储过程执行时,SQL Server预估的影响行数是1(注意不会出现0行),所以优化器认为对于查找单行数据,使用Col3列上的索引是最高效的。
- OPTION(RECOMPILE)/ WITH RECOMPILE
- 添加一个可选的索引,覆盖Col1,Col2,Col3,并且包含Col4。
- 改写Col3的索引为筛选索引,或者直接删掉。
- 使用索引提示强制查询使用其他索引。
- 使用OPTIMIZE FOR查询提示。
- 把@p3的值复制到本地变量。
- 修改应用程序行为。
添加新索引:
更改/删除Col3上的索引:
Col3上的索引真正用处在哪里?因为不是熟悉的系统,所以也不能回答这个问题。但是通常来说,在选择度很低的列上的索引一般都不高效甚至无效。所谓其中一个方案就是删除Col3上的 索引,避免优化器对这些索引进行考虑。也许索引是因为某个原因在某个时候错误添加,或者是很多很多年前添加的。但是随着系统的使用,不能一成不变地对待。
CREATE INDEX col3_ix ON Table_A(col3) WHERE col3 IN ('FIG', 'RASPBERRY', 'APPLE', 'APRICOT')
这样有两个好处:
- 索引的体积降低了将近99%。
- 这个索引不再成为问题查询的影响因素,因为SQL Server必须选择一个可以满足所有输入值的查询计划,所以即使嗅探到“APPLE”这个值,SQL Server也不会使用,这个索引,因为对于KIWI这个值而言,查询计划不能覆盖。
强制使用不同的索引:
SELECT c.* FROM Table_C c JOIN Table_B b ON c.Col1 = b.Ccol2 JOIN Table_A a WITH (INDEX = Combo_ix) ON a.Col4 = b.Col1 WHERE a.Col1 = @p1 AND a.Col2 = @p2 AND a.Col3 = @p3
WITH (INDEX (combo_ix, col2_ix))
让优化器在这两个“好”的索引之间选择。
OPTIMIZE FOR:
SELECT c.* FROM Table_C c JOIN Table_B b ON c.col1 = b.col2 JOIN Table_A a ON a.col4 = b.col1 WHERE a.col1 = @p1 AND a.col2 = @p2 AND a.col3 = @p3 OPTION (OPTIMIZE FOR (@p3 = 'KIWI'))
OPTION (OPTIMIZE FOR (@p3 UNKNOWN))
复制参数到本地变量:
修改应用程序:
小结:
应用程序缓存引起的案例:
CREATE PROCEDURE memdb_get_updated_customers @tstamp timestamp AS SELECT CustomerID, CustomerName, Address, ..., tstamp FROM Customers WHERE tstamp > @tstamp
当MemDB调用这些存储过程时,会传入timestamp中最高的值,主存数据库会传回0x作为参数,获取所有数据。在某些情况下,发现MemDB的存储过程运行时间很长,引入MemDB的目的是分担负载,但是现在反而增加了。
EXEC memdb_get_updated_customers 0x
EXEC memdb_get_updated_customers 0x000000000003E806
但是在夜间因为某些批处理导致内存不足的情况下,存储过程实行失败并不是罕见的事情。所以常见的情况有,在早上运行时,缓存里面实际上没有任何查询计划,然后嗅探0x值。对于这个值,优化器会使用Timestamp上的索引吗?如果是聚集索引,会嗅探,但是由于Timestamp更新得如此频繁,以至这个列并不适合作为聚集索引的候选键。所以一般Timestamp上的索引都是非聚集索引,因此,当优化器看到参数意味着需要返回所有数据,会使用表扫描操作,这个查询计划会放入计划缓存,然后后续的执行都会使用表扫描,即使仅仅是查询最常用的行。这明显会影响性能。
OPTION(RECOMPILE):
EXECUTE WITH RECOMPILE:
EXECUTE memdb_get_updated_customers WITH RECOMPILE
cmd.CommandType = CommandType.Text; cmd.Text = "EXECUTE memdb_get_updated_customers @tstamp WITH RECOMPILE";
使用一个封装好的存储过程:
CREATE PROCEDURE memdb_get_updated_customers @tstamp timestamp AS IF @tstamp = 0x EXECUTE memdb_get_updated_customers_inner @tstamp WITH RECOMPILE ELSE EXECUTE memdb_get_updated_customers_inner @tstamp
不同的代码路径:
CREATE PROCEDURE memdb_get_updated_customers @tstamp timestamp AS IF @tstamp = 0x BEGIN SELECT CustomerID, CustomerName, Address, ..., tstamp FROM Customers END ELSE BEGIN SELECT CustomerID, CustomerName, Address, ..., tstamp FROM Customers WITH (INDEX = timestamp_ix) WHERE tstamp > @tstamp END
不同的存储过程:
CREATE PROCEDURE memdb_get_transactions @transid int AS IF coalesce(@transid, 0) = 0 EXECUTE memdb_get_transactions_refresh ELSE BEGIN DECLARE @maxtransid int SELECT @maxtransid = MAX(transid) FROM transactions EXECUTE memdb_get_transactions_delta @transid, @maxtransid END
这部分比较易懂,不做详细说明。
修复问题SQL:
SELECT ... FROM Orders WHERE (CustomerID = @custid OR @custid IS NULL) AND (EmployeeID = @empid OR @empid IS NULL) AND convert(varchar, OrderDate, 101) = convert(varchar, @orderdate, 101)
允许用户查询很多不同的参数组合。开发人员考虑到OrderDate可能包含时间部分,所以使用了Convert()函数来补全程序端传入不带时间部分的参数。对于这个查询,OrderDate上的索引会被优化器选中,但是这种情况下,SQL Server不会使用索引查找,因为OrderDate已经变成了表达式,由于统计信息丢失原因,这种情况成为非SARG写法。
SELECT ... FROM Orders WHERE (CustomerID = @custid OR @custid IS NULL) AND (EmployeeID = @empid OR @empid IS NULL) AND OrderDate >= @orderdate AND OrderDate < dateadd(DAY, 1, @orderdate)
低效写法通常是性能问题的常见原因,也就是说查询总是很慢。哪怕跟参数嗅探没有关系。但是当你遇到参数嗅探问题时,探讨是否可以避免输入不好的参数避免参数嗅探也同样依赖写法。低效写法有很多中,不可能在这里一一列出。隐式转换是常见的情况,同样会导致索引失效。
总结:
- SQL Server如何编译动态SQL