在本文中,麦老师将给大家介绍如何调优SQL Server的代理作业JOB,并结合实际生产案例将一个运行时间从长达2天的作业调优缩短至令人欣喜的2小时。
本文所使用的调优方法论基本可以通用于其它SQL Server的数据库系统,该套方法论是麦老师经过好几个项目的实战案例总结所得。
闻道有先后,术业有专攻。善语结善缘,恶语伤人心。本文可能有不对之处,欢迎批评指正,欢迎转发评论。
SQL Server版本:2012
首先通过日志文件查看器,简单获取JOB的运行情况,可以发现如下几个问题:
1、该JOB共131个step,其实就是131个存储过程
2、历史日志中,总运行时间从1天到2天不等
3、1月30日运行了12个小时还未跑完,我开始介入进行调优
可以通过如下的SQL语句,查询出JOB中哪个步骤最耗费时间:
SELECT sj.name AS [Name],
sh.step_id,
sh.step_name AS [StepName],
DATETIMEFROMPARTS(
LEFT(padded_run_date, 4), -- year
SUBSTRING(padded_run_date, 5, 2), -- month
RIGHT(padded_run_date, 2), -- day
LEFT(padded_run_time, 2), -- hour
SUBSTRING(padded_run_time, 3, 2), -- minute
RIGHT(padded_run_time, 2), -- second
0) AS [LastRunDateTime], -- millisecond
CASE
WHEN sh.run_duration > 235959
THEN CAST((CAST(LEFT(CAST(sh.run_duration AS VARCHAR), LEN(CAST(sh.run_duration AS VARCHAR)) - 4) AS INT) / 24) AS VARCHAR) + '.' + RIGHT('00' + CAST(CAST(LEFT(CAST(sh.run_duration AS VARCHAR), LEN(CAST(sh.run_duration AS VARCHAR)) - 4) AS INT) % 24 AS VARCHAR), 2) + ':' + STUFF(CAST(RIGHT(CAST(sh.run_duration AS VARCHAR), 4) AS VARCHAR(6)), 3, 0, ':')
ELSE STUFF(STUFF(RIGHT(REPLICATE('0', 6) + CAST(sh.run_duration AS VARCHAR(6)), 6), 3, 0, ':'), 6, 0, ':')
END AS [LastRunDuration (d.HH:MM:SS)],
sh.run_status,
sh.message,
sh.server,
jstep.command
FROM msdb.dbo.sysjobs sj
INNER JOIN msdb.dbo.sysjobhistory sh
ON sj.job_id = sh.job_id
left JOIN [msdb].[dbo].[sysjobsteps] AS [jstep]
ON sj.job_id = jstep.job_id and sh.step_id=jstep.step_id
CROSS APPLY (
SELECT RIGHT('000000' + CAST(sh.run_time AS VARCHAR(6)), 6),
RIGHT('00000000' + CAST(sh.run_date AS VARCHAR(8)), 8)
) AS shp(padded_run_time, padded_run_date)
where name='ACT_JOB'
and sh.run_duration>='6000' -- 执行时长大于60分钟
-- and instance_id>=557203
order by instance_id desc
GO
结果:
从结果可以看出,第3、7、8、13、14、14、17、128这几个存储过程最耗费时间,其中以3和128最耗费时间,尤其是128存储过程需要1天9小时。后边重点只跟踪调优这2个存储过程即可。
在这里,存储过程名称如何获取呢???就是麦老师给的SQL语句中的 jstep.command列或StepName列就可以获取到。
整个调优过程,可能涉及3个大的环节:
A、数据库层面整体调优,包括内存、CPU调整;索引碎片重建;创建missing的索引
B、具体的存储过程的SQL级别的跟踪和调优
C、继续创建missing的索引
D、观察性能是否稳定
这个步骤先不分析具体的SQL语句,因为SQL实在太多,我们先做数据库整体的调优。
先进行数据库配置方面的调优,尤其是内存和并发,可以根据实际情况进行调整,如下:
image-20240201104350891
可以看出,内存和CPU都不是瓶颈。
根据麦老师的经验,若SQL Server的SQL性能渐渐慢下来的话,很大程度上跟大表的索引碎片严重有关系,很多大表的索引碎片会达到90%以上,所以,必须重建。
但是,这类JOB慢,涉及的表很多,作为DBA只能从数据库整体层面来进行索引的重建,我们可以使用如下脚本查询当前数据库中碎片率大于30%的所有索引,若有多个数据库,则需要分别对每个库进行查询,这个脚本执行很慢,可能需要七八个小时甚至十几个小时,不用着急,慢慢跑:
-- 数据库中碎片率大于30%的所有索引
SELECT db_name() dbname,
OBJECT_SCHEMA_NAME(ips.object_id) AS schema_name,
OBJECT_NAME(ips.object_id) AS tb_name,
i.name AS index_name,
i.type_desc AS index_type,
CAST(ips.avg_fragmentation_in_percent AS DECIMAL(5, 2)) avg_fragmentation_in_percent, -- 索引碎片总计百分比,大于30%就得重建
CAST(ips.avg_page_space_used_in_percent AS DECIMAL(5, 2)) avg_page_space_used_in_percent, -- 索引页填充度,小于70%就得重建
ips.index_depth,
ips.page_count,
ips.fragment_count,
ips.alloc_unit_type_desc,
'ALTER INDEX '+ i.name +' ON '+ OBJECT_NAME(ips.object_id) +' REBUILD WITH (MAXDOP=16);' index_rebuild
FROM (
select * from
sys.dm_db_index_physical_stats(db_id(), null, NULL, NULL, NULL ) a
where a.index_level=0 and alloc_unit_type_desc='IN_ROW_DATA'
)AS ips
LEFT JOIN sys.indexes AS i
ON (ips.object_id = i.object_id AND ips.index_id = i.index_id and ips.index_level=0)
where i.name is not null
and ips.avg_fragmentation_in_percent > 30
ORDER BY page_count DESC;
跑出结果后,拷贝index_rebuild列,然后执行SQL重建索引即可,可能又需要好几个小时甚至十几个小时。
相关理论可以参考:https://mp.weixin.qq.com/s/_0AqqTvsZXwPJ2tNrYH5Yw
缺失索引功能是一种轻量工具,用于查找可显著提高查询性能的缺失索引。
我们可以直接使用如下的SQL获取数据库运行过程中需要创建的索引:
-- 缺失索引
select * from (
SELECT TOP 20
CONVERT (decimal (28, 1), migs.avg_total_user_cost * migs.avg_user_impact * (migs.user_seeks + migs.user_scans) ) AS estimated_improvement,
migs.unique_compiles,
migs.user_seeks,
migs.user_scans,
migs.last_user_seek,
migs.last_user_scan,
migs.avg_user_impact,
mid.database_id,
mid.object_id,
mid.equality_columns,
mid.inequality_columns,
mid.included_columns,
mid.statement,
'CREATE INDEX idx_missing_index_' + CONVERT(varchar,mid.database_id) + '_' +
CONVERT(varchar,mid.object_id) + '_' +
CONVERT (varchar, mig.index_group_handle) + '_' +
CONVERT (varchar, mid.index_handle) + ' ON ' +
mid.statement + ' (' + ISNULL (mid.equality_columns, '') +
CASE
WHEN mid.equality_columns IS NOT NULL
AND mid.inequality_columns IS NOT NULL THEN ','
ELSE ''
END + ISNULL (mid.inequality_columns, '') + ')' +
ISNULL (' INCLUDE (' + mid.included_columns + ')', '') +' with (maxdop=32);' AS create_index_statement
FROM sys.dm_db_missing_index_groups mig WITH(NOLOCK)
JOIN sys.dm_db_missing_index_group_stats migs WITH(NOLOCK)
on migs.group_handle = mig.index_group_handle
JOIN sys.dm_db_missing_index_details mid WITH(NOLOCK)
on mig.index_handle = mid.index_handle
where 1=1
and migs.last_user_seek >='2024-01-30 13:40'
-- and migs.user_seeks>2
ORDER BY estimated_improvement DESC
) aa
where estimated_improvement > 100
and avg_user_impact>30
order by database_id,object_id,equality_columns,included_columns
GO
我们可以直接复制create_index_statement列,拷贝到文本编辑器中,将查询出来的索引进行手工的合并,因为有的索引有重叠,该步骤可能需要业务人员进行介入讨论。另外,对于OLTP类型的重要业务库,一定要提交变更才能创建索引,否则最后背锅的都是自己。
修改或合并完成后,把这些缺失的索引都创建上,这个过程也需要很久。
其实,在经过以上1、2和3步骤后,数据库性能应该已经有了显著的提升了,例如,麦老师这个环境:
可以看到,整个job的运行时间先缩短到21小时(碎片重建),再缩短到9小时(创建missing索引)。
step 3从之前的7小时缩短到3小时,step 128从之前的1天9小时缩短到现在的6小时。
但,时间仍然有点长,需要继续进行深入调优。
接下来的调优,因为涉及具体的额存储过程,需要找到存储过程中到底是哪个SQL很慢导致的,所以,需要借助SQL Server Profiler功能进行跟踪,比较费时,使用方法具体可以参考:https://www.xmmup.com/mssqlruhehuoqucunchuguochengzhongzhengzaizhixingdezhenshisqlyuju.html,示例:
另外,开启SQL Server Profiler还不够,还需要做3个事情:
1、拷贝原来的存储过程来创建新的存储过程,可以以debug结尾,例如:sp_aaa 重建为sp_aaa_debug
2、修改sp_aaa_debug存储过程内容,把里边的#修改为##号,好处是,可以在新开的窗口中分析其执行计划(有的场景仍然不能显示)
3、执行存储过程时,若存储过程内容少且无循环语句,则可以选择“包括实际的执行计划”功能,这样,我们可以分析出来存储过程中每一步的执行计划,方便通过执行计划进行调优。
在观察执行计划时,重点关注rid,Key Lookup、开销比较大的步骤等,这些都是我们需要调优的地方。
step 128执行时间历史:
该存储过程由于之前的碎片重建和缺失索引创建,性能已经有所提升,从1天多到10小时,再到5小时。
接下来,继续SQL级别的调优。
在进行setp 128跟踪调优时,发现有一类插入语句很慢,
select * from sql3 where Duration>=6000000;
虽说SQL语句都是插入不同的表中,但是这些SQL语句中都包含了一个共同的表,暂且叫FACT_AA表,查询该表的信息:
可以看到,该表大概 23亿,300GB大小。
找业务人员确认,只需要保留1个月的数据即可,做如下清理操作:
select min(timekey),max(timekey) from FACT_AA
select max(timekey) from FACT_AA where timekey >= getdate()-30
select * into FACT_AA_tmp from FACT_AA where timekey >= getdate()-30
EXEC sp_rename 'FACT_AA', 'FACT_AA_bk';
EXEC sp_rename 'FACT_AA_tmp', 'FACT_AA';
-- SSMS右键获取创建索引的语句,创建索引。。。
修改相关存储过程,并定期进行清理该表。
最终,step 128调优后的总运行时间为30分钟左右:
step 3执行时间历史:
该存储过程由于之前的碎片重建和缺失索引创建,性能也已经有所提升,从7小时到4小时,再到3小时。
接下来,继续SQL级别的调优。
在进行setp 3 跟踪调优时,发现有一个插入语句很慢,查询该进程的等待事件发现是SOS_SCHEDULER_YIELD,查询SQL如下:
-- 某个进程具体等待
SELECT sp.status,sp.cmd,sp.lastwaittype,sp.waitresource,sp.cpu,sp.physical_io,sp.spid,kpid,blocked,dbid
FROM master.dbo.sysprocesses sp WITH(NOLOCK)
where sp.spid=126;
如图所示:
SOS_SCHEDULER_YIELD该等待事件比较有意思,表示一个任务自愿放弃当前的资源占用,让给其他任务使用。 有关该等待事件可以参考:https://www.xmmup.com/sql-serverzhongdedengdaishijiansos_scheduler_yieldshuoming.html
有关该等待事件,麦老师碰到过的最多的情况是:
1、表缺失聚簇索引或执行计划含有rid查找。
2、索引创建的不对。
SQL调优需要仔细分析执行计划,分辨是否含有嵌套循环的操作,或全表扫描的SQL语句,一般在执行计划中都有占比分析。占比比较大的操作就有问题。在OLAP系统中,若有嵌套循环的操作,则多半是因为索引有问题(例如,在同一个表上做Index Seek和Key Lookup操作,然后做NL连接),会导致耗费大量的CPU资源,表现为CPU飙升。
在跑该SQL语句时,系统CPU很高,112核的机器CPU都跑满了:
对该插入语句很复杂,所以我截取了部分执行计划,如下图,
可以看到第一个Index Seek开销为62%,很大,删除该索引后,执行计划变为:
性能瞬间提升,查了一下历史记录,之前该SQL需要跑2小时,目前不到1分钟结束。其实,也可以创建非聚簇的复合include索引来进一步提升性能。
最终,step 3调优后的总运行时间为6分钟:
在经过以上对存储过程的精准调优步骤后,数据库性能再上一个台阶,例如,麦老师这个环境:
从之前的9小时,缩短到现在的3小时。
由于整个调优可能需要好几天,在这几天内,因为SQL Server最多收集 600 个缺失索引组的建议。达到此阈值后,不会收集更多缺少的索引组数据。 所以,我们在调优的最后仍然需要再做1次missing的索引。
这里不再赘述。。。
创建完成后,再观察几天,看看性能是否稳定。
今天开发跟我说,数据库内存需要降低,因为这台环境是集群,涉及到故障转移,所以,只能使用一半的内存,无奈,只能将内存减少100GB。
在调整内存后,发现性能有所反弹。只能继续进行跟踪处理。
发现整个作业比之前慢了1个小时左右:
仔细分析发现有个存储过程127突然慢了很多:
无奈,只能继续跟踪调优,发现了2个地方需要调优:
1、删除数据使用了循环,这里优化的话直接改成直接删除
image-20240204095737317
2、还有一个SQL,使用了Key Lookup,查询很慢,部分执行计划
优化的话,可以使用include的索引,创建完成后,继续查看该SQL的计划:
观察了几天,执行时间还算稳定:
在第二次创建missing的索引后,发现还有一个存储过程8变慢了:
经过跟踪发现,该存储过程有3个问题:
1、如下的sql执行计划有问题:
在创建了相关聚簇覆盖索引后,执行计划变为了:
2、还有一个查询也比较慢,执行计划为:
对于a表有书签扫描,需要进行索引合并。
在删除不需要的索引,然后创建相关聚簇索引后,执行计划变为:
初步看着没啥问题,执行了一下,还行。
3、还有一个表叫FAT_BB的插入和删除都很慢,但是插入的查询语句(insert into xxxx select * from xxxx)很快,在2分钟内
跟踪结果:
该表7300万数据,每次需要删除60万数据,
分析相关执行计划并没有啥问题,但是查看了一下表,发现表上的索引超级多,这些大部分索引都是我在C步骤中创建的,没有经过严格分析:
所以,我又把不需要的索引删除的只保留了聚簇和一个非聚簇的索引后,性能正常。
最终的调优结果,还是比较稳定的:
其实:step 8和step 127的性能反弹,有一部分原因是没有仔细分析之前收集到的missing的索引导致的,要记住,索引并非越多越好,有些索引还是需要进行合并的,但是这个过程其实要和开发做大量的交流。
经过一周时间的调优+跟踪,整个JOB的运行时间从最初的2天变为1天,再到9小时,再到3小时,最后到2小时。
总体来说,若JOB作业运行缓慢,可以从以下几个步骤来优化:
1、数据库整体层面调优(内存、CPU)
2、重建数据库总体层面的索引碎片、并创建missing索引
3、找出慢的存储过程,并创建debug存储过程;将#修改为##号(为了在新窗口查看执行计划);若存储过程内容少且无循环语句,则可以开启“包括实际的执行计划”功能;最后对慢的存储过程进行sql profile跟踪;跟踪过程中进行分析慢SQL的执行计划并进行调优。在观察执行计划时,重点关注rid,Key Lookup、开销比较大的步骤等,这些都是我们需要调优的地方。
4、是否有超大表经常查询,该表是否可以归档清理,只保留近期使用的数据
5、表是否需要创建聚簇索引和非聚簇索引消除RID扫描、Key Lookup等
https://www.xmmup.com/mssqlzuoyejobjieshao.html
https://www.xmmup.com/mssqlruhehuoqucunchuguochengzhongzhengzaizhixingdezhenshisqlyuju.html