¿Por qué NOLOCK hace un escaneo con una asignación de variables más lenta?

11

Estoy luchando contra NOLOCK en mi entorno actual. Un argumento que he escuchado es que la sobrecarga del bloqueo ralentiza una consulta. Entonces, ideé una prueba para ver cuánto podría ser esta sobrecarga.

Descubrí que NOLOCK en realidad ralentiza mi escaneo.

Al principio estaba encantado, pero ahora estoy confundido. ¿Es mi prueba inválida de alguna manera? ¿No debería NOLOCK realmente permitir un escaneo un poco más rápido? ¿Que esta pasando aqui?

Aquí está mi guión:

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())

Lo que he intentado que no funcionó:

  • Se ejecuta en diferentes servidores (los mismos resultados, los servidores fueron 2016-SP1 y 2016-SP2, ambos silenciosos)
  • Ejecutando en dbfiddle.uk en diferentes versiones (ruidoso, pero probablemente los mismos resultados)
  • ESTABLECER NIVEL DE AISLAMIENTO en lugar de pistas (mismos resultados)
  • Desactivar la escalada de bloqueo en la tabla (mismos resultados)
  • Examinar el tiempo de ejecución real de la exploración en el plan de consulta real (mismos resultados)
  • Sugerencia de recompilación (mismos resultados)
  • Grupo de archivos de solo lectura (mismos resultados)

La exploración más prometedora proviene de eliminar la variable papelera y utilizar una consulta sin resultados. Inicialmente, esto mostró a NOLOCK como un poco más rápido, pero cuando le mostré la demostración a mi jefe, NOLOCK volvió a ser más lento.

¿Qué tiene NOLOCK que ralentiza un escaneo con asignación variable?

Para descanso
fuente
Se necesitaría alguien con acceso al código fuente y un generador de perfiles para dar una respuesta definitiva. Pero NOLOCK tiene que hacer un trabajo adicional para asegurarse de que no entre en un bucle infinito en presencia de datos mutantes. Y puede haber optimizaciones que están deshabilitadas (es decir, nunca probadas) para consultas NOLOCK.
David Browne - Microsoft
1
No hay repro para mí en Microsoft SQL Server 2016 (SP1) (KB3182545) - 13.0.4001.0 (X64) localdb.
Martin Smith

Respuestas:

12

NOTA: este podría no ser el tipo de respuesta que está buscando. Pero quizás sea útil para otros posibles respondedores en cuanto a proporcionar pistas sobre dónde comenzar a buscar

Cuando ejecuto estas consultas bajo el rastreo ETW (usando PerfView), obtengo los siguientes resultados:

Plain  - 608 ms  
NOLOCK - 659 ms

Entonces la diferencia es 51ms . Esto es bastante acertado con su diferencia (~ 50ms). Mis números son ligeramente más altos en general debido a la sobrecarga de muestreo del generador de perfiles.

Encontrando la diferencia

Aquí hay una comparación de lado a lado que muestra que la diferencia de 51 ms está en el FetchNextRowmétodo en sqlmin.dll:

FetchNextRow

La selección simple está a la izquierda a 332 ms, mientras que la versión nolock está a la derecha a 383 ( 51 ms más). También puede ver que las dos rutas de código difieren de esta manera:

  • Llanura SELECT

    • sqlmin!RowsetNewSS::FetchNextRow llamadas
      • sqlmin!IndexDataSetSession::GetNextRowValuesInternal
  • Utilizando NOLOCK

    • sqlmin!RowsetNewSS::FetchNextRow llamadas
      • sqlmin!DatasetSession::GetNextRowValuesNoLock que llama
        • sqlmin!IndexDataSetSession::GetNextRowValuesInternal o
        • kernel32!TlsGetValue

Esto muestra que hay algunas ramificaciones en el FetchNextRowmétodo basadas en el nivel de aislamiento / sugerencia de nolock.

¿Por qué la NOLOCKrama tarda más?

La rama nolock en realidad pasa menos tiempo llamando GetNextRowValuesInternal(25ms menos). Pero el código directamente GetNextRowValuesNoLock(sin incluir los métodos que llama AKA la columna "Exc") se ejecuta durante 63 ms, que es la mayoría de la diferencia (63 - 25 = 38 ms de aumento neto en el tiempo de CPU).

Entonces, ¿cuáles son los otros 13 ms (51 ms en total - 38 ms representados hasta ahora) de gastos generales FetchNextRow?

Despacho de interfaz

Pensé que esto era más una curiosidad que otra cosa, pero la versión nolock parece incurrir en una sobrecarga de despacho de interfaz al llamar al método API de Windows a kernel32!TlsGetValuetravés de kernel32!TlsGetValueStubun total de 17 ms. La selección simple parece no pasar por la interfaz, por lo que nunca llega al apéndice y solo pasa 6 ms TlsGetValue(una diferencia de 11 ms ). Puedes ver esto arriba en la primera captura de pantalla.

Probablemente debería ejecutar este seguimiento nuevamente con más iteraciones de la consulta, creo que hay algunas cosas pequeñas, como interrupciones de hardware, que no fueron detectadas por la frecuencia de muestreo de 1 ms de PerfView


Fuera de ese método, noté otra pequeña diferencia que hace que la versión nolock se ejecute más lentamente:

Liberación de cerraduras

La rama nolock parece ejecutar el sqlmin!RowsetNewSS::ReleaseRowsmétodo de forma más agresiva , lo que puedes ver en esta captura de pantalla:

Liberación de cerraduras

La selección simple está en la parte superior, a 12 ms, mientras que la versión nolock está en la parte inferior a 26 ms ( 14 ms más). También puede ver en la columna "Cuándo" que el código se ejecutó con más frecuencia durante la muestra. Esto puede ser un detalle de implementación de nolock, pero parece introducir bastante sobrecarga para muestras pequeñas.


Hay muchas otras pequeñas diferencias, pero esas son las grandes partes.

Josh Darnell
fuente