Pourquoi NOLOCK ralentit-il une analyse avec affectation variable?

11

Je me bats contre NOLOCK dans mon environnement actuel. Un argument que j'ai entendu est que la surcharge du verrouillage ralentit une requête. J'ai donc conçu un test pour voir à quel point ces frais généraux pourraient être.

J'ai découvert que NOLOCK ralentit réellement mon scan.

Au début, j'étais ravi, mais maintenant je suis juste confus. Mon test est-il invalide d'une manière ou d'une autre? NOLOCK ne devrait-il pas permettre un scan légèrement plus rapide? Qu'est-ce qu'il se passe ici?

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

Ce que j'ai essayé n'a pas fonctionné:

  • Fonctionnant sur différents serveurs (mêmes résultats, les serveurs étaient 2016-SP1 et 2016-SP2, tous deux silencieux)
  • Fonctionnant sur dbfiddle.uk sur différentes versions (bruyant, mais probablement les mêmes résultats)
  • DÉFINIR LE NIVEAU D'ISOLEMENT au lieu des indices (mêmes résultats)
  • Désactiver l'escalade des verrous sur la table (mêmes résultats)
  • Examen du temps d'exécution réel de l'analyse dans le plan de requête réel (mêmes résultats)
  • Astuce de recompilation (mêmes résultats)
  • Groupe de fichiers en lecture seule (mêmes résultats)

L'exploration la plus prometteuse vient de la suppression de la variable corbeille et de l'utilisation d'une requête sans résultat. Au début, cela montrait NOLOCK comme légèrement plus rapide, mais quand j'ai montré la démo à mon patron, NOLOCK était redevenu plus lent.

Qu'est-ce que NOLOCK ralentit un scan avec affectation de variable?

Forrest
la source
Il faudrait quelqu'un avec un accès au code source et un profileur pour donner une réponse définitive. Mais NOLOCK doit faire un travail supplémentaire pour s'assurer qu'il n'entre pas dans une boucle infinie en présence de données en mutation. Et il peut y avoir des optimisations qui sont désactivées (aka jamais testées) pour les requêtes NOLOCK.
David Browne - Microsoft
1
Pas de reproche pour moi sur Microsoft SQL Server 2016 (SP1) (KB3182545) - 13.0.4001.0 (X64) localdb.
Martin Smith

Réponses:

12

REMARQUE: ce n'est peut-être pas le type de réponse que vous recherchez. Mais il sera peut-être utile à d’autres répondeurs potentiels de fournir des indices sur les points de départ

Lorsque j'exécute ces requêtes sous le traçage ETW (à l'aide de PerfView), j'obtiens les résultats suivants:

Plain  - 608 ms  
NOLOCK - 659 ms

La différence est donc de 51 ms . C'est assez mort avec votre différence (~ 50 ms). Mes chiffres sont globalement légèrement plus élevés en raison des frais généraux d'échantillonnage du profileur.

Trouver la différence

Voici une comparaison côte à côte montrant que la différence de 51 ms est dans la FetchNextRowméthode dans sqlmin.dll:

FetchNextRow

La sélection simple est à gauche à 332 ms, tandis que la version nolock est à droite à 383 ( 51 ms de plus). Vous pouvez également voir que les deux chemins de code diffèrent de cette manière:

  • Plaine SELECT

    • sqlmin!RowsetNewSS::FetchNextRow appels
      • sqlmin!IndexDataSetSession::GetNextRowValuesInternal
  • En utilisant NOLOCK

    • sqlmin!RowsetNewSS::FetchNextRow appels
      • sqlmin!DatasetSession::GetNextRowValuesNoLock qui appelle soit
        • sqlmin!IndexDataSetSession::GetNextRowValuesInternal ou
        • kernel32!TlsGetValue

Cela montre qu'il y a une ramification dans la FetchNextRowméthode basée sur l'indice de niveau d'isolement / nolock.

Pourquoi la NOLOCKsuccursale prend-elle plus de temps?

La branche nolock passe en fait moins de temps à appeler GetNextRowValuesInternal(25 ms de moins). Mais le code directement GetNextRowValuesNoLock(sans inclure les méthodes qu'il appelle AKA la colonne "Exc") fonctionne pendant 63 ms - ce qui représente la majorité de la différence (63 - 25 = augmentation nette de 38 ms du temps CPU).

Alors, quels sont les 13 ms restants (51 ms au total - 38 ms comptabilisés jusqu'à présent) FetchNextRow?

Envoi de l'interface

Je pensais que c'était plus une curiosité qu'autre chose, mais la version nolock semble entraîner des frais de répartition d'interface en appelant la méthode API Windows kernel32!TlsGetValuevia kernel32!TlsGetValueStub- un total de 17 ms. La sélection simple semble ne pas passer par l'interface, donc elle ne frappe jamais le talon et ne passe que 6 ms TlsGetValue(une différence de 11 ms ). Vous pouvez le voir ci-dessus dans la première capture d'écran.

Je devrais probablement réexécuter cette trace avec plus d'itérations de la requête, je pense qu'il y a quelques petites choses, comme les interruptions matérielles, qui n'ont pas été détectées par le taux d'échantillonnage de 1 ms de PerfView


En dehors de cette méthode, j'ai remarqué une autre petite différence qui ralentit la version nolock:

Déverrouillage des verrous

La branche nolock semble exécuter la sqlmin!RowsetNewSS::ReleaseRowsméthode de manière plus agressive , que vous pouvez voir dans cette capture d'écran:

Déverrouillage des verrous

La sélection simple est en haut, à 12 ms, tandis que la version nolock est en bas à 26 ms ( 14 ms de plus). Vous pouvez également voir dans la colonne "Quand" que le code a été exécuté plus fréquemment au cours de l'exemple. Cela peut être un détail d'implémentation de nolock, mais il semble introduire un peu de surcharge pour les petits échantillons.


Il y a beaucoup d'autres petites différences, mais ce sont les gros morceaux.

Josh Darnell
la source