Por que o NOLOCK faz uma digitalização com a atribuição de variáveis ​​mais lenta?

11

Estou lutando contra o NOLOCK no meu ambiente atual. Um argumento que ouvi é que a sobrecarga do bloqueio diminui a velocidade de uma consulta. Então, criei um teste para ver o quanto essa sobrecarga pode ser.

Descobri que o NOLOCK realmente diminui a velocidade da digitalização.

No começo, fiquei encantado, mas agora estou confuso. Meu teste é inválido de alguma forma? O NOLOCK não deveria realmente permitir uma verificação um pouco mais rápida? O que está acontecendo aqui?

Aqui está o meu script:

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

O que eu tentei que não funcionou:

  • Executando em servidores diferentes (mesmos resultados, os servidores eram 2016-SP1 e 2016-SP2, ambos silenciosos)
  • Executando no dbfiddle.uk em versões diferentes (barulhento, mas provavelmente os mesmos resultados)
  • DEFINIR NÍVEL DE ISOLAMENTO em vez de dicas (mesmos resultados)
  • Desativando a escalação de bloqueio na tabela (mesmos resultados)
  • Examinando o tempo real de execução da verificação no plano de consulta real (mesmos resultados)
  • Dica de recompilação (mesmos resultados)
  • Grupo de arquivos somente leitura (mesmos resultados)

A exploração mais promissora vem da remoção da variável do lixo e do uso de uma consulta sem resultados. Inicialmente, isso mostrou o NOLOCK como um pouco mais rápido, mas quando mostrei a demonstração para o meu chefe, o NOLOCK voltou a ser mais lento.

O que há no NOLOCK que retarda uma varredura com atribuição de variável?

Forrest
fonte
Seria necessário alguém com acesso ao código-fonte e um criador de perfil para dar uma resposta definitiva. Mas o NOLOCK precisa fazer algum trabalho adicional para garantir que ele não entre em um loop infinito na presença de dados mutantes. E pode haver otimizações desabilitadas (também conhecidas como nunca testadas) para consultas NOLOCK.
David Browne - Microsoft
1
Nenhuma reprodução para mim no Microsoft SQL Server 2016 (SP1) (KB3182545) - 13.0.4001.0 (X64) localdb.
Martin Smith

Respostas:

12

NOTA: este pode não ser o tipo de resposta que você está procurando. Mas talvez seja útil para outros respondentes em potencial, na medida em que fornece pistas sobre onde começar a procurar

Quando executo essas consultas no rastreamento ETW (usando PerfView), obtenho os seguintes resultados:

Plain  - 608 ms  
NOLOCK - 659 ms

Então a diferença é 51ms . Isso está bastante morto com a sua diferença (~ 50ms). Meus números são um pouco mais altos em geral devido à sobrecarga de amostragem do criador de perfil.

Encontrando a diferença

Aqui está uma comparação lado a lado, mostrando que a diferença de 51ms está no FetchNextRowmétodo em sqlmin.dll:

FetchNextRow

A seleção simples está à esquerda em 332 ms, enquanto a versão nolock está à direita em 383 ( 51ms a mais). Você também pode ver que os dois caminhos de código diferem desta maneira:

  • Avião SELECT

    • sqlmin!RowsetNewSS::FetchNextRow chamadas
      • sqlmin!IndexDataSetSession::GetNextRowValuesInternal
  • Usando NOLOCK

    • sqlmin!RowsetNewSS::FetchNextRow chamadas
      • sqlmin!DatasetSession::GetNextRowValuesNoLock que chama tanto
        • sqlmin!IndexDataSetSession::GetNextRowValuesInternal ou
        • kernel32!TlsGetValue

Isso mostra que há alguma ramificação no FetchNextRowmétodo com base no nível de isolamento / dica nolock.

Por que o NOLOCKramo leva mais tempo?

Na verdade, o ramo nolock gasta menos tempo GetNextRowValuesInternalacessando (25ms a menos). Mas o código diretamente GetNextRowValuesNoLock(sem incluir os métodos que chama de AKA de "Exc") é executado por 63ms - que é a maior parte da diferença (63 - 25 = 38ms de aumento líquido no tempo da CPU).

Então, quais são os outros 13ms (51ms no total - 38ms até agora) de despesas gerais FetchNextRow?

Envio de interface

Eu pensei que isso era mais uma curiosidade do que qualquer coisa, mas a versão nolock parece incorrer em alguma sobrecarga de despacho de interface chamando o método da API do Windows kernel32!TlsGetValuevia kernel32!TlsGetValueStub- um total de 17ms. A seleção simples parece não passar pela interface, portanto nunca atinge o stub e gasta apenas 6ms TlsGetValue(uma diferença de 11ms ). Você pode ver isso acima na primeira captura de tela.

Provavelmente eu deveria executar esse rastreamento novamente com mais iterações da consulta, acho que existem algumas pequenas coisas, como interrupções de hardware, que não foram detectadas pela taxa de amostra de 1ms do PerfView


Fora desse método, notei outra pequena diferença que faz com que a versão nolock seja mais lenta:

Liberando bloqueios

O ramo nolock parece executar o sqlmin!RowsetNewSS::ReleaseRowsmétodo de forma mais agressiva , que você pode ver nesta captura de tela:

Liberando bloqueios

A planície de seleção está no topo, em 12ms, enquanto a versão nolock está no fundo a 26 ms ( 14ms mais). Você também pode ver na coluna "Quando" que o código foi executado com mais frequência durante a amostra. Este pode ser um detalhe de implementação do nolock, mas parece introduzir um pouco de sobrecarga em pequenas amostras.


Existem muitas outras pequenas diferenças, mas essas são as grandes partes.

Josh Darnell
fonte