【问题标题】:Improve reporting stored procedure execution time - tuning temporary tables?改进报告存储过程执行时间 - 调整临时表?
【发布时间】:2016-08-02 07:35:12
【问题描述】:

我的任务是提高由 SSRS 前端和存储过程调用的报告存储过程的性能(这是我的第一个实际性能调整任务)目前在最大数量的数据上运行大约需要 30 秒(基于从报告前端设置的过滤器)。

此存储过程细分了 19 个在其中执行的查询,其中大部分将数据从基表内部的初始(旧)格式转换为有意义的数据集,以显示给业务方。

我创建了一个基于几个 DMV 的查询,以便找出存储过程中最消耗资源的查询(下面的小 sn-p),我发现一个查询大约需要 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 延迟而增加了时间)。

但是,即便如此,正如我所提到的,这是我的第一个性能调优任务,我在过去几天尝试尽可能多地阅读与此相关的内容,这些是我目前的结论,但我想向更广泛的受众寻求建议,并希望对我可以做些什么来改进这个程序有更多的见解和理解。

如果可以回答一些明确的问题,我将不胜感激:

  • 我上面所说的(根据我对数据库的理解或我的假设)有什么不正确的地方吗?
  • 向临时表添加索引是否真的会增加执行时间,因为该表(及其相关索引在每次执行时都在重建)?
  • 在这种情况下是否可以在无需重新编写过程/查询并且仅通过索引或其他调优方法完成的情况下进行其他操作? (我读过一些文章标题,您也可以“调整 tempdb”,但我还没有深入了解这些细节)。

非常感谢任何帮助,如果您需要更多详细信息,我很乐意发布。

更新(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 要求,我添加了信息。

另外,回答他的一些问题:

  • 临时表上没有索引
  • 内存大小:32 GB

更新(2016 年 8 月 3 日):

我尝试了@kcung 的建议,将CASE 语句从聚合生成查询中移出,不幸的是,总体而言,过程时间并没有明显改善,因为它仍然在±0.25 到±1.0 秒的范围内波动(是的,比存储过程的原始版本更低和更高的时间 - 但我猜这是由于我的机器上的可变工作负载)。

同一个查询的执行计划现在是:

【问题讨论】:

    标签: sql-server performance stored-procedures sql-server-2008-r2


    【解决方案1】:
    1. 向临时表添加索引肯定会改善读取调用,但会减慢对临时表的写入调用。
    2. 这里,正如您所提到的,该过程中有 19 个查询在执行,因此仅用执行计划分析一个查询并没有多大帮助。
    3. 添加更多,如果可能的话,只执行这个查询并检查它需要多少时间(受影响的行)。
    4. 您可以尝试其他方法,不确定是否适合您的情况,尝试使用表变量而不是临时表。这是因为,在临时表上使用表变量具有额外的优势,例如,预编译过程,不维护事务日志。等等,你不需要写 drop table。

    【讨论】:

    • 2.大多数查询是从一个临时表到另一个 (SELECT INTO FROM) 的 SELECT,基于更多的连接/聚合等对数据进行了一些修改。还有一些查询总计约 15更多秒(但那些是 4-5 个查询,而不是 1 个)。因此,我试图在转移到其他查询之前尽可能多地从这个大型查询中节省时间,我认为它们(目前)还无法改进。
    • 3.我认为我无法从 DMV 以外的其他地方获得有关这些查询的更多详细信息。因此,一次添加/测试更多查询不会帮助我找到任何新的东西,因为我已经知道需要多少时间以及谁是我的“罪魁祸首”。
    • 4.我已经尝试过表变量并且执行时间有所改善,但在大约 25 次重复执行后仅提高了 5-10%(0.25 - 1 秒),平均约为 0.5 秒。我可以将这个范围“归咎于”“服务器”负载。此外,据我所知,如果表变量不适合内存,它们仍然会命中 tempdb,因此再次增加了 I/O 延迟。我同意事务日志,但这不是我现在关心的问题,也不是企业想要听到的(如果它不能解决问题)。此外,删除表没有明确说明,临时表在会话关闭时被销毁。
    • 由于他在 select 中为其他临时表引用临时表,我想如果表足够大,在这样的临时表上添加索引可以大大加快速度。对于大型复杂流程,我们已经多次这样做了,这是最容易尝试的事情,因为它不涉及重写查询,然后进行测试以确保您在加速过程中没有更改功能。通常我们插入临时表,创建索引,做其他工作,然后在最后删除索引。
    • 对于小型数据集,表变量通常更快,而索引临时表通常更适合大型数据集。
    【解决方案2】:

    我可以看到查询吗?和两个表上的索引? 你的公羊有多大?每个表中的行有多大(大约)? 您可以更新表的统计信息并重新发送查询计划器吗?

    回答你的问题:

    1. 除了添加索引的部分外,您基本上是对的。添加索引将有助于查询进行查找。它还将使查询计划者有机会考虑嵌套循环连接计划而不是哈希连接计划。很遗憾,在我的问题得到解答之前,我无法回答更多。
    2. 您不需要向临时表添加索引。向这个临时表(或任何插入目标表)添加索引会增加写入时间,因为插入需要更新该索引。试想一个索引是你的表的副本,它的信息较少,它位于你的表的顶部,它需要与你的表同步。每次写入(插入、更新、删除)都需要更新此索引。
    3. 查看两个表的总行数,此查询的运行速度应该超过 10 秒,除非您有一台柠檬 PC,否则情况就不同了。

    编辑: 只想指出第 2 点,我没有意识到您的源表也是临时表。临时表在连接的每个会话结束后被销毁。给临时表添加索引意味着每次创建这个临时表都会增加额外的时间来创建这个索引。

    编辑: 对不起,我现在用手机。我只是会很短。 所以本质上是两件事:

    • 在创建临时表时添加主键,以便您一次性完成。不要为添加非聚集索引或任何覆盖索引而烦恼,您最终会花费更多时间来创建它们。

    • 查看您的查询,所有 case when 语句,而不是在此查询中执行,为什么不将它们添加为表中的另一列。本质上,您希望在进行分组时避免即时计算。您可以将 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.更新了问题的更多细节。
    • 2.临时表上没有索引。
    • 4.你在考虑页面碎片吗?
    • 5.临时表的统计信息?我不确定它是否有帮助,因为这些估计来自其他临时表等的数据。
    • 好的,内存不是你的问题。您是否尝试在创建时在两个表上添加主键?您无需担心统计信息,此临时表的生命周期有限,并且每次都会创建,因此统计信息将是最新的。
    【解决方案3】:
    1. 一个建议是增加存储过程的执行时间。 cmd.CommandTimeout = 200 // in seconds.
    2. 您还可以生成报告链接,并在生成报告时通过电子邮件将其发送给用户。
    3. 除此之外,使用 CTE 永远不要使用临时表,因为它们更昂贵。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-12-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-02-21
      • 1970-01-01
      相关资源
      最近更新 更多