为什么 NOLOCK 使带有变量赋值的扫描变慢?

For*_*est 14 sql-server sql-server-2016

在我目前的环境中,我正在与 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())
Run Code Online (Sandbox Code Playgroud)

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

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

最有希望的探索来自删除垃圾变量并使用无结果查询。最初这显示 NOLOCK 稍微快一点,但是当我向老板展示演示时,NOLOCK 又变慢了。

使用变量赋值减慢扫描速度的 NOLOCK 是什么?

Jos*_*ell 14

注意:这可能不是您正在寻找的答案类型。但也许它会帮助其他潜在的回答者提供线索从哪里开始寻找

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

Plain  - 608 ms  
NOLOCK - 659 ms
Run Code Online (Sandbox Code Playgroud)

所以差异是51ms。这与您的差异(约 50 毫秒)完全相同。由于分析器采样开销,我的数字总体上略高。

寻找差异

这是一个并排比较,显示 51ms 的差异在于 sqlmin.dll 中的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(少25 毫秒)。但是直接在GetNextRowValuesNoLock其中的代码(不包括它称为“Exc”列的方法)运行了 63 毫秒 - 这是差异的大部分(63 - 25 = 38 毫秒的 CPU 时间净增加)。

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

接口调度

我认为这是超过任何一个好奇心,但NOLOCK版本出现通过调用Windows API的方法招致一些接口调度开销kernel32!TlsGetValue通过kernel32!TlsGetValueStub-共17MS的。普通的 select 似乎没有通过接口,所以它永远不会命中存根,并且只花费 6 毫秒TlsGetValue(相差 11毫秒)。您可以在上面的第一个屏幕截图中看到这一点。

我可能应该通过查询的更多迭代再次运行此跟踪,我认为有一些小事情,例如硬件中断,没有被 PerfView 的 1ms 采样率捕获


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

释放锁

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

释放锁

普通选择在顶部,为 12 毫秒,而 nolock 版本在底部为 26毫秒(长14 毫秒)。您还可以在“何时”列中看到代码在示例期间执行的频率更高。这可能是 nolock 的一个实现细节,但是对于小样本来说,它似乎引入了相当多的开销。


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