百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

SQL Server - 最佳实践 - 参数嗅探问题

bigegpt 2024-08-04 11:38 8 浏览

author: 风移

摘要

MSSQL Server参数嗅探既是一个涉及知识面非常广泛,又是一个比较难于解决的课题,即使对于数据库老手也是一个比较头痛的问题。这篇文章从参数嗅探是什么,如何产生,表象是什么,会带来哪些问题,如何解决这五个方面来探讨参数嗅探的来龙去脉,期望能够将SQL Server参数嗅探问题理清楚,道明白。

什么参数嗅探

当SQL Server第一次执查询语句或存储过程(或者查询语句与存储过程被强制重新编译)的时候,SQL Server会有一个进程来评估传入的参数,并根据传入的参数生成对应的执行计划缓存,然后参数的值会伴随查询语句或存储过程执行计划一并保存在执行计划缓存里。这个评估的过程就叫着参数嗅探。

参数嗅探是如何产生的

SQL Server对查询语句编译和缓存机制是SQL语句执行过程中非常重要的环节,也是SQLOS内存管理非常重要的一环。理由是SQL Server对查询语句编译过程是非常消耗系统性能,代价昂贵的。因为它需要从成百上千条执行路径中选择一条最优的执行计划方案。所以,查询语句可以重用执行计划的缓存,避免重复编译,以此来节约系统开销。这种编译查询语句,选择最优执行方案,缓存执行计划的机制就是参数嗅探问题产生的理论基础。

参数嗅探的表象

以上是比较枯燥的理论解释,这里我们来看看两个实际的例子。在此,我们以AdventureWorks2008R2数据库中的Sales.SalesOrderDetail表做为我们测试的数据源,我们挑选其中三个典型的产品,ProductID分别为897,945和870,分别对应的订单总数为2,257和4688。

挑选的方法如下:

use AdventureWorks2008R2GO;WITH DATA

AS(select ProductID, COUNT(1) as order_count, rowid = ROW_NUMBER() OVER(ORDER BY COUNT(1) asc)from Sales.SalesOrderDetail with(nolock)group by ProductID

)SELECT *FROM DATAWHERE rowid in (1, 266, 133)

得到如下结果:

查询语句的参数嗅探表象

接下来,我们看三个非常相似的查询语句(仅传入的参数值不同)的执行计划有什么差异。

三个查询语句:

use AdventureWorks2008R2GOSELECT SalesOrderDetailID, OrderQtyFROM Sales.SalesOrderDetail WITH(NOLOCK)WHERE ProductID = 897;SELECT SalesOrderDetailID, OrderQtyFROM Sales.SalesOrderDetail WITH(NOLOCK)WHERE ProductID = 945;SELECT SalesOrderDetailID, OrderQtyFROM Sales.SalesOrderDetail WITH(NOLOCK)WHERE ProductID = 870;

分别的执行计划:

从这个执行计划对比来看,ProductID为945和897的两条语句执行计划一致,因为满足条件的记录数非常少,分别为257条和2条,所以SQL Server均选择走最优执行计划Index Seek + Key Lookup。但是与ProductID为870的查询语句执行计划完全不同,这条语句SQL Server选择走的是Clustered Index Scan,几乎等价于Table Scan的性能消耗。这是因为,SQL Server认为满足条件ProductID = 870的记录数太多,达到了4688条记录,与其走Index Seek + Key Lookup,还不如走Clustered Index Scan顺序IO的效率高。从这里可以看出,SQL Server会因为传入参数值的不同而选择走不同的执行计划,执行效率也大不相同。确切的说,这个就是属于查询语句的参数嗅探问题范畴。

存储过程的参数嗅探表象

上一小节,我们看了查询语句的参数嗅探表象,这一小节我们来看看存储过程参数嗅探的表象又是如何的呢?

首先,我们创建如下存储过程:

USE AdventureWorks2008R2GOCREATE PROCEDURE UP_GetOrderIDAndOrderQtyByProductID(

@ProductID INT)ASBEGIN

SET NOCOUNT ON

SELECT

SalesOrderDetailID

, OrderQty FROM Sales.SalesOrderDetail WITH(NOLOCK) WHERE ProductID = @ProductID;ENDGO

接下来,我们执行两次这个存储过程,传入不同的参数:

EXEC dbo.UP_GetOrderIDAndOrderQtyByProductID 870EXEC dbo.UP_GetOrderIDAndOrderQtyByProductID 945

从这个执行计划来看,ProductID为870和945走的相同的执行计划Clustered Index Scan,这个和上一小节得到的结果是不一样的。上一节中ProductID = 945的查询语句执行计划走的是Index Seek + Key Lookup。

当我们选择第二个执行计划的Clustered Index Scan的时候,我们观察Properties中的Estimated Number of Rows,这里显示的是4668,但实际上正确得行数应该是257。如下如所示:

这到底是为什么呢?从另外一个角度来讲,这个不正确的统计估值甚至会导致SQL Server走到一个不是最优的执行计划上来(根据上一小节,ProductID = 945的最优执行计划其实是Index Seek + Key Lookup)。

答案其实就是存储过程的参数嗅探问题。这是因为,我们在首次执行这个存储过程的时候,传入的参数ProductID = 870对应的订单总数为4668,SQL Server在编译,缓存执行计划的时候,连同这个值一起记录到执行计划缓存中了。从而影响到存储过程的第二次及以后的执行计划方案,进而影响到存储过程的执行效率。

我们可以通过如下方法来查看执行计划中传入参数的值,右键 => Show Execution Plan XML => 搜索 ParameterCompiledValue

在此例中,我们很清楚的发现传入参数值是870,同时也很清楚得看到了参数嗅探对于执行计划的影响:

...

<ColumnReference Column="@ProductID" ParameterCompiledValue="(870)" ParameterRuntimeValue="(870)" />

...

至此,我们分别从查询语句和存储过程两个方便看到了参数嗅探的表象。

参数嗅探导致的问题

从参数嗅探的表象这一章节,我们可以对此参数嗅探的问题窥探一二。但是,参数嗅探可能会导致哪些常见的问题呢?根据我们的经验,如果你遭遇了MSSQL Server以下奇怪问题,你可能就遇到参数嗅探这个“大魔头”了。

ALTER PROCEDURE解决性能问题

某些传入参数导致存储过程执行非常缓慢,但是ALTER PROCDURE(所有代码没做任何改动)后,性能恢复正常。这个场景是我们之前经常遇到的,原因是当你ALTER PROCEDURE后,MSSQL Server会主动清除对应的存储过程执行计划缓存,然后再次执行该存储过程的时候,系统会重新编译并缓存该存储过程执行计划。

相同的存储过程,相同的传入参数,执行时快时慢

这个听起来非常奇怪吧,当我们执行相同的存储过程,传入相同的参数值,但是执行效率时快时慢,请注意下面例子中的注释部分。

USE AdventureWorks2008R2

GO

--SQL Server 创建执行计划,优化ProductID = 870对应的大量订单量,运行时间500毫秒

EXEC dbo.UP_GetOrderIDAndOrderQtyByProductID 870--SQL Server直接获取缓存中的执行计划,对于小量订单来说可能不是最好的执行计划,不过没关系,执行时间450毫秒

EXEC dbo.UP_GetOrderIDAndOrderQtyByProductID 945

现在我们清空了执行计划缓存,为了方便,我直接清除所有的执行计划缓存。

DBCC FREEPROCCACHE

再次执行存储过程,这次我们交换了执行顺序,先执行ProductID 945,然后执行ProductID 870。

USE AdventureWorks2008R2

GO

--SQL Server 创建执行计划,优化ProductID = 945,对应于小量订单的最优执行计划,运行时间100毫秒

EXEC dbo.UP_GetOrderIDAndOrderQtyByProductID 945--SQL Server直接获取缓存中的执行计划,对于大量订单的ProductID 870来说,可能是很差的执行计划,执行时间60秒

EXEC dbo.UP_GetOrderIDAndOrderQtyByProductID 870

从这两个批次执行的时间对比来看,ProductID 945和870执行时间有比较大的差异,特别是ProductID = 870。这种相同的存储过程,相同的传入参数,执行时快时慢的问题,也是由于参数嗅探导致的。

注意:这里只是为了描述这种现象,由于表数据量本来不大的原因,可能实际上执行时间可能没有那么大的差异。

查询语句放在存储过程中和单独执行效率差异大

某一个查询语句,放在存储过程中执行和拿出来单独执行,时间消耗差异大,一般情况是拿出来单独执行的语句很快,放到存储过程中执行很慢。这个情况也是我们在产品环境常见的一种典型参数嗅探导致的问题。

参数嗅探的解决方法

上一节,我们探讨了参数嗅探可能会导致的问题。当发现这些问题的时候,我们来看看两类人的不同解决方法。请允许我将这两类人分别命名为菜鸟和老鸟,没有任何歧视,只是一个名字代号而已。

菜鸟的解决方法

菜鸟的理论很简单粗暴,既然参数嗅探是因为查询语句或者存储过程的执行计划缓存导致,那么我只需要清空内存就可以解决这个问题了嘛。嗯,来看看菜鸟很傻很天真的做法吧。

  • 方法一:重启Windows OS。果然很黄很暴力,重启Windows操作系统,彻底清空Windows所有内存内容。

  • 方法二:重启SQL Server Service。稍微温柔一点点啦,重启SQL Server Service,彻底清空SQL Server占用的所有内存,当然执行计划缓存也被清空了。

  • 方法三:DBCC命令清空SQL Server执行计划缓存。又温柔了不少吧,彻底清空了SQL Server所有的执行计划缓存,包含有问题的和没有问题的缓存。

DBCC FREEPROCCACHE

老鸟的解决方法

当菜鸟还在为自己的解决方法解决了参数嗅探问题而沾沾自喜的时候,老鸟的思维已经走得很远很远了,老鸟就是老鸟,是菜鸟所望尘莫及的。老鸟的思维逻辑其实也很简单,既然是某个或者某些查询语句或存储过程的执行计划缓存有问题,那么,我们只需要重新编译缓存这些害群之马就好了。

  • 方法一:创建存储过程使用WITH RECOMPILE

USE AdventureWorks2008R2GOALTER PROCEDURE dbo.UP_GetOrderIDAndOrderQtyByProductID(

@ProductID INT)WITH RECOMPILEASBEGIN

SET NOCOUNT ON

SELECT

SalesOrderDetailID

, OrderQty FROM Sales.SalesOrderDetail WITH(NOLOCK) WHERE ProductID = @ProductID;ENDGO

再重新执行两次存储过程,传入不同的参数值,我们可以看到均走到最优的执行计划上来了,说明参数嗅探的问题已经解决。这个方法带来的一个问题就是每次执行这个存储过程系统都会重新编译,无法使用执行计划缓存。但是相对来说,重新编译的系统开销要远远小于参数嗅探导致的系统性能消耗,所以,两害取其轻。

  • 方法二:查询语句使用Query Hits

如果我们知道ProductID对应的订单总数分布,认为ProductID = 945为最好的执行计划,那么我们可以强制SQL Server按照参数输入945来执行存储过程,我们可以添加Query Hits来实现。这种方法的难点在于对表中数据分布有着精细的认识,可操作性不强,因为表中数据分布是随时在改变的。

USE AdventureWorks2008R2GOALTER PROCEDURE dbo.UP_GetOrderIDAndOrderQtyByProductID(

@ProductID INT)ASBEGIN

SET NOCOUNT ON

SELECT

SalesOrderDetailID

, OrderQty FROM Sales.SalesOrderDetail WITH(NOLOCK) WHERE ProductID = @ProductID --OPTION (RECOMPILE);

OPTION (OPTIMIZE FOR (@ProductID=945));

--OPTION (OPTIMIZE FOR (@ProductID UNKNOWN));ENDGO

  • 方法三:DBCC清除特定语句或存储过程缓存

当清除执行计划缓存后,SQL Server再次执行会重新编译对应语句或者存储过程,以获得最好的执行计划。在此以清除特定存储过程执行计划缓存为例。

USE AdventureWorks2008R2GOdeclare

@plan_id varbinary(64)

;SELECT TOP 1 @plan_id = cache.plan_handleFROM sys.dm_exec_cached_plans cache CROSS APPLY sys.dm_exec_query_plan(cache.plan_handle) AS plaCROSS APPLY sys.dm_exec_sql_text(cache.plan_handle) AS txtWHERE pla.objectid = object_id(N'UP_GetOrderIDAndOrderQtyByProductID','P')and txt.objectid = object_id(N'UP_GetOrderIDAndOrderQtyByProductID','P')

DBCC FREEPROCCACHE (@plan_id); GO

  • 方法四:更新表对象统计信息

表统计信息过时导致执行计划评估不准确,进而影响查询语句执行效率。这个也是导致参数嗅探问题另一个重要原因。这种情况,我们只需要手动更新表统计信息。这个解决方法的难点在于找到有问题的查询语句和对应有问题的表。统计信息更新方法如下,如果发现StatsUpdated时间太过久远就应该是被怀疑的对象:

USE AdventureWorks2008R2GOUPDATE STATISTICS Sales.SalesOrderDetail WITH FULLSCAN;SELECT

name AS index_name

, STATS_DATE(OBJECT_ID, index_id) AS StatsUpdatedFROM sys.indexesWHERE OBJECT_ID = OBJECT_ID('Sales.SalesOrderDetail')GO

  • 方法五:重整表对象索引

另外一个导致执行计划评估不准确的重要原因是索引碎片过高(超过30%),这个也会导致参数嗅探问题的重要原因。这种情况我们需要手动重整索引碎片,方法如下:

USE AdventureWorks2008R2GOselect

DB_NAME = DB_NAME(database_id)

,SCHEMA_NAME = SCHEMA_NAME(schema_id)

,OBJECT_NAME = tb.name

,ix.name

,avg_fragmentation_in_percentfrom sys.dm_db_index_physical_stats(db_id(),object_id('Sales.SalesOrderDetail','U'),NULL,NULL,'LIMITED') AS fra CROSS APPLY sys.indexes AS ix WITH (NOLOCK) INNER JOIN sys.tables as tb WITH(NOLOCK) ON ix.object_id = tb.object_idWHERE ix.object_id = fra.object_id and ix.index_id = fra.index_idUSE AdventureWorks2008R2GOALTER INDEX PK_SalesOrderDetail_SalesOrderID_SalesOrderDetailIDON Sales.SalesOrderDetail REBUILD;

  • 方法六:创建缺失的索引

还有一个重要的导致执行计划评估不准确的因素是表缺失索引,这个也是会导致参数嗅探的问题。查找缺失索引的方法如下:

USE AdventureWorks2008R2

GO

SELECT TOP 10

database_name = db_name(details.database_id)

, schema_name = SCHEMA_NAME(tb.schema_id)

, object_name = tb.name

, avg_estimated_impact = dm_migs.avg_user_impact*(dm_migs.user_seeks+dm_migs.user_scans)

, last_user_seek = dm_migs.last_user_seek

, create_index =

'CREATE INDEX [IX_' + OBJECT_NAME(details.OBJECT_ID,details.database_id) + '_'+ REPLACE(REPLACE(REPLACE(ISNULL(details.equality_columns,''),', ','_'),'[',''),']','')

+ CASE

WHEN details.equality_columns IS NOT NULL

AND details.inequality_columns IS NOT NULL THEN '_'

ELSE ''

END

+ REPLACE(REPLACE(REPLACE(ISNULL(details.inequality_columns,''),', ','_'),'[',''),']','')

+ ']'

+ ' ON ' + details.statement

+ ' (' + ISNULL (details.equality_columns,'')

+ CASE WHEN details.equality_columns IS NOT NULL AND details.inequality_columns

IS NOT NULL THEN ',' ELSE '' END

+ ISNULL (details.inequality_columns, '')

+ ')'

+ ISNULL (' INCLUDE (' + details.included_columns + ')', '')

FROM sys.dm_db_missing_index_groups AS dm_mig WITH(NOLOCK)

INNER JOIN sys.dm_db_missing_index_group_stats AS dm_migs WITH(NOLOCK)

ON dm_migs.group_handle = dm_mig.index_group_handle

INNER JOIN sys.dm_db_missing_index_details AS details WITH(NOLOCK)

ON dm_mig.index_handle = details.index_handle

INNER JOIN sys.tables AS tb WITH(NOLOCK)

ON details.object_id = tb.object_idWHERE details.database_ID = DB_ID()ORDER BY Avg_Estimated_Impact DESC

GO

  • 方法七:使用本地变量

这是一个非常奇怪的解决方法,使用这种方法的原因是,对于本地变量SQL Server使用统计密度来代替统计直方图,它会认为所有的本地变量均拥有相同的统计密度,即对应于相同的记录数。这样可以避免因为数据分布不均匀导致的参数嗅探问题。

USE AdventureWorks2008R2GOALTER PROCEDURE dbo.UP_GetOrderIDAndOrderQtyByProductID(

@ProductID INT)ASBEGIN

SET NOCOUNT ON

DECLARE

@Local_ProductID INT

;

SET

@Local_ProductID = @ProductID

;

SELECT

SalesOrderDetailID

, OrderQty FROM Sales.SalesOrderDetail WITH(NOLOCK) WHERE ProductID = @Local_ProductIDENDGO

至此结束,本节分享了菜鸟和老鸟关于参数嗅探问题的解决方法,我相信大家应该可以轻松的做出正确选择适合自己的解决方法。

补充说明

以上所有代码的测试环境是在MSSQL Server 2008R2 Enterprise中完成。

Microsoft SQL Server 2008 R2 (SP2) - 10.50.4000.0 (X64) Jun 28 2012 08:36:30 Copyright (c) Microsoft Corporation Enterprise Edition (64-bit) on Windows NT 6.1 <X64> (Build 7601: Service Pack 1) (Hypervisor)

相关推荐

方差分析简介(方差分析通俗理解)

介绍方差分析(ANOVA,AnalysisofVariance)是一种广泛使用的统计方法,用于比较两个或多个组之间的均值。单因素方差分析是方差分析的一种变体,旨在检测三个或更多分类组的均值是否存在...

正如404页面所预示,猴子正成为断网元凶--吧嗒吧嗒真好吃

吧嗒吧嗒,绘图:MakiNaro你可以通过加热、冰冻、水淹、模塑、甚至压溃压力来使网络光缆硬化。但用猴子显然是不行的。光缆那新挤压成型的塑料外皮太尼玛诱人了,无法阻挡一场试吃盛宴的举行。印度政府正...

Python数据可视化:箱线图多种库画法

概念箱线图通过数据的四分位数来展示数据的分布情况。例如:数据的中心位置,数据间的离散程度,是否有异常值等。把数据从小到大进行排列并等分成四份,第一分位数(Q1),第二分位数(Q2)和第三分位数(Q3)...

多组独立(完全随机设计)样本秩和检验的SPSS操作教程及结果解读

作者/风仕在上一期,我们已经讲完了两组独立样本秩和检验的SPSS操作教程及结果解读,这期开始讲多组独立样本秩和检验,我们主要从多组独立样本秩和检验介绍、两组独立样本秩和检验使用条件及案例的SPSS操作...

方差分析 in R语言 and Excel(方差分析r语言例题)

今天来写一篇实际中比较实用的分析方法,方差分析。通过方差分析,我们可以确定组别之间的差异是否超出了由于随机因素引起的差异范围。方差分析分为单因素方差分析和多因素方差分析,这一篇先介绍一下单因素方差分析...

可视化:前端数据可视化插件大盘点 图表/图谱/地图/关系图

前端数据可视化插件大盘点图表/图谱/地图/关系图全有在大数据时代,很多时候我们需要在网页中显示数据统计报表,从而能很直观地了解数据的走向,开发人员很多时候需要使用图表来表现一些数据。随着Web技术的...

matplotlib 必知的 15 个图(matplotlib各种图)

施工专题,我已完成20篇,施工系列几乎覆盖Python完整技术栈,目标只总结实践中最实用的东西,直击问题本质,快速帮助读者们入门和进阶:1我的施工计划2数字专题3字符串专题4列表专题5流程控制专题6编...

R ggplot2常用图表绘制指南(ggplot2绘制折线图)

ggplot2是R语言中强大的数据可视化包,基于“图形语法”(GrammarofGraphics),通过分层方式构建图表。以下是常用图表命令的详细指南,涵盖基本语法、常见图表类型及示例,适合...

Python数据可视化:从Pandas基础到Seaborn高级应用

数据可视化是数据分析中不可或缺的一环,它能帮助我们直观理解数据模式和趋势。本文将全面介绍Python中最常用的三种可视化方法。Pandas内置绘图功能Pandas基于Matplotlib提供了简洁的绘...

Python 数据可视化常用命令备忘录

本文提供了一个全面的Python数据可视化备忘单,适用于探索性数据分析(EDA)。该备忘单涵盖了单变量分析、双变量分析、多变量分析、时间序列分析、文本数据分析、可视化定制以及保存与显示等内容。所...

统计图的种类(统计图的种类及特点图片)

统计图是利用几何图形或具体事物的形象和地图等形式来表现社会经济现象数量特征和数量关系的图形。以下是几种常见的统计图类型及其适用场景:1.条形图(BarChart)条形图是用矩形条的高度或长度来表示...

实测,大模型谁更懂数据可视化?(数据可视化和可视化分析的主要模型)

大家好,我是Ai学习的老章看论文时,经常看到漂亮的图表,很多不知道是用什么工具绘制的,或者很想复刻类似图表。实测,大模型LaTeX公式识别,出乎预料前文,我用Kimi、Qwen-3-235B...

通过AI提示词让Deepseek快速生成各种类型的图表制作

在数据分析和可视化领域,图表是传达信息的重要工具。然而,传统图表制作往往需要专业的软件和一定的技术知识。本文将介绍如何通过AI提示词,利用Deepseek快速生成各种类型的图表,包括柱状图、折线图、饼...

数据可视化:解析箱线图(box plot)

箱线图/盒须图(boxplot)是数据分布的图形表示,由五个摘要组成:最小值、第一四分位数(25th百分位数)、中位数、第三四分位数(75th百分位数)和最大值。箱子代表四分位距(IQR)。IQR是...

[seaborn] seaborn学习笔记1-箱形图Boxplot

1箱形图Boxplot(代码下载)Boxplot可能是最常见的图形类型之一。它能够很好表示数据中的分布规律。箱型图方框的末尾显示了上下四分位数。极线显示最高和最低值,不包括异常值。seaborn中...