对 SQL Server 中似乎永不结束的查询进行故障排除

本文介绍查询似乎从未完成的问题的故障排除步骤,或者完成查询可能需要几个小时或几天时间。

什么是永不结束的查询?

本文档重点介绍继续执行或编译的查询,即 CPU 继续增加。 它不适用于被阻止或等待某些从未释放的资源的查询(CPU 保持不变或更改很少)。

重要

如果查询保留以完成其执行,最终将完成。 可能需要几秒钟,或者可能需要几天时间。

术语永不结束用于描述实际上查询最终完成时未完成的查询的感知。

标识永不结束的查询

若要确定查询是持续执行还是停滞在瓶颈上,请执行以下步骤:

  1. 运行以下查询:

    DECLARE @cntr int = 0
    
    WHILE (@cntr < 3)
    BEGIN
        SELECT TOP 10 s.session_id,
                        r.status,
                        r.wait_time,
                        r.wait_type,
                        r.wait_resource,
                        r.cpu_time,
                        r.logical_reads,
                        r.reads,
                        r.writes,
                        r.total_elapsed_time / (1000 * 60) 'Elaps M',
                        SUBSTRING(st.TEXT, (r.statement_start_offset / 2) + 1,
                        ((CASE r.statement_end_offset
                            WHEN -1 THEN DATALENGTH(st.TEXT)
                            ELSE r.statement_end_offset
                        END - r.statement_start_offset) / 2) + 1) AS statement_text,
                        COALESCE(QUOTENAME(DB_NAME(st.dbid)) + N'.' + QUOTENAME(OBJECT_SCHEMA_NAME(st.objectid, st.dbid)) 
                        + N'.' + QUOTENAME(OBJECT_NAME(st.objectid, st.dbid)), '') AS command_text,
                        r.command,
                        s.login_name,
                        s.host_name,
                        s.program_name,
                        s.last_request_end_time,
                        s.login_time,
                        r.open_transaction_count,
                        atrn.name as transaction_name,
                        atrn.transaction_id,
                        atrn.transaction_state
            FROM sys.dm_exec_sessions AS s
            JOIN sys.dm_exec_requests AS r ON r.session_id = s.session_id 
                    CROSS APPLY sys.Dm_exec_sql_text(r.sql_handle) AS st
            LEFT JOIN (sys.dm_tran_session_transactions AS stran 
                 JOIN sys.dm_tran_active_transactions AS atrn
                    ON stran.transaction_id = atrn.transaction_id)
            ON stran.session_id =s.session_id
            WHERE r.session_id != @@SPID
            ORDER BY r.cpu_time DESC
    
        SET @cntr = @cntr + 1
    WAITFOR DELAY '00:00:05'
    END
    
  2. 检查示例输出。

    • 当你注意到与以下输出类似的输出时,本文中的故障排除步骤特别适用,其中 CPU 在经过的时间中按比例增加,且没有明显的等待时间。 请务必注意,在这种情况下,更改 logical_reads 并不相关,因为某些 CPU 绑定的 T-SQL 请求可能根本不执行任何逻辑读取(例如执行计算或 WHILE 循环)。

      session_id status cpu_time logical_reads wait_time wait_type
      56 “正在运行” 7038 101000 0 Null
      56 可运行 12040 301000 0 Null
      56 “正在运行” 17020 523000 0 Null
    • 如果观察到的等待方案与以下情况类似,则本文不适用,其中 CPU 不会更改或更改非常轻微,并且会话正在等待资源。

      session_id status cpu_time logical_reads wait_time wait_type
      56 suspended 0 3 8312 LCK_M_U
      56 suspended 0 3 13318 LCK_M_U
      56 suspended 0 5 18331 LCK_M_U

    有关详细信息,请参阅 诊断等待或瓶颈

编译时间长

在极少数情况下,你可能会发现 CPU 在一段时间内持续增加,但这不是由查询执行驱动的。 相反,它可能由过多的编译(查询分析和编译)驱动。 在这些情况下,请检查 transaction_name 输出列并查找值 sqlsource_transform。 此事务名称指示编译。

收集诊断数据

若要使用 SQL Server Management Studio (SSMS)收集诊断数据,请执行以下步骤:

  1. 捕获估计的 查询执行计划 XML。

  2. 查看查询计划,查看是否有明显的迹象表明速度缓慢。 典型的示例包括:

    • 表或索引扫描(查看估计行)。
    • 由大型外部表数据集驱动的嵌套循环。
    • 嵌套循环,该循环的内侧有一个大分支。
    • 表后台处理程序。
    • SELECT列表中需要很长时间来处理每一行的函数。
  3. 如果查询随时快速运行,则可以捕获要比较的实际 XML 执行计划“快速”执行

查看收集的计划的方法

本部分将演示如何查看收集的数据。 它将使用 SQL Server 2016 SP1 和更高版本中收集的多个 XML 查询计划(使用扩展 *.sqlplan)。

按照以下步骤比较执行计划

  1. 打开以前保存的查询执行计划文件(.sqlplan)。

  2. 右键单击执行计划的空白区域,然后选择“ 比较显示计划”。

  3. 选择要比较的第二个查询计划文件。

  4. 查找指示大量行在运算符之间流动的粗箭头。 然后选择箭头前后的运算符,并比较两个计划中的实际行数

  5. 比较第二和第三个计划,以查看行的最大流是否在同一运算符中发生。

    下面是一个示例:

    比较 SSMS 中的查询计划。

解决方法

  1. 确保为查询中使用的表更新统计信息。

  2. 在查询计划中查找缺少的索引建议并应用任何索引。

  3. 使用简化查询的目标重写查询:

    • 使用更具选择性 WHERE 的谓词来减少预先处理的数据。
    • 将其分开。
    • 在临时表中选择一些部分,并在以后联接它们。
    • 由于优化器行目标,在TOP长时间运行的查询中删除和 EXISTSFAST (T-SQL)。 或者,可以使用提示DISABLE_OPTIMIZER_ROWGOAL。 有关详细信息,请参阅 行目标消失流氓
    • 避免在将语句合并为单个大型查询时使用通用表表达式(CTE)。
  4. 尝试使用 查询提示 生成更好的计划:

    • HASH JOINMERGE JOIN 提示
    • FORCE ORDER 提示
    • FORCESEEK 提示
    • RECOMPILE
    • PLAN N'<xml_plan>'如果快速查询计划可以强制使用,请使用
  5. 使用 查询存储 (QDS) 强制建立良好的已知计划(如果存在此类计划),如果 SQL Server 版本支持查询存储。

诊断等待或瓶颈

如果问题不是长时间运行的 CPU 驱动查询,请在此处提供此部分作为参考。 可以使用它对由于等待时间较长的查询进行故障排除。

若要优化正在等待瓶颈的查询,请确定等待的时间以及瓶颈的位置(等待类型)。 确认等待类型后,请减少等待时间或完全消除等待时间。

若要计算近似等待时间,请从查询运行时间中减去 CPU 时间(工作时间)。 通常,CPU 时间是实际执行时间,查询生存期的剩余部分正在等待。

如何计算近似等待持续时间的示例:

已用时间 (ms) CPU 时间(毫秒) 等待时间(ms)
3200 3000 200
7080 1000 6080

确定瓶颈或等待

  • 若要标识历史长时间等待查询(例如, >20% 的总运行时间是等待时间),请运行以下查询。 自 SQL Server 启动以来,此查询使用缓存查询计划的性能统计信息。

    SELECT t.text,
             qs.total_elapsed_time / qs.execution_count
             AS avg_elapsed_time,
             qs.total_worker_time / qs.execution_count
             AS avg_cpu_time,
             (qs.total_elapsed_time - qs.total_worker_time) / qs.execution_count
             AS avg_wait_time,
             qs.total_logical_reads / qs.execution_count
             AS avg_logical_reads,
             qs.total_logical_writes / qs.execution_count
             AS avg_writes,
             qs.total_elapsed_time
             AS cumulative_elapsed_time
    FROM sys.dm_exec_query_stats qs
             CROSS apply sys.Dm_exec_sql_text (sql_handle) t
    WHERE (qs.total_elapsed_time - qs.total_worker_time) / qs.total_elapsed_time
             > 0.2
    ORDER BY qs.total_elapsed_time / qs.execution_count DESC
    
  • 若要识别当前执行时间超过 500 毫秒的查询,请运行以下查询:

    SELECT r.session_id, r.wait_type, r.wait_time AS wait_time_ms
    FROM sys.dm_exec_requests r 
       JOIN sys.dm_exec_sessions s ON r.session_id = s.session_id 
    WHERE wait_time > 500
    AND is_user_process = 1
    
  • 如果可以收集查询计划,请从 SSMS 中的执行计划属性检查 WaitStats

    1. 运行包含 实际执行 计划的查询。
    2. 在“执行计划”选项卡中右键单击最左侧的运算符
    3. 选择“属性,然后选择 WaitStats 属性。
    4. 检查 WaitTimeMsWaitType
  • 如果熟悉 PSSDiag/SQLdiagSQL LogScout LightPerf/GeneralPerf 方案,请考虑使用其中任一方案收集性能统计信息并识别 SQL Server 实例上的等待查询。 可以使用 SQL Nexus 导入收集的数据并分析性能数据

帮助消除或减少等待的参考

每种等待类型的原因和解决方法各不相同。 没有一种常规方法来解析所有等待类型。 下面是排查和解决常见等待类型问题的文章:

有关许多等待类型及其指示的说明,请参阅“等待类型”中的表。