¿Por qué las teclas GUID secuenciales funcionan más rápido que las teclas INT secuenciales en mi caso de prueba?


39

Después de hacer esta pregunta comparando GUID secuenciales y no secuenciales, traté de comparar el rendimiento de INSERT en 1) una tabla con una clave primaria GUID inicializada secuencialmente newsequentialid()y 2) una tabla con una clave primaria INT inicializada secuencialmente identity(1,1). Esperaría que este último sea más rápido debido al menor ancho de los enteros, y también parece más simple generar un entero secuencial que un GUID secuencial. Pero para mi sorpresa, los INSERT en la tabla con la tecla entera fueron significativamente más lentos que la tabla secuencial GUID.

Esto muestra el uso de tiempo promedio (ms) para las ejecuciones de prueba:

NEWSEQUENTIALID()  1977
IDENTITY()         2223

¿Alguien puede explicar esto?

Se utilizó el siguiente experimento:

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

ACTUALIZACIÓN: Modificando el script para realizar las inserciones basadas en una tabla TEMP, como en los ejemplos de Phil Sandler, Mitch Wheat y Martin a continuación, también encuentro que IDENTITY es más rápido como debería ser. Pero esa no es la forma convencional de insertar filas, y todavía no entiendo por qué el experimento salió mal al principio: incluso si omito GETDATE () de mi ejemplo original, IDENTITY () sigue siendo mucho más lento. Por lo tanto, parece que la única forma de hacer que IDENTITY () supere a NEWSEQUENTIALID () es preparar las filas para insertar en una tabla temporal y realizar las muchas inserciones como inserción por lotes utilizando esta tabla temporal. En general, no creo que hayamos encontrado una explicación al fenómeno, e IDENTITY () todavía parece ser más lento para la mayoría de los usos prácticos. ¿Alguien puede explicar esto?


44
Solo un pensamiento: ¿podría ser que se pueda generar un nuevo GUID sin involucrar a la tabla en absoluto, mientras que obtener el siguiente valor de identidad disponible introduce algún tipo de bloqueo temporalmente para garantizar que dos hilos / conexiones no obtengan el mismo valor? Solo estoy adivinando realmente. ¡Interesante pregunta!
persona enojada

44
¿Quién dice que lo hacen? Hay muchas pruebas de que no lo hacen, ver que el espacio en disco de Kimberly Tripp es barato, ¡ese NO es el punto! entrada de blog - que hace bastante una extensa revisión, y GUID siempre pierden con claridad aINT IDENTITY
marc_s

2
Bueno, el experimento anterior muestra lo contrario, y los resultados son repetibles.
someName

2
El uso IDENTITYno requiere un bloqueo de mesa. Conceptualmente, pude ver que podría esperar que tome MAX (id) + 1, pero en realidad se almacena el siguiente valor. En realidad, debería ser más rápido que encontrar el siguiente GUID.

44
Además, presumiblemente la columna de relleno para la tabla TestGuid2 debería ser CHAR (88) para que las filas tengan el mismo tamaño
Mitch Wheat

Respuestas:


19

Modifiqué el código de @Phil Sandler para eliminar el efecto de llamar a GETDATE () (puede haber efectos / interrupciones de hardware involucrados ??), e hice filas de la misma longitud.

[Ha habido varios artículos desde SQL Server 2000 relacionados con problemas de temporización y temporizadores de alta resolución, por lo que quería minimizar ese efecto].

En el modelo de recuperación simple con datos y archivos de registro de tamaño similar al requerido, aquí están los tiempos (en segundos): (actualizado con nuevos resultados basados ​​en el código exacto a continuación)

       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

El código usado:

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

Después de leer la investigación de @ Martin, volví a correr con el TOP sugerido (@num) en ambos casos, es decir

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

y aquí están los resultados de tiempo:

       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

¡No pude obtener el plan de ejecución real, ya que la consulta nunca regresó! Parece un error probable. (Ejecución de Microsoft SQL Server 2008 R2 (RTM) - 10.50.1600.1 (X64))


77
Ilustra claramente el elemento crítico de una buena evaluación comparativa: asegúrese de medir solo una cosa a la vez.
Aaronaught

¿Qué plan tienes aquí? ¿Tiene un SORToperador para los GUID?
Martin Smith

@ Martin: Hola, no revisé los planes (haciendo algunas cosas a la vez :)). Echaré un vistazo un poco más tarde ...
Mitch Wheat

@Mitch - ¿Algún comentario sobre esto? Sospecho que lo principal que está midiendo aquí es el tiempo necesario para clasificar las guías para inserciones grandes que, si bien es interesante, no responde a la pregunta original del OP, que era sobre dar una explicación de por qué las guías secuenciales funcionaron mejor que las columnas de identidad en un solo inserciones de fila en las pruebas del OP.
Martin Smith

2
@Mitch: aunque cuanto más lo pienso, menos entiendo por qué alguien querría usar de NEWSEQUENTIALIDtodos modos. Hará que el índice sea más profundo, usará un 20% más de páginas de datos en el caso del OP y solo se garantizará que aumente hasta que se reinicie la máquina, por lo que tiene muchas desventajas identity. ¡Parece que en este caso el Plan de consulta agrega otro innecesario!
Martin Smith

19

En una nueva base de datos en un modelo de recuperación simple con el archivo de datos de 1 GB y el archivo de registro a 3 GB (máquina portátil, ambos archivos en la misma unidad) y el intervalo de recuperación establecido en 100 minutos (para evitar que un punto de control sesgue los resultados) veo resultados similares para usted con una sola fila inserts.

Probé tres casos: para cada caso hice 20 lotes de inserción de 100,000 filas individualmente en las siguientes tablas. Los scripts completos se pueden encontrar en el historial de revisión de esta respuesta .

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

Para la tercera tabla, la prueba insertó filas con un Idvalor incremental , pero esto se calculó incrementando el valor de una variable en un bucle.

Promediar el tiempo empleado en los 20 lotes dio los siguientes resultados.

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

Conclusión

Por lo tanto, definitivamente parece ser una sobrecarga del identityproceso de creación responsable de los resultados. Para el entero incremental autocalculado, los resultados están mucho más en línea con lo que se esperaría ver al considerar solo el costo IO.

Cuando pongo el código de inserción descrito anteriormente en los procedimientos almacenados y lo reviso, sys.dm_exec_procedure_statsse obtienen los siguientes resultados

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

Entonces, en esos resultados total_worker_timees aproximadamente un 30% más alto. Esto representa

Cantidad total de tiempo de CPU, en microsegundos, que consumieron las ejecuciones de este procedimiento almacenado desde que se compiló.

Por lo tanto, simplemente parece que el código que genera el IDENTITYvalor es más intensivo en CPU que el que genera el NEWSEQUENTIALID()(La diferencia entre las 2 cifras es 10231308 que promedia aproximadamente 5 µs por inserto) y que para esta definición de tabla este costo fijo de CPU era lo suficientemente alto como para superar las lecturas y escrituras lógicas adicionales incurridas debido al mayor ancho de la clave. (Nota: Itzik Ben Gan hizo pruebas similares aquí y encontró una penalización de 2 µs por inserción)

Entonces, ¿por qué se IDENTITYnecesita más CPU que UuidCreateSequential?

Creo que esto se explica en este artículo . Por cada décimo identityvalor generado, SQL Server tiene que escribir el cambio en las tablas del sistema en el disco

¿Qué pasa con los insertos MultiRow?

Cuando se insertaron las 100.000 filas en una sola declaración, descubrí que la diferencia desapareció y que tal vez sea un ligero beneficio para el GUIDcaso, pero que ni de lejos resulta tan claro. El promedio de 20 lotes en mi prueba fue

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

La razón por la que no tiene la penalidad aparente en el código de Phil y el primer conjunto de resultados de Mitch es porque sucedió que el código que usé para hacer la inserción de varias filas utilizada SELECT TOP (@NumRows). Esto evitó que el optimizador estimara correctamente el número de filas que se insertarán.

Esto parece ser beneficioso ya que hay un cierto punto de inflexión en el que agregará una operación de clasificación adicional para los (¡supuestamente secuenciales!) GUIDS.

GUID Sort

Esta operación de clasificación no es necesaria del texto explicativo en BOL .

Crea un GUID que es mayor que cualquier GUID generado previamente por esta función en una computadora específica desde que se inició Windows. Después de reiniciar Windows, el GUID puede comenzar nuevamente desde un rango inferior, pero aún es globalmente único.

Por lo tanto, me pareció un error o falta de optimización que SQL Server no reconoce que la salida del escalar de cómputo ya estará ordenada previamente, como aparentemente ya lo hace para la identitycolumna. ( Editar , informé esto y el problema de clasificación innecesario ahora está solucionado en Denali )


No es que tenga un gran impacto, pero solo en aras de la claridad, el número que citó Denny, 20 valores de identidad en caché, es incorrecto, debería ser 10.
Aaron Bertrand

@AaronBertrand - Gracias. Ese artículo que vinculó es más informativo.
Martin Smith

8

Muy simple: con GUID, es más barato generar el siguiente número en la línea que IDENTIDAD (el valor actual del GUID no tiene que almacenarse, la IDENTIDAD tiene que ser). Esto es cierto incluso para NEWSEQUENTIALGUID.

Podría hacer que la prueba sea más justa y usar un SECUENCIADOR con un CACHÉ grande, que es más barato que IDENTIDAD.

Pero como dice MR, hay algunas ventajas importantes para los GUID. De hecho, son MUCHO más escalables que las columnas IDENTITY (pero solo si NO son secuenciales).

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


Creo que te perdiste que están usando guías secuenciales.
Martin Smith

Martin: el argumento también es cierto para el GUID secuencial. IDENTITY debe almacenarse (para volver a su valor anterior después de un reinicio), el GUID secuencial no tiene esta limitación.
Thomas Kejser

2
Sí, me di cuenta después de mi comentario de que estaba hablando de almacenar persistentemente en lugar de almacenarlo en la memoria. Sin embargo, 2012 también usa un caché IDENTITY. por lo tanto, quejas aquí
Martin Smith

4

Estoy fascinado por este tipo de preguntas. ¿Por qué tuviste que publicarlo un viernes por la noche? :)

Creo que incluso si su prueba SOLO está destinada a medir el rendimiento de INSERT, usted (puede) haber introducido una serie de factores que podrían ser engañosos (bucle, una transacción de larga duración, etc.)

No estoy completamente convencido de que mi versión pruebe algo, pero la identidad funciona mejor que los GUID (3.2 segundos frente a 6.8 segundos en una PC doméstica):

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

El otro factor que nadie ha mencionado es el modelo de recuperación de la base de datos y el crecimiento de los archivos de registro ...
Mitch Wheat

@Mitch en una nueva base de datos en un modelo de recuperación simple con datos y archivos de registro de tamaño similar al requerido, obtengo resultados similares al OP.
Martin Smith,

Acabo de obtener tiempos de 2.560 segundos para Identity y 3.666 segundos para Guid (en un modelo de recuperación simple con datos y archivo de registro de tamaño muy superior al requerido)
Mitch Wheat

@Mitch - ¿En el código del OP con todo en la misma transacción o en el código de Phil?
Martin Smith

en este código de carteles, por eso estoy comentando aquí. También publiqué el código que usé ...
Mitch Wheat

3

Ejecuté su script de muestra varias veces haciendo algunos ajustes al recuento y tamaño del lote (y muchas gracias por proporcionarlo).

Primero diré que solo está midiendo una vez el aspecto del rendimiento de las teclas: la INSERTvelocidad. Entonces, a menos que esté específicamente preocupado solo por obtener datos en las tablas lo más rápido posible, hay mucho más para este animal.

Mis hallazgos fueron en general similares a los suyos. Sin embargo, mencionaría que la variación en la INSERTvelocidad entre GUIDy IDENTITY(int) es ligeramente mayor con GUIDque con IDENTITY- tal vez +/- 10% entre carreras. Los lotes que usaron IDENTITYvariaron menos del 2 - 3% cada vez.

También para tener en cuenta, mi caja de prueba es claramente menos potente que la suya, por lo que tuve que usar recuentos de filas más pequeños.


Cuando el PK es un GUID, ¿es posible que el motor no utilice un índice sino un algoritmo de hash para determinar la ubicación física del registro correspondiente? Las inserciones en una tabla dispersa con claves primarias hash siempre son más rápidas que las inserciones en una tabla con un índice en la clave primaria debido a la ausencia de sobrecarga del índice. Es solo una pregunta: no me rechacen si la respuesta es No. Solo proporcione el enlace a la autoridad.

1

Voy a referirme a otra conv en stackoverflow para este mismo tema: https://stackoverflow.com/questions/170346/what-are-the-performance-improvement-of-sequential-guid-over-standard-guid

Una cosa que sí sé es que tener GUID secuenciales es que el uso del índice es mejor debido al muy poco movimiento de las hojas y, por lo tanto, reduce la búsqueda de HD. Creo que debido a esto, las inserciones también serían más rápidas, ya que no tiene que distribuir las claves en una gran cantidad de páginas.

Mi experiencia personal es que cuando implementa una gran base de datos de alto tráfico, es mejor usar GUID, porque la hace mucho más escalable para la integración con otros sistemas. Eso va para la replicación, específicamente, y los límites int / bigint ... no es que te quedes sin bigints, pero eventualmente lo harás, y volverás en ciclo.


1
No te quedas sin BIGINT, nunca ... Mira esto: sqlmag.com/blog/it-possible-run-out-bigint-values
Thomas Kejser
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.