【发布时间】: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