web-dev-qa-db-fra.com

Pourquoi les touches séquentielles GUID fonctionnent-elles plus rapidement que les touches INT séquentielles dans mon scénario de test?

Après avoir posé cette question comparant les GUID séquentiels et non séquentiels, j'ai essayé de comparer les performances INSERT sur 1) une table avec une GUID clé primaire initialisée séquentiellement avec newsequentialid(), et 2) une table avec une clé primaire INT initialisée séquentiellement avec identity(1,1). Je m'attendrais à ce que ce dernier soit le plus rapide en raison de la plus petite largeur des entiers, et il semble également plus simple de générer un entier séquentiel qu'un GUID séquentiel. Mais à ma grande surprise, les INSERT sur la table avec la clé entière étaient beaucoup plus lents que la table séquentielle GUID.

Cela montre l'utilisation moyenne du temps (ms) pour les tests:

NEWSEQUENTIALID()  1977
IDENTITY()         2223

Quelqu'un peut-il expliquer cela?

L'expérience suivante a été utilisée:

SET NOCOUNT ON

CREATE TABLE TestGuid2 (Id UNIQUEIDENTIFIER NOT NULL DEFAULT NEWSEQUENTIALID() PRIMARY KEY,
SomeDate DATETIME, batchNumber BIGINT, FILLER CHAR(100))

CREATE TABLE TestInt (Id Int NOT NULL identity(1,1) PRIMARY KEY,
SomeDate DATETIME, batchNumber BIGINT, FILLER CHAR(100))

DECLARE @BatchCounter INT = 1
DECLARE @Numrows INT = 100000


WHILE (@BatchCounter <= 20)
BEGIN 
BEGIN TRAN

DECLARE @LocalCounter INT = 0

    WHILE (@LocalCounter <= @NumRows)
    BEGIN
    INSERT TestGuid2 (SomeDate,batchNumber) VALUES (GETDATE(),@BatchCounter)
    SET @LocalCounter +=1
    END

SET @LocalCounter = 0

    WHILE (@LocalCounter <= @NumRows)
    BEGIN
    INSERT TestInt (SomeDate,batchNumber) VALUES (GETDATE(),@BatchCounter)
    SET @LocalCounter +=1
    END

SET @BatchCounter +=1
COMMIT 
END

DBCC showcontig ('TestGuid2')  WITH tableresults
DBCC showcontig ('TestInt')  WITH tableresults

SELECT batchNumber,DATEDIFF(ms,MIN(SomeDate),MAX(SomeDate)) AS [NEWSEQUENTIALID()]
FROM TestGuid2
GROUP BY batchNumber

SELECT batchNumber,DATEDIFF(ms,MIN(SomeDate),MAX(SomeDate)) AS [IDENTITY()]
FROM TestInt
GROUP BY batchNumber

DROP TABLE TestGuid2
DROP TABLE TestInt

MISE À JOUR: Modification du script pour effectuer les insertions basées sur une table TEMP, comme dans les exemples de Phil Sandler, Mitch Wheat et Martin ci-dessous, j'ai aussi constater que l'IDENTITÉ est plus rapide comme il se doit. Mais ce n'est pas la manière conventionnelle d'insérer des lignes, et je ne comprends toujours pas pourquoi l'expérience a mal tourné au début: même si j'omet GETDATE () de mon exemple d'origine, IDENTITY () est encore beaucoup plus lent. Il semble donc que la seule façon de rendre IDENTITY () surpasse NEWSEQUENTIALID () est de préparer les lignes à insérer dans une table temporaire et d'effectuer les nombreuses insertions en tant qu'insertion par lots à l'aide de cette table temporaire. Dans l'ensemble, je ne pense pas que nous ayons trouvé une explication au phénomène, et IDENTITY () semble toujours être plus lent pour la plupart des utilisations pratiques. Quelqu'un peut-il expliquer cela?

39
someName

J'ai modifié le code de @Phil Sandler pour supprimer l'effet d'appeler GETDATE () (il peut y avoir des effets/interruptions matérielles impliqués ??), et j'ai fait des lignes de la même longueur.

[Il y a eu plusieurs articles depuis SQL Server 2000 concernant les problèmes de synchronisation et les minuteries haute résolution, donc je voulais minimiser cet effet.]

Dans un modèle de récupération simple avec des données et un fichier journal dans les deux sens sur ce qui est requis, voici les délais (en secondes): (mis à jour avec de nouveaux résultats basés sur le code exact ci-dessous)

       Identity(s)  Guid(s)
       ---------    -----
       2.876        4.060    
       2.570        4.116    
       2.513        3.786   
       2.517        4.173    
       2.410        3.610    
       2.566        3.726
       2.376        3.740
       2.333        3.833
       2.416        3.700
       2.413        3.603
       2.910        4.126
       2.403        3.973
       2.423        3.653
    -----------------------
Avg    2.650        3.857
StdDev 0.227        0.204

Le code utilisé:

SET NOCOUNT ON

CREATE TABLE TestGuid2 (Id UNIQUEIDENTIFIER NOT NULL DEFAULT NEWSEQUENTIALID() PRIMARY KEY,
SomeDate DATETIME, batchNumber BIGINT, FILLER CHAR(88))

CREATE TABLE TestInt (Id Int NOT NULL identity(1,1) PRIMARY KEY,
SomeDate DATETIME, batchNumber BIGINT, FILLER CHAR(100))

DECLARE @Numrows INT = 1000000

CREATE TABLE #temp (Id int NOT NULL Identity(1,1) PRIMARY KEY, rowNum int, adate datetime)

DECLARE @LocalCounter INT = 0

--put rows into temp table
WHILE (@LocalCounter < @NumRows)
BEGIN
    INSERT INTO #temp(rowNum, adate) VALUES (@LocalCounter, GETDATE())
    SET @LocalCounter += 1
END

--Do inserts using GUIDs
DECLARE @GUIDTimeStart DateTime = GETDATE()
INSERT INTO TestGuid2 (SomeDate, batchNumber) 
SELECT adate, rowNum FROM #temp
DECLARE @GUIDTimeEnd  DateTime = GETDATE()

--Do inserts using IDENTITY
DECLARE @IdTimeStart DateTime = GETDATE()
INSERT INTO TestInt (SomeDate, batchNumber) 
SELECT adate, rowNum FROM #temp
DECLARE @IdTimeEnd DateTime = GETDATE()

SELECT DATEDIFF(ms, @IdTimeStart, @IdTimeEnd) AS IdTime, DATEDIFF(ms, @GUIDTimeStart, @GUIDTimeEnd) AS GuidTime

DROP TABLE TestGuid2
DROP TABLE TestInt
DROP TABLE #temp
GO

Après avoir lu l'enquête de @ Martin, j'ai relancé avec le TOP suggéré (@num) dans les deux cas, c'est-à-dire.

...
--Do inserts using GUIDs
DECLARE @num INT = 2147483647; 
DECLARE @GUIDTimeStart DATETIME = GETDATE(); 
INSERT INTO TestGuid2 (SomeDate, batchNumber) 
SELECT TOP(@num) adate, rowNum FROM #temp; 
DECLARE @GUIDTimeEnd DATETIME = GETDATE();

--Do inserts using IDENTITY
DECLARE @IdTimeStart DateTime = GETDATE()
INSERT INTO TestInt (SomeDate, batchNumber) 
SELECT TOP(@num) adate, rowNum FROM #temp;
DECLARE @IdTimeEnd DateTime = GETDATE()
...

et voici les résultats du timing:

       Identity(s)  Guid(s)
       ---------    -----
       2.436        2.656
       2.940        2.716
       2.506        2.633
       2.380        2.643
       2.476        2.656
       2.846        2.670
       2.940        2.913
       2.453        2.653
       2.446        2.616
       2.986        2.683
       2.406        2.640
       2.460        2.650
       2.416        2.720

    -----------------------
Avg    2.426        2.688
StdDev 0.010        0.032

Je n'ai pas pu obtenir le plan d'exécution réel, car la requête n'est jamais revenue! Il semble qu'un bogue soit probable. (Exécution de Microsoft SQL Server 2008 R2 (RTM) - 10.50.1600.1 (X64))

19
Mitch Wheat

Sur une nouvelle base de données dans un modèle de récupération simple avec un fichier de données de 1 Go et un fichier journal de 3 Go (ordinateur portable, les deux fichiers sur le même lecteur) et un intervalle de récupération défini sur 100 minutes (pour éviter un point de contrôle faussant les résultats) Je vois des résultats similaires à vous avec la seule ligne inserts.

J'ai testé trois cas: Pour chaque cas, j'ai fait 20 lots d'insertion de 100 000 lignes individuellement dans les tableaux suivants. Les scripts complets peuvent être trouvés dans l'historique des révisions de cette réponse .

CREATE TABLE TestGuid
  (
     Id          UNIQUEIDENTIFIER NOT NULL DEFAULT NEWSEQUENTIALID() PRIMARY KEY,
     SomeDate    DATETIME, batchNumber BIGINT, FILLER CHAR(100)
  )

CREATE TABLE TestId
  (
     Id          Int NOT NULL identity(1, 1) PRIMARY KEY,
     SomeDate    DATETIME, batchNumber BIGINT, FILLER CHAR(100)
  )

CREATE TABLE TestInt
  (
     Id          Int NOT NULL PRIMARY KEY,
     SomeDate    DATETIME, batchNumber BIGINT, FILLER  CHAR(100)
  )  

Pour le troisième tableau, le test a inséré des lignes avec une valeur Id incrémentée, mais elle a été auto-calculée en incrémentant la valeur d'une variable dans une boucle.

La moyenne du temps pris sur les 20 lots a donné les résultats suivants.

NEWSEQUENTIALID() IDENTITY()  INT
----------------- ----------- -----------
1999              2633        1878

Conclusion

Il semble donc clairement être au-dessus du processus de création de identity qui est responsable des résultats. Pour l'entier incrémenté auto-calculé, les résultats sont beaucoup plus conformes à ce que l'on attendrait en considérant uniquement le coût IO.

Lorsque je mets le code d'insertion décrit ci-dessus dans des procédures stockées et que j'examine sys.dm_exec_procedure_stats, Il donne les résultats suivants

proc_name      execution_count      total_worker_time    last_worker_time     min_worker_time      max_worker_time      total_elapsed_time   last_elapsed_time    min_elapsed_time     max_elapsed_time     total_physical_reads last_physical_reads  min_physical_reads   max_physical_reads   total_logical_writes last_logical_writes  min_logical_writes   max_logical_writes   total_logical_reads  last_logical_reads   min_logical_reads    max_logical_reads
-------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- --------------------
IdentityInsert 20                   45060360             2231067              2094063              2645079              45119362             2234067              2094063              2660080              0                    0                    0                    0                    32505                1626                 1621                 1626                 6268917              315377               276833               315381
GuidInsert     20                   34829052             1742052              1696051              1833055              34900053             1744052              1698051              1838055              0                    0                    0                    0                    35408                1771                 1768                 1772                 6316837              316766               298386               316774

Donc, dans ces résultats, total_worker_time Est environ 30% plus élevé. Cela représente

Durée totale du processeur, en microsecondes, consommée par les exécutions de cette procédure stockée depuis sa compilation.

Il apparaît donc simplement comme si le code qui génère la valeur IDENTITY est plus gourmand en CPU que celui qui génère la fonction NEWSEQUENTIALID() (La différence entre les 2 chiffres est 10231308 qui est en moyenne d'environ 5µs par insérer.) et que pour cette définition de table, ce coût fixe du processeur était suffisamment élevé pour l'emporter sur les lectures et écritures logiques supplémentaires encourues en raison de la plus grande largeur de la clé. (NB: Itzik Ben Gan a fait test similaire ici et a trouvé une pénalité de 2µs par insert)

Alors pourquoi IDENTITY est-il plus gourmand en CPU que UuidCreateSequential?

Je crois que cela est expliqué dans cet article . Pour chaque dixième identity valeur générée, SQL Server doit écrire la modification dans les tables système sur le disque

Qu'en est-il des inserts MultiRow?

Lorsque les 100 000 lignes sont insérées dans une seule instruction, j'ai trouvé que la différence disparaissait avec peut-être encore un léger avantage pour le cas GUID mais loin d'être des résultats aussi nets. La moyenne pour 20 lots dans mon test était

NEWSEQUENTIALID() IDENTITY()
----------------- -----------
1016              1088

La raison pour laquelle il n'y a pas de pénalité apparente dans le code de Phil et le premier ensemble de résultats de Mitch est parce qu'il se trouve que le code que j'ai utilisé pour l'insertion multi-lignes a utilisé SELECT TOP (@NumRows). Cela a empêché l'optimiseur d'estimer correctement le nombre de lignes qui seront insérées.

Cela semble être avantageux car il y a un certain point de basculement auquel il ajoutera une opération de tri supplémentaire pour les (soi-disant séquentiels!) GUIDs.

GUID Sort

Cette opération de tri n'est pas requise à partir de le texte explicatif dans BOL .

Crée un GUID supérieur à tout GUID précédemment généré par cette fonction sur un ordinateur spécifié depuis le démarrage de Windows. Après le redémarrage de Windows, le GUID peut recommencer à partir d'une plage inférieure, mais est toujours unique au monde.

Il m'a donc semblé un bogue ou une optimisation manquante que SQL Server ne reconnaît pas que la sortie du scalaire de calcul sera déjà pré-triée, comme elle le fait apparemment déjà pour la colonne identity. ( Modifier J'ai signalé cela et le problème de tri inutile est maintenant résolu dans Denali)

19
Martin Smith

Assez simple: avec GUID, il est moins cher de générer le numéro suivant dans la ligne que pour IDENTITY (La valeur actuelle de GUID n'a pas besoin d'être stockée, l'IDENTITY doit être Cela est vrai même pour NEWSEQUENTIALGUID.

Vous pouvez rendre le test plus équitable et utiliser un SEQUENCER avec un grand CACHE - ce qui est moins cher que l'IDENTITE.

Mais comme le dit M.R., les GUID présentent certains avantages majeurs. En fait, elles sont BEAUCOUP plus évolutives que les colonnes IDENTITY (mais seulement si elles ne sont PAS séquentielles).

Voir: http://blog.kejser.org/2011/10/05/boosting-insert-speed-by-generating-scalable-keys/

8
Thomas Kejser

Je suis fasciné par ce type de question. Pourquoi avez-vous dû l'afficher un vendredi soir? :)

Je pense que même si votre test est UNIQUEMENT destiné à mesurer les performances INSERT, vous avez (peut-être) introduit un certain nombre de facteurs qui pourraient être trompeurs (bouclage, transaction de longue durée, etc.)

Je ne suis pas complètement convaincu que ma version prouve quoi que ce soit, mais l'identité fonctionne mieux que les GUID (3,2 secondes contre 6,8 secondes sur un PC domestique):

SET NOCOUNT ON

CREATE TABLE TestGuid2 (Id UNIQUEIDENTIFIER NOT NULL DEFAULT NEWSEQUENTIALID() PRIMARY KEY,
SomeDate DATETIME, batchNumber BIGINT, FILLER CHAR(100))

CREATE TABLE TestInt (Id Int NOT NULL identity(1,1) PRIMARY KEY,
SomeDate DATETIME, batchNumber BIGINT, FILLER CHAR(100))

DECLARE @Numrows INT = 1000000

CREATE TABLE #temp (Id int NOT NULL Identity(1,1) PRIMARY KEY, rowNum int)

DECLARE @LocalCounter INT = 0

--put rows into temp table
WHILE (@LocalCounter < @NumRows)
BEGIN
    INSERT INTO #temp(rowNum) VALUES (@LocalCounter)
    SET @LocalCounter += 1
END

--Do inserts using GUIDs
DECLARE @GUIDTimeStart DateTime = GETDATE()
INSERT INTO TestGuid2 (SomeDate, batchNumber) 
SELECT GETDATE(), rowNum FROM #temp
DECLARE @GUIDTimeEnd  DateTime = GETDATE()

--Do inserts using IDENTITY
DECLARE @IdTimeStart DateTime = GETDATE()
INSERT INTO TestInt (SomeDate, batchNumber) 
SELECT GETDATE(), rowNum FROM #temp
DECLARE @IdTimeEnd DateTime = GETDATE()

SELECT DATEDIFF(ms, @IdTimeStart, @IdTimeEnd) AS IdTime
SELECT DATEDIFF(ms, @GUIDTimeStart, @GUIDTimeEnd) AS GuidTime

DROP TABLE TestGuid2
DROP TABLE TestInt
DROP TABLE #temp
4
Phil Sandler

J'ai exécuté votre exemple de script plusieurs fois en faisant quelques ajustements au nombre de lots et à la taille (et merci beaucoup de nous l'avoir fourni).

Tout d'abord, je dirai que vous ne mesurez qu'une seule fois les performances des touches - INSERT vitesse. Donc, à moins que vous ne soyez spécifiquement préoccupé uniquement par la saisie des données dans les tableaux le plus rapidement possible, cet animal a bien plus à offrir.

Mes conclusions étaient en général similaires aux vôtres. Cependant, je mentionnerais que la variance de la vitesse de INSERT entre GUID et IDENTITY (int) est légèrement plus grande avec GUID que avec IDENTITY - peut-être +/- 10% entre les exécutions. Les lots qui utilisaient IDENTITY variaient de moins de 2 à 3% à chaque fois.

À noter également, ma boîte de test est clairement moins puissante que la vôtre, j'ai donc dû utiliser un plus petit nombre de lignes.

3
Yuck

Je vais me référer à une autre conv sur stackoverflow pour ce même sujet - https://stackoverflow.com/questions/170346/what-are-the-performance-improvement-of-sequential-guid-over -standard-guid

Une chose que je sais, c'est que l'utilisation de GUID séquentiels est que l'utilisation de l'index est meilleure en raison du très petit mouvement des feuilles, et donc de la réduction de la recherche HD. Je pense que pour cette raison, les insertions seraient également plus rapides, car il n'a pas à répartir les clés sur un grand nombre de pages.

Mon expérience personnelle est que lorsque vous implémentez une grande base de données à fort trafic, il est préférable d'utiliser des GUID, car cela la rend beaucoup plus évolutive pour l'intégration avec d'autres systèmes. Cela vaut pour la réplication, en particulier, et les limites int/bigint .... pas que vous ne manquiez de bigints, mais finalement vous le ferez et reviendrez.

1
M.R.