为什么NOLOCK会使具有变量分配的扫描变慢?


11

在当前环境下,我正在与NOLOCK作战。我听到的一个论点是,锁定的开销会使查询变慢。因此,我设计了一个测试以查看此开销可能是多少。

我发现NOLOCK实际上减慢了我的扫描速度。

起初我很高兴,但是现在我很困惑。我的考试以某种方式无效吗?NOLOCK实际上不应该允许扫描速度稍快吗?这里发生了什么事?

这是我的脚本:

USE TestDB
GO

--Create a five-million row table
DROP TABLE IF EXISTS dbo.JustAnotherTable
GO

CREATE TABLE dbo.JustAnotherTable (
ID INT IDENTITY PRIMARY KEY,
notID CHAR(5) NOT NULL )

INSERT dbo.JustAnotherTable
SELECT TOP 5000000 'datas'
FROM sys.all_objects a1
CROSS JOIN sys.all_objects a2
CROSS JOIN sys.all_objects a3

/********************************************/
-----Testing. Run each multiple times--------
/********************************************/
--How fast is a plain select? (I get about 587ms)
DECLARE @trash CHAR(5), @dt DATETIME = SYSDATETIME()

SELECT @trash = notID  --trash variable prevents any slowdown from returning data to SSMS
FROM dbo.JustAnotherTable
ORDER BY ID
OPTION (MAXDOP 1)

SELECT DATEDIFF(MILLISECOND,@dt,SYSDATETIME())

----------------------------------------------
--Now how fast is it with NOLOCK? About 640ms for me
DECLARE @trash CHAR(5), @dt DATETIME = SYSDATETIME()

SELECT @trash = notID
FROM dbo.JustAnotherTable (NOLOCK)
ORDER BY ID --would be an allocation order scan without this, breaking the comparison
OPTION (MAXDOP 1)

SELECT DATEDIFF(MILLISECOND,@dt,SYSDATETIME())

我尝试过的方法不起作用:

  • 在不同的服务器上运行(相同的结果,服务器分别为2016-SP1和2016-SP2,均安静)
  • 在不同版本的dbfiddle.uk运行(嘈杂,但结果可能相同)
  • 设置隔离级别而不是提示(结果相同)
  • 关闭表上的锁升级(结果相同)
  • 在实际查询计划中检查扫描的实际执行时间(相同结果)
  • 重新编译提示(结果相同)
  • 只读文件组(结果相同)

最有前途的探索来自删除垃圾变量并使用无结果查询。最初,这表明NOLOCK的速度稍快一些,但是当我向老板展示该演示时,NOLOCK又变慢了。

NOLOCK有什么用,它会减慢使用变量分配进行的扫描的速度?


拥有源代码访问权限和分析器的人员才能给出确切的答案。但是NOLOCK必须做一些额外的工作,以确保在存在变异数据的情况下它不会进入无限循环。对于NOLOCK查询,可能会禁用(也从未测试过)优化。
David Browne-微软

1
在Microsoft SQL Server 2016(SP1)(KB3182545)-13.0.4001.0(X64)localdb上没有适合我的副本。
马丁·史密斯

Answers:


12

注意:这可能不是您要查找的答案的类型。但这可能会对其他潜在的回答者有所帮助,因为它提供了从哪里开始寻找的线索。

在ETW跟踪下(使用PerfView)运行这些查询时,得到以下结果:

Plain  - 608 ms  
NOLOCK - 659 ms

所以相差51ms。两者之间的差异(〜50ms)差强人意。由于探查器采样开销,我的总体数字略高。

找出差异

这是并排比较,显示sqlmin.dll中的FetchNextRow方法存在51ms的差异:

FetchNextRow

普通选择在332毫秒处位于左侧,而nolock版本在383处(较长51毫秒)处位于右侧。您还可以看到两个代码路径以这种方式不同:

  • 平原 SELECT

    • sqlmin!RowsetNewSS::FetchNextRow 来电
      • sqlmin!IndexDataSetSession::GetNextRowValuesInternal
  • 使用 NOLOCK

    • sqlmin!RowsetNewSS::FetchNextRow 来电
      • sqlmin!DatasetSession::GetNextRowValuesNoLock 哪个叫
        • sqlmin!IndexDataSetSession::GetNextRowValuesInternal 要么
        • kernel32!TlsGetValue

这表明FetchNextRow基于隔离级别/ nolock提示的方法中存在一些分支。

为什么NOLOCK分支需要更长的时间?

实际上,nolock分支花在调用上的时间更少GetNextRowValuesInternal(减少了25ms)。但是直接输入的代码GetNextRowValuesNoLock(不包括称为“ Exc”列的方法)不运行63ms,这是两者之间的大部分差异(63-25 = 38ms的CPU时间净增加)。

那么还有其他13ms(总共51ms-到目前为止占38ms)的开销是FetchNextRow多少?

接口分配

我以为这比什么都更有趣,但是nolock版本似乎通过以下方式调用Windows API方法kernel32!TlsGetValue,从而招致了一些接口调度开销kernel32!TlsGetValueStub:总计17ms。普通选择似乎没有通过界面,因此它从未命中存根,仅花费了6ms的时间TlsGetValue(相差11ms)。您可以在上面的第一个屏幕截图中看到这一点。

我可能应该在查询的更多迭代中再次运行此跟踪,我认为有些小东西(例如硬件中断)没有被PerfView的1ms采样率接受


在该方法之外,我注意到另一个小的差异,导致nolock版本的运行速度变慢:

释放锁

nolock分支似乎可以更积极地运行该sqlmin!RowsetNewSS::ReleaseRows方法,您可以在以下屏幕截图中看到该方法:

释放锁

普通选择位于顶部,时间为12ms,而nolock版本位于底部,时间为26ms(长14ms)。您还可以在“时间”列中看到在示例过程中执行代码的频率更高。这可能是nolock的实现细节,但似乎为小样本引入了很多开销。


还有许多其他小的差异,但是这些差异很大。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.