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?
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))
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
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)
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
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!) GUID
s.
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)
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/
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
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.
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.