Чому NOLOCK робить сканування зі змінним призначенням повільніше?


11

Я боюсь проти НОЛОКУ в моєму поточному оточенні. Один з аргументів, які я чув, - це те, що накладні витрати блокування сповільнюють запит. Отже, я створив тест, щоб побачити, наскільки може бути цей наклад.

Я виявив, що 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.
Девід Браун - Майкрософт

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 мс, а версія блокування справа - на 383 ( 51 м довше). Ви також можете бачити, що два кодові шляхи відрізняються таким чином:

  • Рівнина SELECT

    • sqlmin!RowsetNewSS::FetchNextRow дзвінки
      • sqlmin!IndexDataSetSession::GetNextRowValuesInternal
  • Використання NOLOCK

    • sqlmin!RowsetNewSS::FetchNextRow дзвінки
      • sqlmin!DatasetSession::GetNextRowValuesNoLock який дзвонить або
        • sqlmin!IndexDataSetSession::GetNextRowValuesInternal або
        • kernel32!TlsGetValue

Це показує, що в FetchNextRowметоді є деякі розгалуження, засновані на рівні натяку на рівень ізоляції / блокування.

Чому NOLOCKфілія займає більше часу?

Власне відділення фактично витрачає менше часу на дзвінки GetNextRowValuesInternal(на 25 мс менше). Але код безпосередньо в GetNextRowValuesNoLock(без урахування методів, які він називає AKA, стовпець "Екс") працює за 63 мс - що є більшою частиною різниці (63 - 25 = 38 мс чисте збільшення часу процесора).

Отже, що за інші 13 мс (загалом 51 мс - наразі 38 мс) накладних витрат FetchNextRow?

Інтерфейс відправлення

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

Мені, мабуть, слід запустити цей слід ще раз із повторенням запиту, я думаю, що є деякі дрібниці, як-от апаратні переривання, які не були зібрані частотою вибірки 1 мс PerfView


Поза цим методом я помітив ще одну невелику різницю, яка призводить до того, що версія блокування працює повільніше:

Звільнення замків

Схоже, гілка блокування більш агресивно працює sqlmin!RowsetNewSS::ReleaseRowsметодом, який ви можете побачити на цьому знімку екрана:

Звільнення замків

Простий вибір знаходиться на вершині, на 12ms, в той час як версія NOLOCK знаходиться на дні в 26ms ( 14ms довше). Ви також можете бачити в колонці "Коли", що код частіше виконувався під час вибірки. Це може бути деталізація щодо реалізації блокування, але, мабуть, введено зовсім невеликі накладні витрати для невеликих зразків.


Є багато інших невеликих відмінностей, але це великі шматки.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.