记一次真实SQL性能调优(调优思路及数据偏移实现)

记一次真实SQL性能调优(调优思路及数据偏移实现)

近日,接到一则报表性能优化的需求,起因是客户用了五年的报表,最近不好使了。预览报表报错:超时时间已到,可把客户给急得。


乍一看报表样式,浓浓的“中国式报表”风(还是老版的报表设计器),表格列是各公司类型下的工单类型,共25列;表格行是各项目下的各项指标,最多20*14行,从整体记录数来看,行数并不多,怎么会超时呢?


打开SQL Server Profilers跟踪器,捕捉到该报表是通过存储过程实现的,存储过程运行30秒,前台便报超时,说明超时时间为30秒(翻了下产品文档,超时时间还不给调)。


在SSMS中单独执行存储过程,40+秒,还没走到前端渲染这步就妥妥超时了。


翻开存储过程的代码扫望一遍,引用的表并不多,更多是计算,先上常规优化手段:重建索引+更新统计信息+表收缩(业务繁忙时切忌全表优化,需要限定下表名),结果依旧。

--重建索引 更新统计信息 表收缩
DECLARE @DBCCString NVARCHAR(1000)
DECLARE @DBCCString1 NVARCHAR(1000)
DECLARE @DBCCString2 NVARCHAR(1000)
DECLARE @TableName VARCHAR(150)
declare @n int
declare @cu int
select @n= (select count(1) from  sysobjects WHERE xType='U' and (name like 'T_%')  and name not like 'tmp%')
select @cu=1
DECLARE Cur_Index CURSOR FOR
SELECT Name AS TblName FROM sysobjects WHERE xType='U'  and (name like 'T_%') and name not like 'tmp%' ORDER BY TblName
FOR READ ONLY
OPEN Cur_Index
FETCH NEXT FROM Cur_Index INTO @TableName
WHILE @@FETCH_STATUS=0
BEGIN
  SET @DBCCString ='alter index all on  '+@TableName+ ' rebuild;'
  EXEC (@DBCCString)   
  SET @DBCCString1 =  'UPDATE STATISTICS '+@TableName+ ';'
  EXEC (@DBCCString1)
  SET @DBCCString2 =  'ALTER TABLE '+@TableName+ ' rebuild WITH (DATA_COMPRESSION =ROW);'
  EXEC (@DBCCString2)
  PRINT '第' + CONVERT(varchar (12), @cu)+ ' /共 ' +CONVERT(varchar (12), @n) +  '  表: ' + @TableName +' 重建索引、更新统计信息、表收缩完成!'
  FETCH NEXT FROM Cur_Index INTO @TableName
  select @cu=@cu+1
CLOSE Cur_Index
DEALLOCATE Cur_Index
PRINT '操作完成!'


继续上武器,打开MSSQL的执行计划,找开销占比最高的查询及节点,发现几个计算关闭率的节点查询开销占比高,而且还是实打实的开销,打开查询计划建议的“缺少索引”建议,发发现是把所有用上的字段拼接的组合索引,凭经验看,这类索引一般没效果,果然不行。


接下来,只能真枪实弹,分析原始代码逻辑,直奔 开销占比最高 的SQL代码块分析,发现疑点:怎么比例用除法做计算,写得那么复杂;进一步分析,原来是其他指标已经实现的汇总计算,却为了计算比例,再次进行SUM汇总,可谓事倍功半。


不过事出必有因,我们常用的相除都是同一行的字段间的“列”相除,想要做到“中国式报表”的“行”相除,还真的不好整(SQL sever2012以上版本,可以利用 LAG函数 ,但是也涉及到函数的重复调用;编程语言中,如Python中的 SHIFT函数 实现数据上下偏移),只好搞点骚操作: 先排个序标个号,在通过排号关联实现偏移(文末有演示数据和实现SQL) ,把被除数和除数放在同一行之后再进行相除(记得除以零的兼容处理)。


把比例计算好后,构造好相应的行数据,再通过UNION ALL“放”回原数据集中,构造完成。执行新的存储过程,15秒+,美滋滋。


当然,改造归改造,保证结果准确性也是必须滴,借助EXCEL的条件筛选,新旧结果集比对一目了然。参考:


至此,大功告成。从40秒到15秒,提速 167% ,靠的是一步步顺藤摸瓜。最后,总结下优化点和知识点。

优化点: 避免重复计算,对计算结果再运算时,保证在已经“浓缩”的结果集上再运算,减少运算量。

知识点: 排查SQL性能问题的思路(化整为零,步步为营),数据上下偏移的SQL实现


附:数据上下偏移的SQL实现(SQL Server)


原始数据集:

指标 主要任务量 次要任务量
本期关闭 8 9
本期新增 10 12
累计关闭 96 97
累计新增 100 200


结果数据集:

指标 主要任务量 次要任务量
本期关闭 8 9
本期新增 10 12
本期关闭率(%) 80 75
累计关闭 96 97
累计关闭 100 200
累计关闭率(%) 96 48


SQL实现:

--1 原始数据集构造
SELECT * 
INTO #TEMP_SHOW_DATA
FROM (
SELECT '本期关闭' 指标, 8 主要任务量, 9 次要任务量
UNION ALL
SELECT '本期新增', 10, 12
UNION ALL
SELECT '累计关闭', 96, 97
UNION ALL
SELECT '累计新增', 100, 200)
--2 自定义排序,添加序号
SELECT  ROW_NUMBER() OVER (ORDER BY 主要任务量 ASC) AS 序号,
        主要任务量,
	次要任务量
INTO	#TEMP_SHOW_DATA_SEQ
FROM    #TEMP_SHOW_DATA;
--3 通过关联实现上下偏移,并计算指标
SELECT  CASE WHEN A.指标 = '本期关闭' THEN '本期关闭率(%)' WHEN A.指标 = '累计关闭' THEN '累计关闭率(%)' END AS 指标,
        CASE WHEN B.主要任务量 = 0 THEN 0 ELSE A.主要任务量 * 100 / B.主要任务量 END AS 主要任务量,
		CASE WHEN B.次要任务量 = 0 THEN 0 ELSE A.次要任务量 * 100 / B.次要任务量 END AS 次要任务量
INTO	#TEMP_SHOW_DATA_RATE
FROM    #TEMP_SHOW_DATA_SEQ A
JOIN #TEMP_SHOW_DATA_SEQ B ON A.序号 = B.序号 - 1
WHERE   B.序号 IN ( 2, 4 );
--4 拼接获得结果集
SELECT * FROM (
	SELECT 指标,
		   主要任务量,
		   次要任务量
	FROM #TEMP_SHOW_DATA
	UNION ALL