前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >参数化(四):处理非均匀数据分布

参数化(四):处理非均匀数据分布

作者头像
用户1217611
发布于 2018-01-30 09:15:43
发布于 2018-01-30 09:15:43
99500
代码可运行
举报
文章被收录于专栏:文渊之博文渊之博
运行总次数:0
代码可运行

    前面我们了解了参数嗅探可能是好的也可能是坏的。当数列的分布不均匀的时候参数嗅探就是不好的事情。例如,考虑“Status”列在Orders表中有总共10M行。该列有7个不同的值,如下分布:

Status

Number of Rows

Open

314

Pending Approval

561

Approved

28,990

Paid

17,610

Shipped

817,197

Closed

7,922,834

Cancelled

1,032,886

    如果查询status是“Open”的数据时使用参数嗅探,那么优化器很可能选择一个带有index seek 和 key lookup的执行计划。这个计划放在缓存中便于重用。当其他用户执行查询closed状态的时候,相同的执行计划被重用,这就很可能是一个灾难,因为现在将进行8M个键值查找操作。

    另外的使用参数嗅探的糟糕情况是用非相等的谓词使用参数。请看下面的查询:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
SELECT
	Id ,
	CustomerId ,
	TransactionDateTime ,
	StatusId
FROM
	Billing.Transactions
WHERE
	TransactionDateTime BETWEEN @FromDateTime AND @ToDateTime
ORDER BY
	TransactionDateTime ASC;

     如果查询使用参数嗅探编译,使用值“2014-07-01″ 和“2014-08-01″,那么优化器基于统计估计行数并且大概估计行数为20000。然后创建基于这个估计行数的计划并且放在缓存中。后来的执行可以使用完全不同的参数。例如,用户执行查询用时间参数“2012-01-01″ 和“2014-01-01″。结果集大概有61000行,但是基于之前的行数的计划被重用,并且很可能不是一个好的执行计划。

    那么,我们能做些什么来影响参数嗅探?

    我将展示一些基于我之前使用存储过程实例的技术:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
CREATE PROCEDURE
	Marketing.usp_CustomersByCountry
(
	@Country AS NCHAR(2)
)
AS

SELECT
	Id ,
	Name ,
	LastPurchaseDate
FROM
	Marketing.Customers
WHERE
	Country = @Country;
GO

这里是一个“Country”列的分布情况:

Country

Number of Rows

BE

70

CL

55

CN

29,956

DK

74

EG

64

IL

72

MT

83

PT

75

TR

63

UK

28,888

US

40,101

VE

78

     正如所见,一共12个不同的值,其中三个是较多的行数,然而其余的行数非常少。这是一个极端的分配不均匀情况没,生产环境中可能很难看到。这里恰好可以展示我的观点…

     在讨论可行的解决方案之前,先看一下问题…

     首先参数赋值为IL。当存储过程首次用“IL”参数执行时,生成计划包含了一个寻找“Country”的索引。对于这个指定的执行这是很有帮助的优化器估计行数是72,完全准确。

     下次存储过程执行时,使用参数为“US”。数据中有40,101行,并且这种情况下的最佳执行计划是使用聚集索引扫描,可以避免很多“key lookups”。但是计划已经在内存中,就会重用。不幸的是,这个计划包含了索引查找和“key lookup ”而不是聚集索引扫描,这就是一个非常差的执行计划。此时我们看到索引查找操作符的属性中估计行数是72,然后实际却是40000+。这就是执行计划错误引起的估计行数错误。如果我们查看SELECT 的“Parameter List” 属性,就能发现原因所在。由于编译1是“IL”,而运行时是“US”。

    那么现在我们发现了问题,接下来让我们看一下可能的解决方案… Solution #1 – sys.sp_recompile

    很简单就是使用系统存储过程sys.sp_recompile从缓存中移除指定的执行计划或者所有计划引用的指定表和视图。这就是说下次存储过程再次执行时需要重新编译,新的执行计划将被创建。

    记住我们的主要问题是值的分布。因此基于一套新的参数重新编译存储过程将创建指定的执行计划,但是大多数时候这并不解决问题,因为新的计划仍然只针对本次的值是好的,当遇到其他不同分布的参数值时依然是不好的计划。我建议当查询中过滤的值绝大多数情况下是惟一值的时候可以考虑重新编译的方式来解决问题,比如当where后面的status 状态为1的占据99%的数据值时,一般情况就是好的计划。

Solution #2 – WITH RECOMPILE

如果你不喜欢前面这个赌博式的方法,那么WITH RECOMPILE很适合你。与之前依赖传递给指定执行的参数值不同,这种方式使你可以告诉优化器编译在每一个存储过程中编译计划。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ALTER PROCEDURE
	Marketing.usp_CustomersByCountry
(
	@Country AS NCHAR(2)
)
WITH
	RECOMPILE
AS

SELECT
	Id ,
	Name ,
	LastPurchaseDate
FROM
	Marketing.Customers
WHERE
	Country = @Country;
GO

    每一次参数嗅探被使用时,意味着执行将得到优化器提供的最佳执行计划。既然新的计划每次执行都被创建,那么SQLServer将不会把计划放到缓存中。

这是一个不错的解决方案,因为每次执行存储过程都产生一个最佳的计划,消除了随机赌博式的副作用。但是缺点是每次编译都必须经过昂贵的优化过程。这是需要密集的CPU处理过程。如果系统已经处在PCU高负载并且存储过程频繁执行,那么这种方式是不合适的。另一方面,如果CPU使用率相对较低并且存储过程只是偶尔执行,那么这就是一个带给你最佳的解决方案。

Solution #3 – OPTION (RECOMPILE)

是一个与前者相似的解决方案,但是也有两个重要的不同点。首先,这个查询参数针对有问题的查询语句而不是整个存储过程。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ALTER PROCEDURE
	Marketing.usp_CustomersByCountry
(
	@Country AS NCHAR(2)
)
AS

SELECT
	Id ,
	Name ,
	LastPurchaseDate
FROM
	Marketing.Customers
WHERE
	Country = @Country
OPTION
	(RECOMPILE);
GO
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  只对一个语句的重编译节省了大量的资源。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  其次,“WITH RECOMPILE”发生在编译时,而“OPTION (RECOMPILE)” 发生在运行时。整个例子中运行时执行这个语句时,暂停执行,重新编译该查询,生成新的执行计划。而其他部分则使用计划缓存。运行时编译带来的好处就是使优化器能预先知道所有的运行时值,甚至不需要参数嗅探。优化器知道参数的值,局部变量和环境设置,然后使用这些数据编译查询。多数情况下,运行时编译生成的计划要比编译时生成的计划好很多。

因此,你应该考虑使用“OPTION (RECOMPILE)” 而不是“WITH RECOMPILE”,因为它使用了更少的资源长生了更好的计划。但是要注意这种方式依然是十分占用CPU的。

Solution #4 – OPTIMIZE FOR

    另一查询选项“OPTIMIZE FOR”也可以解决参数嗅探问题。该选项指示优化器使用特定的一套参数而不是实际的参数来编译查询。实际上就是重写参数嗅探。注意,这个选项只有当查询必须被重编译的时候才能被使用。选项本身不会引起重编译。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ALTER PROCEDURE
	Marketing.usp_CustomersByCountry
(
	@Country AS NCHAR(2)
)
AS

SELECT
	Id ,
	Name ,
	LastPurchaseDate
FROM
	Marketing.Customers
WHERE
	Country = @Country
OPTION
	(OPTIMIZE FOR (@Country = N'US'));
GO

     还记得“Sales. Orders”表的情形吗?99%的执行会使用“Pending Approval”作为参数。而不是使用sys.sp_recompile(重编译),综上所述,如果希望下一次执行已然使用这个参数,俺么使用OPTIMIZE FOR 将会是此种情况的更佳选择,并且指示优化器无论实际参数在下一次执行时是什么都使用该参数(如上例中的US)。

     通过使用“OPTIMIZE FOR UNKNOWN”可以禁止参数嗅探。这个选项指示优化器将参数设为位置,实际上就是禁用了参数嗅探。如果存储过程有多个参数,那么你能分别对每一个参数进行选项处理(禁用)。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ALTER PROCEDURE
	Marketing.usp_CustomersByCountry
(	
	@Country AS NCHAR(2)
)
AS

SELECT
	Id ,
	Name ,
	LastPurchaseDate
FROM
	Marketing.Customers
WHERE
	Country = @Country
OPTION
	(OPTIMIZE FOR (@Country UNKNOWN));
GO

Solution #5 – 最佳方案

    到目前为止你可能注意到了,有两个我们希望达到有互相冲突的目的。一个是为每个执行创建最优的计划,另一个是最小化编译避免资源的浪费。“WITH RECOMPILE”方式完成了第一个目的,但是它需要每个执行重新编译。另一方面,sys.sp_recompile方式只重新编译了一次存储过程,但是不会为每个执行产生最佳计划。

    那么最佳的解决方案就是平衡这两种冲突的目标。这种平衡思想就是分离参数值到不同的组,每组有不同的优化计划,并且生成不同的优化计划。每个计划只被编译一次,然后从这点来说每个执行都会得到最佳计划,因为计划基于参数值产生,所以合理的分组导致生成对应组的计划。

    听起来像魔法吗?让我们看一下这个戏法如何实现…

    首先我们需要把值分成不同的组。这是关键部分,并且有许多方式去分组。这里我将使用国家作为参数,将普通国家和非普通国家分成两组。如果该国家的行数占到了表行数的1%以上我将其定义为普通国家。假定SQLServer已经定义了普通国家,通过统计国家列字段。SQLServer 通常使用普通的参数值作为图形统计的条目。

    因此我们将普通国家插入到“CommonCountries”表的“Country”,然后删除非普通国家…

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
CREATE TABLE
	Marketing.CommonCountries
(
	RANGE_HI_KEY		NCHAR(2)	NOT NULL ,
	RANGE_ROWS			INT			NOT NULL ,
	EQ_ROWS				INT			NOT NULL ,
	DISTINCT_RANGE_ROWS	INT			NOT NULL ,
	AVG_RANGE_ROWS		FLOAT		NOT NULL ,

	CONSTRAINT
		pk_CommonCountries_c_RANGEHIKEY
	PRIMARY KEY CLUSTERED
		(RANGE_HI_KEY ASC)
);
GO


INSERT INTO
	Marketing.CommonCountries
(
	RANGE_HI_KEY ,
	RANGE_ROWS ,
	EQ_ROWS ,
	DISTINCT_RANGE_ROWS ,
	AVG_RANGE_ROWS
)
EXECUTE ('DBCC SHOW_STATISTICS (N''Marketing.Customers'' , ix_Customers_nc_nu_Country) WITH HISTOGRAM');
GO


DECLARE
	@RowCount AS INT;

SELECT
	@RowCount = COUNT (*)
FROM
	Marketing.Customers;

DELETE FROM
	Marketing.CommonCountries
WHERE
	EQ_ROWS < @RowCount * 0.01;
GO

表的查询内容如下:

RANGE_HI_KEY

RANGE_ROWS

EQ_ROWS

DISTINCT_RANGE_ROWS

AVG_RANGE_ROWS

CN

0

29956

0

1

UK

0

28888

0

1

US

0

40101

0

1

     这样清楚极了。这三个是普通国家的例子。当然这是比较简单的例子,实际环境可能要复杂的多,有时甚至需要提出一些算法来区分普通和不普通的值。可以使用我这种统计的结果。也可以使用某种监视机制来追踪使用结果和计划。又或者需要开发一套自己的统计机制。无论如何,多数时候是需要开发一个算法来区分值为不同的组。

    那么我们可以用这个国家的分组分别生成优化计划。这种方式需要创建不同存储过程,而存储过程除了名字外几乎都是一样的。

    在实例中,我创建“Marketing.usp_CustomersByCountry_Common”和“Marketing.usp_CustomersByCountry_Uncommon”两个存储过程。如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
CREATE PROCEDURE
	Marketing.usp_CustomersByCountry_Common
(
	@Country AS NCHAR(2)
)
AS

SELECT
	Id ,
	Name ,
	LastPurchaseDate
FROM
	Marketing.Customers
WHERE
	Country = @Country;
GO


CREATE PROCEDURE
	Marketing.usp_CustomersByCountry_Uncommon
(
	@Country AS NCHAR(2)
)
AS

SELECT
	Id ,
	Name ,
	LastPurchaseDate
FROM
	Marketing.Customers
WHERE
	Country = @Country;
GO

    接下来我们修改一个原始的存储过程,这个存储过程变成一个路由。它的工作就是价差参数值并根据值的分组确定执行哪一个对应的存储过程。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ALTER PROCEDURE
	Marketing.usp_CustomersByCountry
(
	@Country AS NCHAR(2)
)
AS

IF
	EXISTS
		(
			SELECT
				NULL
			FROM
				Marketing.CommonCountries
			WHERE
				RANGE_HI_KEY = @Country
		)
BEGIN

	EXECUTE Marketing.usp_CustomersByCountry_Common
		@Country = @Country;

END
ELSE
BEGIN

	EXECUTE Marketing.usp_CustomersByCountry_Uncommon
		@Country = @Country;

END;
GO

这是一个漂亮的解决方案:

    首次普通国家作为参数使用,路由存储过程调用普通存储过程。一旦第一次被执行以后,计划被生产在缓存中。多亏了参数嗅探,从此以后,只要普通国家的存储过程被执行都会使用这个计划。然后,同样不常用国家也是如此…

    因此,我们为每个参数值都提供了优秀的计划,并且每个计划只被编译一次。通常来书只有2到3组值,因此最多2到3个编译。这就是魔法的实质。

缺点:

   当然这只是一个理想的方式,需要注意的是该方案的维护成本。一旦数据发生了改变,算法必须去维护修改来再次适应。如上面的例子,需要每一段时间去重新创建普通国家的表。

总结:

    参数嗅探能是好的也可以是坏的事情。既然在SQLServer中默认使用,只要它是好的,我们就应该使用。我们的目的是根据不同场景识别参数嗅探,然后应用文中提到的方式来解决不好的参数嗅探问题。

    今后我会选择一些具体生产问题来展示一下各种参数嗅探以及相应的衍生问题的处理方案。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2016-06-01 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
参数化(二):执行查询的方式
前面一篇我介绍了执行计划缓存以及执行之前批处理经过的流程。这篇将用几个最普通的例子介绍查询的几种执行方式。 请看下面这个我使用的这个查询: SELECT Id , Name , LastPurchaseDate FROM Marketing.Customers WHERE Country = N'IL';      这是一个简单的检索指定国家的顾客的查询。现在我们来测试前面这个查询,并且展示七个不同的查询方式。同时介绍执行方法对计划缓存和计划重用的影响。      为了检测影响,我们使用下面的视图
用户1217611
2018/01/30
1.2K0
参数化(三):参数嗅探
    在之前的随笔中我提到过参数嗅探,这是非常重要的概念。下面我们深入的研究一下参数嗅探…     首先我们知道批处理可以是参数化的或者非参数化。参数化的批处理计划有两种类型:“Prepared” 或者“Proc”。前者对应带有至少一个参数的sys.sp_executesql的执行,并且从T-SQL批处理,或者应用程序通过ADO.NET等直接被执行的。后者的执行计划对应一个存储过程。     参数嗅探在这两种类型中是完全相同的。它的行为在两种计划中是完全一样的。因此我们这里不去讨论类型,只关心参数化批处理
用户1217611
2018/01/30
2.2K0
参数化(三):参数嗅探
参数化(三):参数嗅探
在之前的随笔中我提到过参数嗅探,这是非常重要的概念。下面我们深入的研究一下参数嗅探…
全栈程序员站长
2021/11/29
2K0
参数化(三):参数嗅探
存储过程详解
存储过程简介 什么是存储过程:存储过程可以说是一个记录集吧,它是由一些T-SQL语句组成的代码块,这些T-SQL语句代码像一个方法一样实现一些功能(对单表或多表的增删改查),然后再给这个代码块取一个名字,在用到这个功能的时候调用他就行了。 存储过程的好处: 1.由于数据库执行动作时,是先编译后执行的。然而存储过程是一个编译过的代码块,所以执行效率要比T-SQL语句高。 2.一个存储过程在程序在网络中交互时可以替代大堆的T-SQL语句,所以也能降低网络的通信量,提高通信速率。 3.通过存储过程能够使没
郑小超.
2018/01/24
2.3K0
SQL Server 执行计划缓存
概述 了解执行计划对数据库性能分析很重要,其中涉及到了语句性能分析与存储,这也是写这篇文章的目的,在了解执行计划之前先要了解一些基础知识,所以文章前面会讲一些概念,学起来会比较枯燥,但是这些基础知识非常重要。 目录 概述 基础概念 怎样缓存执行计划 SQL Server自动删除执行计划 重新编译执行计划 测试 执行计划相关系统视图 手动清空缓存执行计划 测试索引更改对执行计划的影响 测试增加字段对执行计划的影响 总结 基础概念 SQL Server 有一个用于存储执行计划和数据缓冲区
逸鹏
2018/04/11
2K0
SQL Server 执行计划缓存
MySQL进阶三板斧(二)揭开“存储过程”的神秘面纱
如下是一条SELECT语句从student表(该表测试数据在实际应用模块)中返回的所有行:
陈哈哈
2020/07/06
1K0
性能调优之CPU
关系型数据库严重依赖底层的硬件资源,CPU是服务器的大脑,当CPU开销很高时,内存和硬盘系统都会产生不必需要的压力。CPU的性能问题,直观来看,就是任务管理器中看到的CPU利用率始终处于100%,而侦测CPU压力的工具,最精确的就是性能监控器。
物流IT圈
2019/07/16
1.3K0
性能调优之CPU
存储过程
什么是存储过程:存储过程可以说是一个记录集吧,它是由一些T-SQL语句组成的代码块,这些T-SQL语句代码像一个方法一样实现一些功能(对单表或多表的增删改查),然后再给这个代码块取一个名字,在用到这个功能的时候调用他就行了。
用户2038009
2021/03/07
2.2K0
50多条实用mysql数据库优化建议
1.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。 缺省情况下建立的索引是非群集索引,但有时它并不是最佳的。在非群集索引下,数据在物理上随机存放在数据页上。合理的索引设计要建立在对各种查询的分析和预测上。一般来说: a.有大量重复值、且经常有范围查询( > ,< ,> =,< =)和 order by、group by 发生的列,可考虑建立集群索引; b.经常同时存取多列,且每列都含有重复值可考虑建立组合索引, 选择度高的列建议作为索引的第一个字
机器学习AI算法工程
2018/03/14
4.1K1
50多条实用mysql数据库优化建议
MySQL查询优化
通过上述参数可以了解当前DB应用是插入更新为主还是查询为主,以及各类的SQL执行比例。
JavaEdge
2021/10/18
1.9K0
Oracle-Soft Parse/Hard Parse/Soft Soft Parse解读
在Oracle中存在两种类型的SQL语句: 一类为 DDL语句(数据定义语言)CREATE,DROP,ALTER,他们是从来不会共享使用的,也就是每次执行都需要进行硬解析。 一类就是DML语句(数据操纵语言)INSERT,UPDATE,DELETE,SELECT,他们会根据情况选择要么进行硬解析,要么进行软解析。
小小工匠
2021/08/16
2.4K0
SQL参数化查询:防注入与计划缓存的双重优势
SQL 注入攻击和性能瓶颈是开发者常面临的挑战。参数化查询作为一种高效解决方案,不仅能从根本上防御注入攻击,还能优化数据库执行效率。
Jimaks
2025/06/16
1480
SQL参数化查询:防注入与计划缓存的双重优势
数据库优化方案之SQL脚本优化
随着数据库数据越来越大,数据单表存在的数据量也就随之上去了,那么怎么样让我们的脚本查询数据更快呢?
用户1112962
2019/11/15
1.5K0
MySQL 高效查询的实践指南:SQL语句优化篇
在 MySQL 数据库中,编写高效的 SQL 语句不仅能提升查询速度,还能优化资源使用,避免潜在的性能问题。本文将详细介绍 MySQL 查询中的一些强制性和推荐性规范,并提供具体的示例来帮助你更好地理解和应用这些最佳实践。
千羽
2024/07/31
3000
MySQL 高效查询的实践指南:SQL语句优化篇
MYSQL 下 count(*)、count(列)、 count(1) 理解
cout(*)和count(1) 没区别,但是cout(列名) 不统计为 null 的
王小明_HIT
2019/08/29
2.7K0
MySql 全方位基础优化定位执行效率低的SQL语句存储过程与触发器的区别面试回答数据库优化问题从以下几个层面入手
SQL优化 通过show status命令了解各种sql的执行效率 查看本session的sql执行效率 show status like 'Com_%'; 查看全局的统计结果 SHOW GLOBAL STATUS LIKE 'Com_%' 查看服务器的状态 show global status; 结果 Com_select:执行select操作的次数,依次查询之累加1 Com_insert:执行insert操作的次数,对于批量插入的insert操作,只累加依次 Com_update:执行update操作
JavaEdge
2018/05/16
2.3K0
大厂都在用的MySQL优化方案
上面的参数是对所有存储引擎的表进行累计,下面参数是针对InnoDB存储引擎的,累加算法略有不同
JavaEdge
2022/11/30
5440
大厂都在用的MySQL优化方案
3. SQL -- 存储过程
在Sql Server中,可以定义子程序存放在数据库中,这样的子程序称为存储过程,它是数据库对象之一.
py3study
2020/01/14
1.1K0
T-SQL性能调整(一)--编译和重新编译
概念简介 我们平时所说的查询在SQLServer 中主要有两部分来实现: 编译查询,主要包括了五个环节(缓存查找、分析、代数化、优化、缓存新计划)         流程描述: 首先,在计划缓存中查找是
用户1217611
2018/01/30
1K0
T-SQL性能调整(一)--编译和重新编译
存储过程性能分析有哪些专用工具
hide
2025/04/30
1560
相关推荐
参数化(二):执行查询的方式
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验