【问题标题】:How can I improve the performance of this stored procedure如何提高此存储过程的性能
【发布时间】:2016-01-28 11:39:17
【问题描述】:

所以我现在才开始使用 SQL Server 大约两个月(我还是个新手),我必须提高存储过程的性能。它在 3 秒内运行,但出于某种原因,客户对结果不满意。 我试图训练自己阅读执行计划并找出问题所在。

在拿到 SQL Sentry Plan Explorer 之后,我发现只有一部分过程导致了问题,那部分是这样的:

With myAccount as
(
    select 
        ROW_NUMBER() over(order by Account) as Row_ID,
        ID, Account, 
        replace(Name, '*', '#_') Name, 
        Totaling 
    from 
        Account
    where 
        Company_ID = @company_id and Balance = 0)
,myR1C1 (ID, R1C1) as
(
    select 
        t1.ID, 
        case when t4.Account = t6.Account 
             then 'R[' + convert(nvarchar(10), t4.row_id - t1.Row_ID) + ']C'
             else 'R[' + convert(nvarchar(10), t4.Row_ID - t1.Row_ID) + ']C:R[' + convert(nvarchar(10), t6.Row_ID - t1.Row_ID) + ']C'  
        end R1C1 
        --t1.*,t2.*,t4.*,t6.*, t4.id-t1.id,t6.id-t1.id 
    from 
        myAccount t1
    cross apply 
        dbo.abx_sysSplitTwo(Totaling,'|') t2
    cross apply 
        (select top 1 
             Row_ID, ID, account 
         from myAccount t3 
         where t3.account >= t2.VFr 
           and t3.account <= t2.vto 
           and t1.account <> t3.account 
         order by t3.account) t4
    cross apply 
        (select top 1 
             Row_ID, ID, account  
         from myAccount t5 
         where t5.account >= t2.VFr 
           and t5.account <= t2.vto 
           and t1.account <> t5.account order by t5.account) t6
)
, myAccount2 as
(
    Select 
        t1.*, t2.R1C1 
    from myAccount t1 
    left join 
       (select 
            ID, STUFF((select',' + R1C1 
                       from myR1C1
                       where ID = a.id 
                       for xml path ('')), 1, 1, '') as R1C1
        from 
            myR1C1 as a
        group by 
            id) t2 on t1.ID = t2.id
--  order by row_ID

-- Data1
select tv.id [<dang n="BudData" u="0" o="1" fmt="1" fn="ID"/>],tv.account, tv.Name
 ,case when len(tv.R1C1)>0 then '=subtotal(9,' + tv.r1c1 + ')' else convert(nvarchar,sum(case When tp.[Date] between dateadd(yy,-1,@FromDate) and dateadd(d,-1,@FromDate) then isnull(Bud,0) else 0 end)) end A
 ,case when len(tv.R1C1)>0 then '=subtotal(9,' + tv.r1c1 + ')' else convert(nvarchar,Sum(case When tp.[Date] between dateadd(yy,-1,@FromDate) and dateadd(d,-1,@FromDate) then isnull(tp.Actual,0) else 0 end)) end B
 ,case when len(tv.R1C1)>0 then '=subtotal(9,' + tv.r1c1 + ')' else convert(nvarchar,sum(case When tp.[Date] between dateadd(yy,0,@FromDate) and dateadd(d,-1,dateadd(yy,1,@FromDate)) then isnull(Bud,0) else 0 end)) end C
 ,case when len(tv.R1C1)>0 then '=subtotal(9,' + tv.r1c1 + ')' else convert(nvarchar,sum(case When tp.[Date] between dateadd(yy,0,@FromDate) and dateadd(yy,0,@YTD) then isnull(Bud,0) else 0 end)) end D
 ,case when len(tv.R1C1)>0 then '=subtotal(9,' + tv.r1c1 + ')' else convert(nvarchar,Sum(case When tp.[Date] between dateadd(yy,0,@FromDate) and dateadd(yy,0,@YTD) then isnull(tp.Actual,0) else 0 end)) end E 
 ,case when len(tv.R1C1)>0 then '=subtotal(9,' + tv.r1c1 + ')' else convert(nvarchar,Sum(case When tp.[Date] between dateadd(yy,0,@FromDate) and dateadd(yy,0,@YTD) then isnull(tp.Actual,0) else 0 end)-sum(case When tp.[Date] between dateadd(yy,0,@FromDate) and dateadd(yy,0,@YTD) then isnull(tp.Bud,0) else 0 end)) end F
 ,case when len(tv.R1C1)>0 then '' else convert(nvarchar,case when sum(case When tp.[Date] between dateadd(yy,0,@FromDate) and dateadd(yy,0,@YTD) then isnull(tp.Bud,0) else 0 end) between -1 and 1 then 0 else (Sum(case When tp [Date] between dateadd(yy,0,@FromDate) and dateadd(yy,0,@YTD) then isnull(tp.Actual,0) else 0 end)/sum(case When tp.[Date] between dateadd(yy,0,@FromDate) and dateadd(yy,0,@YTD) then isnull(tp.Bud,0) else 0 end)*100) end) end G
 ,case when len(tv.R1C1)>0 then '=subtotal(9,' + tv.r1c1 + ')' else convert(nvarchar,Sum(case When tp.[Date] between dateadd(yy,0,@FromDate) and dateadd(yy,0,@YTD) then isnull(Rev,0)  -- Rev er her rettet fra REv til Faktisk else 0 end +case When tp.[Date] between dateadd(d,1,@YTD) and dateadd(d,-1,dateadd(yy,1,@FromDate)) then isnull(tp.Rev,0) else 0 end)) end H

我知道它看起来很大,也许我以这种方式展示它是愚蠢的,但经过 2 周的尝试并没有取得如此大的成功,人们开始逼迫我并抱怨我在这方面花了很多时间.. . 而且我真的不知道该怎么做。

到目前为止,我一直使用 SQL Profiler 工具来获取包含工作负载的文件,并在 Tunning Advisor 工具中使用它来查看它提出了哪些建议。 我得到了一些建议,说要建立一些统计数据和一些索引,我做到了,但差异几乎不明显。 我认为值得一提的另一件事是,在计算时(在使用 Profiler 和 Tunning Advisor 之前),这部分存储过程应返回的估计行数为 1600,而实际行数为 536。据我所知,这不是一件好事。 现在奇怪的部分来了,在使用从 Tunning Advisor 获得的建议后,它没有减少行的估计 nr,而是增加到 2800,但速度几乎相同,为 2-3 秒。 我知道还有很多事情可以做,但我现在既没有知识也没有时间深入研究它们,所以如果有人能指出我正确的方向,那就太好了。 如果还有什么我可以提供的,以便查明真相,我很乐意这样做,所以请尽管问。 我差点忘记了,预期的结果可能是 1 秒或更短……因为它只有 536 行,而且我的客户已经看到查询在超过 20.000 行上执行得更快

【问题讨论】:

    标签: sql sql-server stored-procedures query-optimization


    【解决方案1】:

    首先我邀请您阅读http://importblogkit.com/2015/05/how-do-you-eat-an-elephant/

    您需要将查询拆分成更小的部分以确定问题出在哪里。您已经找出查询的哪一部分是较慢的部分,现在您应该继续挖掘。

    您有 4 个查询 myAccountmyR1C1myAccount2 和最终选择。只需逐个运行并ANALYZE/EXPLAIN 逐个运行,看看有什么作用、需要多长时间以及使用什么索引。

    检查行数,尝试添加新索引或复合索引以提高速度。

    现在,如果您发现一些看起来很奇怪的东西,请阅读 How to create a Minimal, Complete, and Verifiable example. 并使用该部分创建一个新问题。

    【讨论】:

    • 虽然我对 /elephant-blog/ 的链接表示赞赏,但我认为 ANALYZE/EXPLAIN 在这里没有多大帮助,因为他在 MSSQL 上(从标签和代码判断)。此外,SQL Sentry Plan Explorer 几乎是一个图形解释器,在可视化查询处理器内部发生的事情方面做得非常出色。
    • 感谢您提供的信息。我会尽量用好它:)
    【解决方案2】:

    既然你已经在 SQL Sentry Plan Explorer 中运行了这个,你可以尝试只运行这个查询然后复制 Plan XML 数据吗? (您也可以保护 .queryanalysis 文件,但它可能包含有关您的服务器名称等的信息,您可能不想将其放在网上)。只需将其复制粘贴到 pastebin 或将其压缩到例如保管箱并在此处共享 URL。这样我们就能更好地了解真正发生的事情......

    乍一看,您似乎在做很多 ORDER BY,这可能“在机器上很繁重”,但我主要担心 dbo.abx_sysSplitTwo() 功能。对于初学者来说,函数对性能非常不利,而且它们肯定会弄乱查询优化器的猜测工作。此外,如果函数以“非最佳方式”编写,则可能会更慢。

    => 我是否可以假设它将'ABC | XYZ'之类的东西分成vfr ='ABC'和vto ='XYZ'?还是应该返回多条记录(例如,当它找到 'ABC|XYZ|PQR|STU' 时它返回 2 条记录?)或类似的东西?

    最后,我认为您遗漏了部分查询。

    无论如何,公用表表达式的问题在于,人们很容易被逐个查询构建查询所迷惑,从而无法弄清楚究竟可能出了什么问题。

    我建议将第一部分拆分为单独的临时表,然后使用例如查询计划资源管理器以查看需要这么长时间的原因。 (看看持续时间,“成本”字段可能会指示哪些部分可能无法很好地扩展,但最终持续时间是您现在关心的!)。添加索引可能看起来过多,但它们具有向表隐式添加(非常好的)统计信息的好处,在这种情况下,它们通常有助于随后的查询。此外,索引“大小合理的表”在现代硬件上花费的时间非常少。

    SELECT ROW_NUMBER() OVER (order by Account) as Row_ID,
           ID, Account, Name
           Totaling
      INTO #myAccount
      FROM Account
     WHERE Company_ID = @company_id 
       AND Balance = 0
    
    CREATE UNIQUE CLUSTERED INDEX uq0 ON #myAccount (Row_ID) WITH (FILLFACTOR = 100)
    CREATE                  INDEX idx1 ON #myAccount (Account) WITH (FILLFACTOR = 100) -- used later on for t4 and t6
    
    -- split the Totaling, I'm assuming multiple records can be returned by abx_sysSplitTwo
    SELECT DISTINCT Totaling
      INTO #pre_split
      FROM #myAccount 
    
    SELECT Totaling, vfr, vto
      INTO #split
      FROM #pre_split 
      CROSS APPLY dbo.abx_sysSplitTwo(Totaling,'|') t2
    
    CREATE CLUSTERED INDEX idx0 ON #split (Totaling) WITH (FILLFACTOR = 100)
    
    -- apply splitted values
     select t1.ID, 
            case when t4.Account = t6.Account 
                 then 'R[' + convert(nvarchar(10), t4.Row_ID - t1.Row_ID) + ']C'
                 else 'R[' + convert(nvarchar(10), t4.Row_ID - t1.Row_ID) + ']C:R[' + convert(nvarchar(10), t6.Row_ID - t1.Row_ID) + ']C'  
            end R1C1 
      INTO #myR1C1        
      FROM #myAccount t1
      JOIN #split t2
        ON t2.Totaling = t1.Totaling
      CROSS APPLY 
            (SELECT TOP 1 Row_ID, ID, Account 
              FROM #myAccount t3 
             WHERE t3.Account >= t2.VFr 
               AND t3.Account <= t2.vto 
               AND t3.Account <> t1.Account 
             ORDER BY t3.Account) t4
        CROSS APPLY 
            (SELECT TOP 1 Row_ID, ID, Account  
              FROM #myAccount t5 
             WHERE t5.Account >= t2.VFr 
               AND t5.Account <= t2.vto 
               AND t5.Account <> t1.Account
             ORDER BY t5.Account) t6
    
    CREATE CLUSTERED INDEX idx0 ON #myR1C1 (ID) WITH (FILLFACTOR = 100)
    
    -- final result (concatenate R1C1 fields)
    SELECT Row_ID, ID, Account, 
           REPLACE(Name, '*', '#_') Name, 
           R1C1
      FROM #myAccount
      LEFT OUTER JOIN  (SELECT ID, 
                               STUFF((SELECT ',' + t.R1C1 
                                        FROM #myR1C1 t
                                       WHERE t.ID = a.ID 
                                         FOR XML PATH ('')), 1, 1, '') as R1C1
                         FROM #myR1C1 as a
                        GROUP BY ID) t2 
                   ON t1.ID = t2.ID
    

    PS:是的,我知道,更好的方法是先做SELECT .. INTO #table FROM... WHERE 1 = 2,然后使用INSERT INTO 填充它,但是对于这个测试,我很懒惰......

    【讨论】:

    • 感谢您的指导,很遗憾,由于我被分配了一些其他任务,我目前无法尝试或提供任何其他信息。下周一我会回来回答的
    猜你喜欢
    • 2017-06-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-02
    • 1970-01-01
    • 2018-11-19
    相关资源
    最近更新 更多