NOLOCKで変数割り当てを使用したスキャンが遅くなるのはなぜですか?


11

現在の環境でNOLOCKと戦っています。私が聞いた1つの議論は、ロックのオーバーヘッドがクエリの速度を低下させるというものです。そこで、このオーバーヘッドがどれだけあるかを確認するためのテストを考案しました。

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実行(うるさいが、おそらく同じ結果)
  • ヒントの代わりにISOLATION LEVELを設定(同じ結果)
  • テーブルのロックエスカレーションをオフにする(同じ結果)
  • 実際のクエリプランでのスキャンの実際の実行時間の調査(同じ結果)
  • ヒントを再コンパイル(同じ結果)
  • 読み取り専用ファイルグループ(同じ結果)

最も有望な調査は、ゴミ箱変数を削除し、結果なしのクエリを使用することです。最初はこれによりNOLOCKがわずかに速くなっていることがわかりましたが、上司にデモを見せたところ、NOLOCKは遅くなりました。

変数の割り当てでスキャンを遅くするのはNOLOCKの何ですか?


決定的な答えを出すには、ソースコードにアクセスできる人とプロファイラーが必要です。ただし、NOLOCKは、変化するデータが存在する場合に無限ループに入らないようにするために、いくつかの追加作業を行う必要があります。また、NOLOCKクエリに対して無効化されている(別名、テストされていない)最適化がある場合があります。
David Browne-マイクロソフト、

1
Microsoft SQL Server 2016(SP1)(KB3182545)-13.0.4001.0(X64)localdbでの再現はありません。
マーティン・スミス

回答:


12

注:これはあなたが探している答えのタイプではないかもしれません。しかし、おそらくどこから探し始めるかについての手掛かりを提供する限り、他の潜在的な回答者にとって役立つでしょう

これらのクエリをETWトレース(PerfViewを使用)で実行すると、次の結果が得られます。

Plain  - 608 ms  
NOLOCK - 659 ms

したがって、差は51msです。これはあなたの違い(〜50ms)でかなり死んでいます。プロファイラーのサンプリングオーバーヘッドのため、私の数値は全体的にわずかに高くなっています。

違いを見つける

51msの違いがsqlmin.dllのFetchNextRowメソッドにあることを示す、並べて比較します。

FetchNextRow

プレーンセレクトは左側が332 msで、ノーロックバージョンは右側が383(51ms長い)です。また、2つのコードパスがこのように異なることがわかります。

  • 平野 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時間の増加)。

それで、他の13ms(51ms合計-これまでのところ38ms)のオーバーヘッドはFetchNextRow何ですか?

インターフェイスのディスパッチ

これは何よりも好奇心の強いものだと思いましたが、nolockバージョンは、Windows APIメソッドkernel32!TlsGetValuekernel32!TlsGetValueStub合計17ミリ秒で呼び出すことにより、インターフェイスディスパッチのオーバーヘッドを招くようです。単純な選択はインターフェースを通過しないように見えるため、スタブにヒットすることはなく、6msしか費やしませんTlsGetValue11msの差)。上記の最初のスクリーンショットでこれを確認できます。

おそらくクエリの反復を増やしてこのトレースを再度実行する必要があります。ハードウェアの割り込みなど、PerfViewの1msのサンプルレートでは検出されなかったいくつかの小さな問題があると思います


その方法の外で、私はnolockバージョンの実行を遅くする別の小さな違いに気づきました:

ロックの解放

nolockブランチは、より積極的にsqlmin!RowsetNewSS::ReleaseRowsメソッドを実行しているように見えます。このスクリーンショットで確認できます。

ロックの解放

プレーンセレクトは12msで上部にあり、nolockバージョンは26ms(14ms長い)で下部にあります。また、「いつ」列では、サンプル中にコードがより頻繁に実行されたことがわかります。これはnolockの実装の詳細かもしれませんが、小さなサンプルの場合、かなりのオーバーヘッドが発生するようです。


他にも多くの小さな違いがありますが、それらは大きなチャンクです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.