我的任务是改进SSRS前端和存储过程调用的报告存储过程的性能(,这是我的第一次真实性能调整)目前大约需要30秒才能运行最大量的数据(基于报告前端设置的过滤器)。
此存储过程包含19个在其中执行的查询的细分,其中大部分将数据从基本表内的初始(遗留)格式转换为有意义的数据集,以显示给业务方。
我已经创建了一个基于几个DMV的查询,以便找出哪些是来自存储过程的最耗费资源的查询(下面的小片段),我发现一个查询大约需要10秒,平均,完成。
select
object_name(st.objectid) [Procedure Name]
, dense_rank() over (partition by st.objectid order by qs.last_elapsed_time desc) [rank-execution time]
, dense_rank() over (partition by st.objectid order by qs.last_logical_reads desc) [rank-logical reads]
, dense_rank() over (partition by st.objectid order by qs.last_worker_time desc) [rank-worker (CPU) time]
, dense_rank() over (partition by st.objectid order by qs.last_logical_writes desc) [rank-logical write]
...
from sys.dm_exec_query_stats as qs
cross apply sys.dm_exec_sql_text (qs.sql_handle) as st
cross apply sys.dm_exec_text_query_plan (qs.plan_handle, qs.statement_start_offset, qs.statement_end_offset) as qp
where st.objectid in ( object_id('SuperDooperReportingProcedure') )
, [rank-execution time]
, [rank-logical reads]
, [rank-worker (CPU) time]
, [rank-logical write] desc
现在,这个查询有点奇怪,因为执行计划显示在将数据插入本地临时表时完成大部分工作(~80%),而不是在查询其他表时从中获取源数据然后进行操作。 (下面的截图来自SQL Sentry Plan Explorer)
此外,就行估计而言,执行计划还没有对此进行估计,因为在本地临时表中只插入了4218行而不是执行计划认为其移动的~248k行进入本地临时表。所以,由于这个原因,我正在考虑“统计”,但是如果~80%的工作是实际插入表中,那么这些甚至是否仍然重要?
我的第一个建议之一是重写整个过程和存储过程,以便不包括将数据移动和转换到报告存储过程中,并且每晚将数据转换到一些持久表中(真实的不需要时间数据,只有相关数据直到前一天结束)。但是业务方不希望投入时间和资源来重新设计这个,而是“建议”我会在找到可以添加的位置和索引的意义上进行性能调整,以加快速度。
我不相信向基表添加索引会提高报告的性能,因为运行查询所需的大部分时间都是将数据保存到临时表中(据我所知,它会命中tempdb,意味着它们将被写入磁盘 - >由于I / O延迟而增加的时间。
但是,即便如此,正如我所说,这是我的第一次性能调整任务,我在过去几天尽可能多地阅读与此相关的内容,这些是我目前的结论,但是我我想向更广泛的受众寻求建议,并希望获得更多的见解和理解我可以采取哪些措施来改进这一程序。
作为一些明确的问题,如果可以回答,我会很感激:
非常感谢任何帮助,如果您需要更多详细信息,我将很乐意发布。
更新(2016年8月2日):
有问题的查询是(部分)在下面。缺少的是GROUP BY
部分中的一些聚合列及其对应的行:
select
b.ProgramName
,b.Region
,case when b.AM IS null and b.ProgramName IS not null
then 'Unassigned'
else b.AM
end as AM
,rtrim(ltrim(b.Store)) Store
,trd.Store_ID
,b.appliesToPeriod
,isnull(trd.countLeadActual,0) as Actual
,isnull(sum(case when b.budgetType = 0 and b.budgetMonth between @start_date and @end_date then b.budgetValue else 0 end),0) as Budget
,isnull(sum(case when b.budgetType = 0 and b.budgetMonth between @start_date and @end_date and (trd.considerMe = -1 or b.StoreID < 0) then b.budgetValue else 0 end),0) as CleanBudget
...
into #SalvesVsBudgets
from #StoresBudgets b
left join #temp_report_data trd on trd.store_ID = b.StoreID and trd.newSourceID = b.ProgramID
where (b.StoreDivision is not null or (b.StoreDivision is null and b.ProgramName = 'NewProgram'))
group by
b.ProgramName
,b.Region
,case when b.AM IS null and b.ProgramName IS not null
then 'Unassigned'
else b.AM
end
,rtrim(ltrim(b.Store))
,trd.Store_ID
,b.appliesToPeriod
,isnull(trd.countLeadActual,0)
我不确定这是否真的有用,但由于@kcung要求,我添加了信息。
另外,回答他的一些问题:
更新(2016年8月3日):
我已经尝试了@kcung的建议来从聚合生成查询中移动CASE
语句,不幸的是,总体而言,程序时间没有明显改善,因为它仍然在±0.25到±的范围内波动1.0秒(是的,比原始版本的存储过程更低和更高的时间 - 但我猜这是由于我的机器上的工作负载变化)。
同一查询的执行计划,但经过修改以删除CASE
条件,只留下SUM
聚合,现在是:
答案 0 :(得分:0)
我有机会看到查询吗?和两个表上的索引? 你的公羊有多大?每张表中的行(大致)有多大? 您可以更新两个表的统计信息并重新发送查询计划程序吗?
回答你的问题:
修改强> 只是想指出第2点,我没有意识到你的源表也是临时表。连接的每个会话结束后,临时表将被销毁。向临时表添加索引意味着每次创建此临时表时都会增加额外的时间来创建此索引。
修改强> 对不起,我现在正在使用手机。我只是要做空。 所以基本上有两件事:
在临时表创建时添加主键,这样您就可以一次性完成。不要费心添加非聚集索引或任何覆盖索引,最终会花更多时间创建它们。
查看您的查询,所有case语句,而不是在此查询中执行此操作,为什么不将它们添加为表中的另一列。基本上你想在进行分组时避免计算。您可以将sum()保留在查询中,因为它是一个聚合查询,但请尝试尽可能减少运行时计算。
示例:
case when b.AM IS null and b.ProgramName IS not null
then 'Unassigned'
else b.AM
end as AM
创建表b时,可以创建名为AM的列。 那些rtrim和ltrim。请删除它们并将其粘贴在表创建时间。 :)
答案 1 :(得分:0)