【问题标题】:No blocking, good execution plan, slow query: why?没有阻塞,执行计划好,查询慢:为什么?
【发布时间】:2019-07-12 14:00:21
【问题描述】:

我有一个查询有时需要几分钟才能完成。多个进程同时运行,但没有阻塞(我正在运行扩展事件会话,我可以看到其他事务的阻塞,因此检查记录事件的查询正在运行)。 看查询计划缓存,执行计划不错:在 SSMS 中运行,IO 不到 100 次,没有表或索引扫描。

用户有可能得到不同的计划,但如果我添加提示以在所有表​​上使用扫描(有些相当大),它仍然会在大约 1 秒内返回。所以最糟糕的执行计划仍然不会导致需要几分钟的查询。

排除了阻塞和错误的执行计划后,还有什么可以使查询变慢?

值得指出的一点是,SQL Server 使用了我们创建的索引视图,尽管代码没有引用它(我们使用的是 SQL Server Enterprise)。该索引视图有一个覆盖索引来支持查询并且它正在被使用 - 再次,执行计划非常好。原始查询使用 NOLOCK,我观察到索引视图的任何行或页面都没有锁定(因此 SQL Server 尊重我们的锁定提示,即使它访问的是索引视图而不是基础表 - 很好)。这是有道理的,否则我会看到阻塞。

我们在其他一些查询中使用索引视图,但我们在 SQL 代码中引用它们(并指定 NOLOCK、NOEXPAND)。我没有看到这些查询有任何问题,而且我不知道我们告诉优化器使用的索引视图和优化器本身决定使用的索引视图之间应该有什么区别,但是我所看到的表明有。

有什么想法吗?还有什么我应该看的吗?

这是查询:

execute sp_executesql 
N'SELECT DISTINCT p.policy_id
           , p.name_e AS policy_name_e
           , p.name_l AS policy_name_l
FROM       patient_visit_nl_view AS pv
INNER JOIN swe_cashier_transaction_nl_view AS ct ON ct.patient_visit_id = pv.patient_visit_id
           AND ct.split_date_time IS NOT NULL
INNER JOIN ar_invoice_nl_view AS ai ON ai.ar_invoice_id = ct.invoice_id
           AND ai.company_code = ''KOC''
           AND ai.transaction_status_rcd = ''TEMP''
INNER JOIN policy_nl_view p ON p.policy_id = ai.policy_id
WHERE      pv.patient_id = @pv__patient_id'
, N' @pv__patient_id uniqueidentifier'
, @pv__patient_id = '5D61EDF1-7542-11E8-BFCB-D89EF37315A2'

注意:带有后缀 _nl_view 的视图从带有 NOLOCK 的表中选择(想法是我们可以在将来更改它而不影响业务层代码)。

您可以在此处查看查询计划:https://www.brentozar.com/pastetheplan/?id=HJI9Lj_WH

IO 统计数据:

Table 'policy'. Scan count 0, logical reads 9, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'ar_invoice_cashier_transaction_visit_iview'. Scan count 1, logical reads 5, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

已锁定(IS 锁定所涉及的对象,仅此而已): locks taken

在索引视图的相关部分下方:

CREATE VIEW dbo.ar_invoice_cashier_transaction_visit_iview WITH SCHEMABINDING
AS
SELECT      ai.ar_invoice_id
        , ai.company_code
        , ai.policy_id
        , ai.transaction_status_rcd
        , ct.cashier_transaction_id
        , pv.patient_id
        -- more columns
FROM        dbo.ar_invoice AS ai
INNER JOIN  dbo.swe_cashier_transaction AS ct ON ct.invoice_id = ai.ar_invoice_id AND ct.split_date_time IS NOT NULL
INNER JOIN  dbo.patient_visit AS pv ON pv.patient_visit_id = ct.patient_visit_id

CREATE UNIQUE CLUSTERED INDEX XPKar_invoice_cashier_transaction_visit_iview ON dbo.ar_invoice_cashier_transaction_visit_iview (ar_invoice_id, cashier_transaction_id)

CREATE INDEX XIE4ar_invoice_cashier_transaction_visit_iview ON dbo.ar_invoice_cashier_transaction_visit_iview (patient_id, transaction_status_rcd, company_code) INCLUDE (policy_id)

到目前为止一切顺利。

但是每隔几天(而不是一天中的同一时间),事情就会变成梨形,查询需要几分钟并且实际上会超时(提供程序的命令超时设置为 10 分钟)。发生这种情况时,没有阻塞。我有一个扩展的活动会话,这是我的查询

DECLARE @event_xml xml;

SELECT      @event_xml = CONVERT(xml, target_data)
FROM        sys.dm_xe_sessions AS s
INNER JOIN  sys.dm_xe_session_targets AS t ON s.address = t.event_session_address
WHERE       s.name = 'Blocking over 10 seconds'

SELECT  DATEADD(hour, DATEDIFF(hour, GETUTCDATE(), GETDATE()), R.c.value('@timestamp', 'datetime')) AS time_stamp
    , R.c.value('(data[@name="blocked_process"]/value[1]/blocked-process-report[1]/blocked-process[1]/process)[1]/@spid', 'int') AS blocked_spid
    , R.c.value('(data[@name="blocked_process"]/value[1]/blocked-process-report[1]/blocked-process[1]/process[1]/inputbuf)[1]', 'varchar(max)') AS blocked_inputbuf
    , R.c.value('(data[@name="blocked_process"]/value[1]/blocked-process-report[1]/blocked-process[1]/process[1]/@waitresource)[1]', 'varchar(max)') AS wait_resource
    , R.c.value('(data[@name="blocked_process"]/value[1]/blocked-process-report[1]/blocking-process[1]/process)[1]/@spid', 'int') AS blocking_spid
    , R.c.value('(data[@name="blocked_process"]/value[1]/blocked-process-report[1]/blocking-process[1]/process[1]/inputbuf)[1]', 'varchar(max)') AS blocking_inputbuf
    , R.c.query('.')
FROM    @event_xml.nodes('/RingBufferTarget/event') AS R(c)
ORDER BY R.c.value('@timestamp', 'datetime') DESC

这个查询返回了其他阻塞情况,所以我相信它是正确的。在问题(超时)发生时,没有涉及上述查询或任何其他查询的阻塞情况。

由于没有阻塞,我正在研究错误查询计划的可能性。我没有在缓存中找到一个糟糕的计划(在我被授予远程访问权限之前,我已经建议对表进行 sp_recompile),因此我尝试考虑最糟糕的计划:扫描每个表。应用相关选项,以下是此查询的 IO 统计信息:

Table 'patient_visit'. Scan count 1, logical reads 4559, physical reads 0, read-ahead reads 7, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'swe_cashier_transaction'. Scan count 9, logical reads 24840, physical reads 0, read-ahead reads 23660, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'ar_invoice'. Scan count 9, logical reads 21247, physical reads 0, read-ahead reads 7074, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'policy'. Scan count 9, logical reads 271, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

这是执行计划:https://www.brentozar.com/pastetheplan/?id=rJr29s_br

客户拥有强大的 SQL Server 2012 机器、大量内核(maxdop 设置为 8)、大量内存。它吃掉了这个错误的早餐查询(大约需要 350 毫秒)。

为完整起见,以下是所涉及表的行数:

  • ar_invoice:2363527
  • swe_cashier_transaction: 2946514
  • 患者访问:654976
  • 政策:1038
  • ar_invoice_cashier_transaction_visit_iview:1999609

我还针对返回最多行的患者 ID 和不存在的患者 ID(即 0 行)运行查询。我使用重新编译选项运行这些:在这两种情况下,优化器都选择了相同(好的)执行计划。

所以回到问题:没有阻塞,查询计划似乎很好(即使是坏的,也不会坏到这个查询需要 10 分钟的程度),那么是什么原因导致的?

这里唯一有点不寻常的是,虽然 SQL 没有从索引视图中选择,但优化器仍然使用它——这是或应该是一件好事。我知道企业版声称它可以做到这一点,但这是我第一次在野外看到它(虽然我见过很多相反的情况:在 SQL 中引用索引视图,但优化器从视图中选择无论如何基础表)。我很想相信这是相关的。

【问题讨论】:

  • 无 - 此问题中没有信息。没有查询,没有表模式,没有执行计划,没有数据/行大小。除了NOLOCK - 这是一个错误。 NOLOCK 表示 don't respect locks, read dirty and duplicate data while taking far more locks。当您看到这一点时,这意味着有人遇到了性能问题并试图通过忽略锁来掩盖它
  • 这不是一个编码问题,你将无法用几个简单的表、几行数据和一个查询来重现这个问题。我什至无法在生产数据库中重现它。至于NOLOCK,它不是一个错误。我们的事务隔离级别是可序列化的(我们来自 COM+),这不是您在 ERP 系统中一夜之间改变的东西。这并不理想,但也不是错误。我希望其他人在索引视图方面遇到问题并提供一些提示。
  • COM+ 从来不需要SERIALIZABLE 在数据库中。早在 2000 年,它就使用 SERIALIZABLE 本身,这意味着数据库访问可以有一个更少限制的模型,前提是不同的服务/组件只使用 他们的 数据。在后来的几年里,它允许全方位的隔离级别。是的,NOLOCK 是一个错误。这意味着如果数据必须移动,您将读取不完整、未提交的数据甚至同一行两次。 NOLOCK 本身将在页面级别及更高级别获取额外的锁。
  • COM+ 中的默认隔离级别是可序列化的,我们保留了默认值。这与 NOLOCK 无关
  • 仅在 2000 年代初期。不过,这并没有强制数据库连接使用 SERIALIZABLE。此外,即使在 2000 年代(或者特别是在 2000 年代?)人们仍然使用短连接的乐观并发,所以即使 SERIALIZABLE 也没有造成太大的麻烦。

标签: sql-server


【解决方案1】:

在不了解您的设置的情况下,我会检查其他一些事情:

  • 盒子上的整体 CPU 和内存利用率是多少,是否存在资源争用
  • 如果您的存储位于 SAN 而不是本地存储上,存储端是否存在争用(如果您在不同系统的同一磁盘阵列上进行大量读取/写入,则可能会发生这种情况)

【讨论】:

  • 当时服务器似乎反应灵敏,系统其他区域没有缓慢的报告,但下次发生时我会要求客户收集一些性能计数器存储确实在SAN,我也会尝试获得一些统计数据。感谢您的建议,不胜感激。
【解决方案2】:

减慢查询速度可能还涉及其他几个因素。不过,就我个人而言,我并不真正相信 SQL Server 的优化技术。通常我会建议优化你的查询,这样优化器就不必做艰苦的工作,例如在主表上使用Exists/In,而不是加入和执行distinct/grouping,比如,

select  distinct ia.AttributeCode, ia.AttributeDescription
from    ItemsTable as i 
        inner join ItemAttributesTable as ia on i.AttributeCode = ia.AttributeCode
where   i.Manufacturer = @paramMfr
        and i.MfrYear between @paramYearStart and @paramYearYend

不要像上面那样运行查询,而是像这样运行它

select  ia.AttributeCode, ia.AttributeDescription
from    ItemAttributesTable as ia
where   ia.AttributeCode in (
            select  i.AttributeCode
            from    ItemsTable as i
            where   i.Manufacturer = @paramMfr
                    and i.MfrYear between @paramYearStart and @paramYearYend
        )

我不是真正的索引专家,但对于上述情况,我认为ItemsTable 中只有 1 个索引就足够了

另一个优化可以通过删除视图并直接使用表来完成,因为视图也可能在此处真正不需要的其他表上进行连接。

总而言之,主要的一点是,当查询优化器在找出可能的最佳方案时,它可能会遇到达到超时(称为优化器超时限制)的情况,在这种情况下它可能会选择制定一个在那个特定时间不是很好的计划,这就是为什么应该使用计划缓存的原因。这就是我在这里建议专注于优化查询而不是查看超时的原因的原因。

也检查一下https://blogs.msdn.microsoft.com/psssql/2018/10/19/understanding-optimizer-timeout-and-how-complex-queries-can-be-affected-in-sql-server/

更新 1:

建议:

  1. 使用Exists / In,即使您看到与当前查询相同的执行计划,这仍然有助于优化器几乎总是使用正确的计划
  2. 尝试消除视图并直接使用表,选择列更少。
  3. 确保根据给定参数定义了正确的索引
  4. 尝试将查询分解成更小的部分,例如在临时表中选择过滤后的数据,然后使用临时表获取其余详细信息
  5. 尝试谷歌搜索“应用程序超时而不是 SSMS”并查看不同的黑客攻击

查询超时的常见原因:

  1. 未定义索引
  2. 提取的数据过多
  3. 当您尝试从这些表中读取数据时,一个或多个表上存在锁定
  4. 参数类型和字段类型的区别,比如列是varchar,参数类型是nvarchar
  5. 参数嗅探

【讨论】:

  • 谢谢你,不胜感激。正如预期的那样,使用 IN () 而不是 DISTINCT 运行查询会给我相同的执行计划和 IO 统计信息。这就留下了一个问题:这种替代 SQL 是否不太容易受到优化器出错的影响(请记住,我仍然不知道“错误”是什么样的)?没法说。至于优化器超时,这是一个如此简单的查询,我真的看不出优化器没有时间来寻找最佳计划。编译时间约为 5 毫秒。
  • 正如我所说,我并不真正相信优化器会自行解决。这不是关于简单查询,而是关于服务器在特定时间的负载,所以有时简单查询也可能导致超时。顺便说一句,您也可以按照文章中所述始终强制执行特定计划。
  • 好的,明白了,也就是说,我们有大约 1000 个不同的业务层方法调用(4000 个表),每个都有几种查询风格(根据输入参数动态构建在业务层中)。其中许多查询比这个复杂几个数量级,在所有这些中,只有一个查询不时引起重大问题:这个。如果服务器的负载达到优化器不得不缩短其对最佳计划的搜索的程度,我预计会出现全面混乱,但我们没有看到。
  • 不幸的是,除非亲自检查所有相关的查询和场景,否则没有人能真正帮助您。无论如何,我已经更新了我的答案,请查看。
猜你喜欢
  • 1970-01-01
  • 2014-08-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-04-10
  • 1970-01-01
  • 2017-07-30
  • 2011-12-21
相关资源
最近更新 更多