Почему 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.
Дэвид Браун - Microsoft
1
Не воспроизводить для меня на Microsoft SQL Server 2016 (SP1) (KB3182545) - 13.0.4001.0 (X64) localdb.
Мартин Смит

Ответы:

12

ПРИМЕЧАНИЕ: это может быть не тот тип ответа, который вы ищете. Но, возможно, это будет полезно для других потенциальных ответчиков, поскольку они дают подсказки о том, где начать искать

Когда я выполняю эти запросы в режиме трассировки ETW (используя PerfView), я получаю следующие результаты:

Plain  - 608 ms  
NOLOCK - 659 ms

Таким образом, разница составляет 51 мс . Это довольно мертвое с вашей разницей (~ 50 мс). Мои цифры немного выше в целом из-за накладных расходов выборки профилировщика.

Нахождение разницы

Вот параллельное сравнение, показывающее, что разница в 51 мс заключается в FetchNextRowметоде в 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(не включая методы, которые он называет AKA столбцом «Exc») работает в течение 63 мс, что составляет большую часть разницы (63 - 25 = 38 мс чистого увеличения времени ЦП).

Так в чем же другие 13 мс (всего 51 мс - 38 мс приходилось до сих пор) в накладных расходах FetchNextRow?

Диспетчеризация интерфейса

Я думал, что это было больше любопытно, чем что-либо еще, но версия nolock, по-видимому, влечет за собой некоторые накладные расходы на диспетчеризацию интерфейса, вызывая метод Windows API kernel32!TlsGetValueчерез kernel32!TlsGetValueStub- всего 17 мс. Простой выбор, кажется, не проходит через интерфейс, поэтому он никогда не попадает в заглушку и тратит только 6 мсTlsGetValue (разница в 11 мс ). Вы можете увидеть это выше на первом скриншоте.

Я, вероятно, должен выполнить эту трассировку снова с большим количеством итераций запроса, я думаю, что есть некоторые мелочи, такие как аппаратные прерывания, которые не были обнаружены частотой дискретизации 1 мс PerfView


Помимо этого метода, я заметил еще одно небольшое отличие, которое заставляет версию nolock работать медленнее:

Выпуск замков

Ветвь nolock, кажется, более агрессивно запускает sqlmin!RowsetNewSS::ReleaseRowsметод, который вы можете увидеть на этом скриншоте:

Отпускание замков

Простой выбор находится на вершине, на 12ms, в то время как версия NOLOCK находится на дне в 26ms ( 14ms дольше). В столбце «Когда» также видно, что код выполнялся чаще во время выборки. Это может быть деталью реализации nolock, но, похоже, она вносит немало накладных расходов для небольших выборок.


Есть много других небольших различий, но это большие куски.

Джош Дарнелл
источник