Pourquoi les clés GUID séquentielles sont-elles plus rapides que les clés INT séquentielles dans mon cas de test?

39

Après avoir posé cette question en comparant les GUID séquentiels et non séquentiels, j'ai essayé de comparer les performances INSERT sur 1) une table avec une clé primaire GUID initialisée séquentiellement avec newsequentialid(), et 2) une table avec une clé primaire INT initialisée séquentiellement avecidentity(1,1) . Je m'attendrais à ce que ce dernier soit le plus rapide en raison de la largeur réduite des nombres 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 de la table avec la clé entière étaient beaucoup plus lents que la table GUID séquentielle.

Cela montre le temps moyen d'utilisation (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

UPDATE: 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, je trouve également que IDENTITY est plus rapide que prévu. Mais ce n’est pas la méthode conventionnelle d’insertion de lignes et je ne comprends toujours pas pourquoi l’expérience a mal tourné au début: même si j’ignore GETDATE () de mon exemple initial, IDENTITY () est toujours beaucoup plus lent. Il semble donc que le seul moyen de rendre IDENTITY () supérieur à NEWSEQUENTIALID () est de préparer les lignes à insérer dans une table temporaire et d'effectuer les nombreuses insertions sous forme de batch-insert à l'aide de cette table temporaire. Au total, je ne pense pas que nous ayons trouvé d'explication au phénomène, et IDENTITY () semble toujours être plus lent pour la plupart des usages pratiques. Quelqu'un peut-il expliquer cela?

someName
la source
4
Une pensée uniquement: pourrait-il être possible de générer un nouveau GUID sans impliquer la table, alors que l'obtention de la prochaine valeur d'identité disponible introduit temporairement une sorte de verrou pour garantir que deux threads / connexions ne recevront pas la même valeur? Je devine juste vraiment. Question interessante!
personne en colère
4
Qui a dit qu'ils font ?? Il y a beaucoup de preuves qu'ils ne connaissent pas - voir l' espace disque de Kimberly Tripp n'est pas cher - ce n'est PAS la question! article de blog - elle fait un examen assez approfondi, et les GUID sont toujours clairement INT IDENTITY
perdus
2
L’expérience ci-dessus montre le contraire et les résultats sont reproductibles.
someName
2
L'utilisation IDENTITYne nécessite pas de verrou de table. Conceptuellement, je pouvais voir que vous pourriez vous attendre à ce qu'il prenne MAX (id) + 1, mais en réalité, la valeur suivante est stockée. En fait, cela devrait être plus rapide que de trouver le prochain GUID.
4
En outre, la colonne de remplissage pour la table TestGuid2 devrait probablement être CHAR (88) pour que les rangées soient de la même taille
Mitch Wheat,

Réponses:

19

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

[Plusieurs articles ayant été publiés depuis SQL Server 2000 sur les problèmes de minutage et les minuteries haute résolution, je voulais donc minimiser cet effet.]

Dans un modèle de récupération simple avec données et fichier journal, les temps (en secondes) sont les suivants: (Mise à 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 répété 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 de chronométrage:

       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 retournée! Il semble qu'un bug est probable. (Exécution de Microsoft SQL Server 2008 R2 (RTM) - 10.50.1600.1 (X64))

Mitch Wheat
la source
7
Illustre bien l’élément essentiel d’un bon benchmarking: assurez-vous de ne mesurer qu’une chose à la fois.
Aaronaught
Quel plan obtenez-vous ici? At-il un SORTopérateur pour les GUID?
Martin Smith
@ Martin: Bonjour, je n'ai pas vérifié les plans (faire plusieurs choses à la fois :)). Je regarderai un peu plus tard ...
Mitch Wheat,
@Mitch - Des commentaires à ce sujet? Je soupçonne plutôt que l’essentiel de ce que vous mesurez ici est le temps nécessaire pour trier les guides pour les grands encarts, ce qui, bien qu’intéressant, ne répond pas à la question initiale du PO, qui consistait à expliquer pourquoi les contrôles séquentiels donnaient de meilleurs résultats que les colonnes d’identité sur des disques simples. rangées insère dans les tests de l'OP.
Martin Smith
2
@Mitch - Bien que plus j'y pense, moins je comprends pourquoi quelqu'un voudrait utiliser de NEWSEQUENTIALIDtoute façon. Il approfondira l’index, utilisera 20% de pages de données supplémentaires dans le cas du PO et ne sera garanti d’augmenter que jusqu’à ce que la machine soit redémarrée, ce qui présente de nombreux inconvénients par rapport à un identity. Il semble simplement que, dans ce cas, le plan de requête en ajoute un autre inutile!
Martin Smith
19

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 se trouvant 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 à ceux de la rangée unique inserts .

J'ai testé trois cas: pour chaque cas, j'ai effectué 20 lots d'insertion de 100 000 lignes individuellement dans les tableaux suivants. Les scripts complets se trouvent 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 la troisième table, le test a inséré des lignes avec une Idvaleur d' incrémentation , mais celle-ci a été calculée automatiquement 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

Donc, il semble vraiment être des frais généraux de la identity processus de création qui est responsable des résultats. Pour l'entier incrémentant auto-calculé, les résultats sont bien plus en ligne avec ce que l'on pourrait s'attendre à voir si on ne considère que le coût des entrées-sorties.

Lorsque je mets le code d'insertion décrit ci-dessus dans des procédures stockées et que sys.dm_exec_procedure_statsje le passe en revue, 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_timeest environ 30% plus élevé. Cela représente

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

Il semble donc simplement que le code qui génère la IDENTITYvaleur NEWSEQUENTIALID()consomme plus de ressources de processeur que celui qui génère le (La différence entre les 2 chiffres est de 10231308 ce qui correspond à une moyenne de 5µs par insertion.) Et que pour cette définition de table ce coût de processeur fixe était suffisamment élevé pour compenser les lectures et écritures logiques supplémentaires dues à la plus grande largeur de la clé. (NB: Itzik Ben Gan a fait des tests similaires ici et a trouvé une pénalité de 2µs par insertion)

Alors, pourquoi est IDENTITYplus intensive en CPU que UuidCreateSequential?

Je crois que cela est expliqué dans cet article . Pour chaque dixième identityvaleur 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 déclaration, j'ai constaté que la différence disparaissait avec encore peut-être un léger avantage pour le GUIDcas, mais aucun résultat aussi net. 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 dans le premier ensemble de résultats de Mitch est parce qu'il est arrivé que le code que j'ai utilisé pour effectuer l'insertion à plusieurs lignes soit utilisé SELECT TOP (@NumRows). Cela empêchait l'optimiseur d'estimer correctement le nombre de lignes à insérer.

Cela semble être un avantage, car il existe un certain point de basculement auquel une nouvelle opération de tri sera ajoutée pour les (supposément séquentielles!) GUID.

Tri GUID

Cette opération de tri n'est pas requise à partir du 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 avoir redémarré Windows, le GUID peut redémarrer à partir d'une plage inférieure, mais reste globalement unique.

Il me semblait donc un bogue ou une optimisation manquante que SQL Server ne reconnaît pas que la sortie du scalaire de calcul sera déjà triée comme elle le fait apparemment pour la identitycolonne. ( Edit j'ai signalé ceci et le problème de tri inutile est maintenant résolu dans Denali )

Martin Smith
la source
Non pas que cela ait beaucoup d'impact, mais juste par souci de clarté, le nombre cité par Denny, 20 valeurs d'identité en cache, est incorrect - il devrait être 10.
Aaron Bertrand
@ AaronBertrand - Merci. Cet article que vous avez lié est très informatif.
Martin Smith
8

C'est très simple: avec le GUID, il est moins coûteux de générer le numéro suivant dans la ligne que pour IDENTITY (la valeur actuelle du GUID ne doit pas être stockée, mais IDENTITY). 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 meilleur marché qu’IDENTITY.

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

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

Thomas Kejser
la source
Je pense que vous avez manqué qu'ils utilisent des guids séquentiels.
Martin Smith
Martin: l'argument est vrai pour le GUID séquentiel aussi. IDENTITY doit être stocké (pour retrouver son ancienne valeur après un redémarrage), le GUID séquentiel n'a pas cette limitation.
Thomas Kejser
2
Oui, après mon commentaire, vous avez parlé de stocker de manière persistante plutôt que de la mémoriser. 2012 utilise également un cache pour IDENTITY. d'où les plaintes ici
Martin Smith
4

Je suis fasciné par ce type de question. Pourquoi avez-vous dû le poster 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 pouvant induire en erreur (bouclage, transaction de longue durée, etc.)

Je ne suis pas complètement convaincu que ma version prouve quoi que ce soit, mais identité fonctionne mieux que les GUID qu'il contient (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
Phil Sandler
la source
L'autre facteur que personne n'a mentionné est le modèle de récupération de la base de données et la croissance des fichiers journaux ...
Mitch Wheat,
@Mitch sur une nouvelle base de données dans un modèle de récupération simple avec des données et un fichier journal dimensionné de manière très supérieure à celle requise, j'obtiens des résultats similaires à ceux de l'OP.
Martin Smith
Je viens d'avoir des délais de 2,560 secondes pour l'identité et de 3,666 secondes pour Guid (dans un modèle de récupération simple avec des données et un fichier journal)
Mitch Wheat s'est
@Mitch - Sur le code de l'OP avec tout dans la même transaction ou sur le code de Phil?
Martin Smith
sur ce code des affiches, c'est pourquoi je commente ici. J'ai aussi posté le code que j'ai utilisé ...
Mitch Wheat,
3

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

Tout d'abord, je dirai que vous ne mesurez qu'un seul aspect de la performance des touches: la INSERTvitesse. Donc, à moins que vous ne cherchiez spécifiquement à importer des données dans les tableaux le plus rapidement possible, cet animal est bien plus riche.

Mes découvertes étaient en général similaires aux vôtres. Cependant, je mentionnerais que la variance de INSERTvitesse entre GUIDet IDENTITY(int) est légèrement plus grande avec GUIDque IDENTITY- peut-être +/- 10% entre les essais. Les lots utilisés IDENTITYvariaient de moins de 2 à 3% à chaque fois.

Il faut également noter que mon boîtier de test est nettement moins puissant que le vôtre. Je devais donc utiliser un nombre de lignes plus petit.

Beurk
la source
Lorsque la PC est un GUID, est-il possible que le moteur utilise non pas un index mais un algorithme de hachage pour déterminer l'emplacement physique de l'enregistrement correspondant? Les insertions dans une table fragmentée avec des clés primaires hachées sont toujours plus rapides que les insertions dans une table avec un index sur la clé primaire en raison de l'absence de surcharge de l'index. C'est juste une question - ne me reprochez pas si la réponse est non. Fournissez simplement le lien à l'autorité.
1

Je vais faire référence à une autre conférence sur stackoverflow pour le 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 le fait d’avoir des GUID séquentiels, c’est que l’utilisation de l’index est meilleure en raison du très faible mouvement des feuilles et donc de la réduction de la recherche HD. Je pense que pour cette raison, les encarts seraient plus rapides, car il n’aurait pas à distribuer les clés sur un grand nombre de pages.

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

M
la source
1
Vous ne manquez pas de BIGINTs, jamais ... Voir ceci: sqlmag.com/blog/it-possible-run-out-bigint-values
Thomas Kejser Le