Perché i tasti GUID sequenziali funzionano più velocemente dei tasti INT sequenziali nel mio caso di test?


39

Dopo aver posto questa domanda confrontando i GUID sequenziali e non sequenziali, ho provato a confrontare le prestazioni INSERT su 1) una tabella con una chiave primaria GUID inizializzata in sequenza con newsequentialid()e 2) una tabella con una chiave primaria INT inizializzata in sequenza con identity(1,1). Mi aspetto che quest'ultimo sia il più veloce a causa della minore larghezza degli interi, e sembra anche più semplice generare un numero intero sequenziale rispetto a un GUID sequenziale. Ma con mia sorpresa, gli INSERTI sulla tabella con il tasto intero erano significativamente più lenti della tabella GUID sequenziale.

Ciò mostra l'utilizzo del tempo medio (ms) per le esecuzioni del test:

NEWSEQUENTIALID()  1977
IDENTITY()         2223

Qualcuno può spiegare questo?

È stato utilizzato il seguente esperimento:

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

AGGIORNAMENTO: Modificando lo script per eseguire gli inserimenti basati su una tabella TEMP, come negli esempi di Phil Sandler, Mitch Wheat e Martin di seguito, trovo che l'IDENTITÀ sia più veloce di quanto dovrebbe essere. Ma questo non è il modo convenzionale di inserire righe, e ancora non capisco perché all'inizio l'esperimento sia andato storto: anche se ometto GETDATE () dal mio esempio originale, IDENTITY () è ancora molto più lento. Quindi sembra che l'unico modo per fare in modo che IDENTITY () superi NEWSEQUENTIALID () sia preparare le righe da inserire in una tabella temporanea ed eseguire i numerosi inserimenti come inserimento batch usando questa tabella temporanea. Tutto sommato, non credo che abbiamo trovato una spiegazione al fenomeno e IDENTITY () sembra essere ancora più lento per la maggior parte degli usi pratici. Qualcuno può spiegare questo?


4
Solo un pensiero: potrebbe essere possibile generare un nuovo GUID senza coinvolgere affatto la tabella, mentre ottenere il successivo valore di identità disponibile introduce temporaneamente un tipo di blocco per garantire che due thread / connessioni non ottengano lo stesso valore? Sto solo indovinando davvero. Domanda interessante!
persona arrabbiata,

4
Chi dice che lo fanno ?? Ci sono molte prove che non lo fanno - vedi lo spazio su disco di Kimberly Tripp è economico - NON è questo il punto! post sul blog - fa una recensione abbastanza approfondita e i GUID si perdono sempre chiaramente aINT IDENTITY
marc_s

2
Bene, l'esperimento sopra mostra il contrario e i risultati sono ripetibili.
someName

2
L'uso IDENTITYnon richiede un blocco della tabella. Concettualmente ho potuto vedere che potresti aspettarti che prenda MAX (id) + 1, ma in realtà viene memorizzato il valore successivo. In realtà dovrebbe essere più veloce di trovare il prossimo GUID.

4
Inoltre, presumibilmente la colonna di riempimento per la tabella TestGuid2 dovrebbe essere CHAR (88) per rendere le file uguali
Mitch Wheat

Risposte:


19

Ho modificato il codice di @Phil Sandler per rimuovere l'effetto della chiamata a GETDATE () (potrebbero esserci effetti / interruzioni hardware ??) e ho creato righe della stessa lunghezza.

[Ci sono stati diversi articoli da SQL Server 2000 relativi a problemi di temporizzazione e timer ad alta risoluzione, quindi volevo minimizzare questo effetto.]

Nel semplice modello di recupero con dati e file di registro entrambi dimensionati rispetto a quanto richiesto, ecco i tempi (in secondi): (Aggiornato con nuovi risultati basati sul codice esatto di seguito)

       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

Il codice utilizzato:

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

Dopo aver letto l'indagine di @ Martin, ho eseguito di nuovo il TOP suggerito (@num) in entrambi i casi, ad es

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

e qui sono i risultati di temporizzazione:

       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

Non sono stato in grado di ottenere il piano di esecuzione effettivo, poiché la query non è mai tornata! Sembra che un bug sia probabile. (Esecuzione di Microsoft SQL Server 2008 R2 (RTM) - 10.50.1600.1 (X64))


7
Illustra chiaramente l'elemento critico di un buon benchmarking: assicurati di misurare solo una cosa alla volta.
Aaronaught

Che piano hai qui? Ha un SORToperatore per i GUID?
Martin Smith,

@Martin: Ciao, non ho controllato i piani (facendo alcune cose contemporaneamente :)). Daro 'un'occhiata un po' più tardi ...
Mitch Wheat,

@Mitch - Qualche feedback in merito? Sospetto piuttosto che la cosa principale che stai misurando qui sia il tempo impiegato per ordinare le guide per inserti di grandi dimensioni che, sebbene interessanti, non rispondono alla domanda originale del PO, che consisteva nel dare una spiegazione sul perché le guide sequenziali si comportassero meglio delle colonne di identità su un singolo inserimenti di riga nel test del PO.
Martin Smith,

2
@Mitch - Anche se più ci penso e meno capisco perché qualcuno vorrebbe mai usare NEWSEQUENTIALIDcomunque. Ciò consentirà di approfondire l'indice, utilizzare il 20% in più di pagine di dati nel caso del PO ed è garantito un aumento costante fino al riavvio della macchina, quindi presenta molti svantaggi rispetto a un identity. Sembra proprio che in questo caso il Piano delle query ne aggiunga un ulteriore non necessario!
Martin Smith,

19

Su un nuovo database nel modello di recupero semplice con il file di dati di 1 GB e il file di registro di 3 GB (computer portatile, entrambi i file sulla stessa unità) e l'intervallo di ripristino impostato su 100 minuti (per evitare un checkpoint che distorca i risultati) Vedo risultati simili a te con la singola riga inserts.

Ho testato tre casi: per ogni caso ho fatto 20 lotti di inserimento individuale di 100.000 righe nelle seguenti tabelle. Gli script completi sono disponibili nella cronologia delle revisioni di questa risposta .

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

Per la terza tabella il test ha inserito le righe con un Idvalore incrementale , ma questo è stato calcolato automaticamente incrementando il valore di una variabile in un ciclo.

La media del tempo impiegato tra i 20 lotti ha dato i seguenti risultati.

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

Conclusione

Quindi sembra sicuramente essere il sovraccarico del identityprocesso di creazione che è responsabile dei risultati. Per l'intero incrementale auto-calcolato, i risultati sono molto più in linea con ciò che ci si aspetterebbe di vedere se si considerasse solo il costo di I / O.

Quando inserisco il codice di inserimento sopra descritto nelle procedure memorizzate e sys.dm_exec_procedure_statslo rivedo, si ottengono i seguenti risultati

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

Quindi in questi risultati total_worker_timeè circa il 30% più alto. Questo rappresenta

Quantità totale di tempo della CPU, in microsecondi, consumata dalle esecuzioni di questa procedura memorizzata da quando è stata compilata.

Quindi sembra semplicemente che il codice che genera il IDENTITYvalore sia più intensivo per la CPU di quello che genera il NEWSEQUENTIALID()(La differenza tra le 2 cifre è 10231308 che si aggira intorno ai 5µs per inserto.) E che per questa definizione di tabella questo costo fisso della CPU era sufficientemente alto da superare le letture e le scritture logiche aggiuntive dovute alla maggiore larghezza della chiave. (NB: Itzik Ben Gan ha fatto test simili qui e ha trovato una penalità di 2µs per inserto)

Quindi perché è IDENTITYpiù intenso della CPU di UuidCreateSequential?

Credo che questo sia spiegato in questo articolo . Per ogni decimo identityvalore generato, SQL Server deve scrivere la modifica nelle tabelle di sistema sul disco

Che dire degli inserti MultiRow?

Quando le 100.000 righe sono inserite in una singola istruzione, ho scoperto che la differenza è scomparsa con ancora forse un leggero vantaggio per il GUIDcaso, ma in nessun modo vicino come risultati netti. La media per 20 lotti nel mio test era

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

La ragione per cui non ha la penalità evidente nel codice di Phil e nella prima serie di risultati di Mitch è perché è successo che il codice che ho usato per fare l'inserzione a più righe utilizzato SELECT TOP (@NumRows). Ciò ha impedito all'ottimizzatore di stimare correttamente il numero di righe che verranno inserite.

Questo sembra essere di beneficio in quanto vi è un certo punto di non ritorno in cui aggiungerà un'operazione di ordinamento aggiuntiva per i (presumibilmente sequenziali!) GUID.

Ordinamento GUID

Questa operazione di ordinamento non è richiesta dal testo esplicativo in BOL .

Crea un GUID maggiore di qualsiasi GUID precedentemente generato da questa funzione su un computer specificato dall'avvio di Windows. Dopo aver riavviato Windows, il GUID può ricominciare da un intervallo inferiore, ma è ancora unico a livello globale.

Quindi mi è sembrato un bug o un'ottimizzazione mancante che SQL Server non riconosce che l'output dello scalare di calcolo sarà già preordinato come apparentemente già fa per la identitycolonna. ( Modifica Ho segnalato questo e il problema di ordinamento non necessario è stato risolto in Denali )


Non che abbia un grande impatto, ma solo nell'interesse della chiarezza il numero citato da Denny, 20 valori di identità memorizzati nella cache, non è corretto - dovrebbe essere 10.
Aaron Bertrand

@AaronBertrand - Grazie. L'articolo che hai collegato è più informativo.
Martin Smith,

8

Abbastanza semplice: con GUID, è più economico generare il numero successivo nella riga di quanto lo sia per IDENTITY (il valore corrente del GUID non deve essere memorizzato, l'IDENTITY deve essere). Questo vale anche per NEWSEQUENTIALGUID.

Potresti rendere il test più equo e utilizzare un SEQUENCER con un grande CACHE, che è più economico dell'IDENTITÀ.

Ma come dice MR, ci sono alcuni importanti vantaggi per i GUID. È un dato di fatto, sono MOLTO più scalabili delle colonne IDENTITY (ma solo se NON sono sequenziali).

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


Penso che ti sia perso il fatto che stiano usando guide sequenziali.
Martin Smith,

Martin: l'argomento è vero anche per il GUID sequenziale. IDENTITY deve essere memorizzato (per tornare al vecchio valore dopo un riavvio), il GUID sequenziale non ha questa limitazione.
Thomas Kejser,

2
Sì, dopo il mio commento, hai capito che stavi parlando di archiviare in modo persistente piuttosto che in memoria. Anche il 2012 utilizza una cache IDENTITY. quindi lamentele qui
Martin Smith,

4

Sono affascinato da questo tipo di domanda. Perché l'hai dovuto pubblicare un venerdì sera? :)

Penso che anche se il tuo test è destinato SOLO a misurare le prestazioni INSERT, tu (potresti) hai introdotto una serie di fattori che potrebbero essere fuorvianti (loop, una transazione a lungo termine, ecc.)

Non sono del tutto convinto che la mia versione provi qualcosa, ma l'identità ha prestazioni migliori rispetto ai GUID (3.2 secondi contro 6.8 secondi su un PC di casa):

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

L'altro fattore che nessuno ha menzionato è il modello di recupero del database e le crescite dei file di registro ...
Mitch Wheat

@Mitch su un nuovo database in un semplice modello di recupero con dati e file di registro di entrambe le dimensioni su ciò che è necessario ottengo risultati simili all'OP.
Martin Smith,

Ho appena avuto un tempo di 2.560 secondi per Identity e 3.666 secondi per Guid (in un semplice modello di recupero con dati e file di registro di entrambe le dimensioni su quanto richiesto)
Mitch Wheat

@Mitch - Sul codice OP con tutto nella stessa transazione o sul codice Phil?
Martin Smith,

su questo codice poster, ecco perché sto commentando qui. Ho anche pubblicato il codice che ho usato ...
Mitch Wheat

3

Ho eseguito il tuo script di esempio diverse volte apportando alcune modifiche al conteggio e alla dimensione dei lotti (e ti ringrazio molto per averlo fornito).

Innanzitutto dirò che stai misurando solo una volta l'aspetto delle prestazioni dei tasti: la INSERTvelocità. Quindi, a meno che tu non sia specificamente preoccupato solo di ottenere i dati nelle tabelle il più rapidamente possibile, c'è molto di più in questo animale.

Le mie scoperte erano in generale simili alle tue. Tuttavia, vorrei ricordare che la varianza in INSERTvelocità tra GUIDe IDENTITY(int) è leggermente più grande con GUIDche con IDENTITY- forse +/- 10% tra le esecuzioni. I lotti che usavano IDENTITYvariavano meno del 2-3% ogni volta.

Inoltre, la mia casella di test è chiaramente meno potente della tua, quindi ho dovuto usare conteggi di righe più piccoli.


Quando il PK è un GUID, è possibile che il motore non utilizzi un indice ma un algoritmo di hashing per determinare la posizione fisica del record corrispondente? Gli inserimenti in una tabella sparsa con chiavi primarie con hash sono sempre più veloci degli inserti in una tabella con un indice sulla chiave primaria a causa dell'assenza del sovraccarico dell'indice. È solo una domanda: non votarmi se la risposta è No. Basta fornire il link all'autorità.

1

Tornerò a un'altra convocazione su stackoverflow per questo stesso argomento - https://stackoverflow.com/questions/170346/what-are-the-performance-improvement-of-sequential-guid-over-standard-guid

Una cosa che so è che avere GUID sequenziali è che l'uso dell'indice è migliore a causa del movimento delle foglie molto ridotto, e quindi riducendo la ricerca HD. Penserei per questo che anche gli inserti sarebbero più veloci, in quanto non è necessario distribuire le chiavi su un gran numero di pagine.

La mia esperienza personale è che quando si implementa un DB ad alto traffico elevato, è meglio usare i GUID, perché lo rende molto più scalabile per l'integrazione con altri sistemi. Questo vale per la replica, in particolare, e per i limiti int / bigint .... non che ti manchino le origini, ma alla fine lo farai e tornerai indietro.


1
Non rimani senza BIGINT, mai ... Vedi questo: sqlmag.com/blog/it-possible-run-out-bigint-values
Thomas
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.