SQL Server

GUID vs INT como Primary Key en SQL Server

INT es compacto y secuencial; GUID se genera en cualquier parte. Su efecto en índices y fragmentación, y por qué UUID v7 no basta en SQL Server.

Por Leyton Manuel Espitia Diaz 5 min de lectura

Fragmento de SQL que compara una clave INT IDENTITY con un uniqueidentifier con NEWSEQUENTIALID

Es una de las discusiones más repetidas en cualquier equipo que modela una base de datos: ¿la clave primaria es un int autoincremental o un GUID? Las dos opciones funcionan, y las dos tienen defensores con buenos argumentos. La respuesta depende de dónde se generan los identificadores y de qué tan grande va a ser la tabla.

INT / BIGINT con IDENTITY

CREATE TABLE Sales.Orders (
    OrderId int IDENTITY(1,1) NOT NULL CONSTRAINT PK_Orders PRIMARY KEY CLUSTERED,
    ...
);

A favor:

  • 4 bytes (int) u 8 (bigint). Como vimos en índices clustered y nonclustered, la clave del clustered se copia en todos los índices nonclustered: una clave pequeña hace todos los índices más pequeños.
  • Secuencial: las filas nuevas siempre van al final del índice, sin divisiones de página.
  • Legible: "el pedido 10482" se puede dictar por teléfono.

En contra:

  • Lo genera la base de datos. Para conocer el ID hay que insertar primero. Crear un pedido con sus líneas exige guardar el pedido, leer el ID y luego guardar las líneas (EF Core lo hace por ti, pero el viaje existe).
  • Es predecible. Si la API expone /api/orders/10482, cualquiera puede probar con 10483. Eso no es una vulnerabilidad por sí solo (para eso existe la autorización), pero revela información: cuántos pedidos tiene el sistema y a qué ritmo crece.
  • Difícil de combinar entre bases. Fusionar datos de dos bases, o generar registros sin conexión desde una aplicación móvil, produce colisiones.
  • int tiene un límite de unos 2.100 millones. En tablas de eventos o logs se alcanza más rápido de lo que parece; para ellas, bigint desde el principio.

GUID (uniqueidentifier)

CREATE TABLE Sales.Orders (
    OrderId uniqueidentifier NOT NULL
        CONSTRAINT PK_Orders PRIMARY KEY CLUSTERED
        CONSTRAINT DF_Orders_OrderId DEFAULT NEWSEQUENTIALID(),
    ...
);

A favor:

  • Se genera en cualquier parte: la aplicación, un cliente móvil sin conexión, otro sistema. El ID existe antes de tocar la base de datos.
  • Único entre bases y sistemas: se pueden fusionar datos sin colisiones.
  • No es enumerable: no revela cantidades ni permite adivinar el siguiente.

En contra:

  • 16 bytes: cuatro veces un int, en la clave y en cada índice nonclustered.
  • Fragmentación si los valores son aleatorios y el índice es clustered.
  • Incómodo para personas: nadie dicta 3f2504e0-4f89-11d3-9a0c-0305e82c3301 por teléfono.

El problema real: GUID aleatorio como índice clustered

Con NEWID() o Guid.NewGuid(), cada valor nuevo cae en una posición aleatoria del índice. SQL Server tiene que insertar en medio de páginas que ya están llenas, dividirlas y dejar espacio libre. El resultado:

  • Más escrituras por cada inserción.
  • Páginas medio vacías: el índice ocupa más espacio y más memoria.
  • Mucha más fragmentación que con un valor secuencial.

En una tabla con pocas inserciones no importa. En una tabla con miles de inserciones por minuto, sí.

Opciones para tener GUID sin ese costo

NEWSEQUENTIALID() en la base de datos

Genera GUIDs crecientes en el servidor. Resuelve la fragmentación, pero devuelve la desventaja de que el ID lo genera la base de datos, y la secuencia puede reiniciar en otro punto después de un reinicio de Windows.

UUID v7 desde .NET 9, con un detalle importante

.NET 9 agregó Guid.CreateVersion7(): un GUID que empieza con una marca de tiempo, así que los valores generados después son mayores. Parece la solución perfecta, pero SQL Server no ordena uniqueidentifier byte por byte de izquierda a derecha: compara primero los últimos 6 bytes. Un UUID v7 es secuencial para .NET y para otros motores, pero en SQL Server no queda ordenado como esperas, y la fragmentación vuelve a aparecer.

Para SQL Server, la alternativa es generar en la aplicación GUIDs "secuenciales al estilo SQL Server", con la parte que cambia en los bytes que SQL Server compara primero. Algunas bibliotecas lo hacen (EF Core usa un generador así por defecto para las claves Guid en SQL Server).

Separar la clave clustered del identificador público

Una solución muy práctica cuando se necesita lo mejor de los dos:

CREATE TABLE Sales.Orders (
    OrderId bigint IDENTITY(1,1) NOT NULL CONSTRAINT PK_Orders PRIMARY KEY CLUSTERED,
    PublicId uniqueidentifier NOT NULL CONSTRAINT DF_Orders_PublicId DEFAULT NEWID(),
    ...
    CONSTRAINT UQ_Orders_PublicId UNIQUE NONCLUSTERED (PublicId)
);
  • Internamente, joins e índices usan el bigint compacto y secuencial.
  • La API expone PublicId, que no es enumerable.
  • El índice sobre PublicId se fragmenta, pero es uno solo y pequeño.

El costo: dos identificadores por entidad y cuidado para no exponer nunca el interno.

El trade-off

INT / BIGINT GUID aleatorio GUID secuencial INT interno + GUID público
Tamaño en índices Mínimo Grande Grande Mínimo
Fragmentación Ninguna Alta Baja Baja (solo índice público)
Generar sin la base de datos No Sí Depende El público, sí
Enumerable desde fuera Sí No Parcialmente No
Complejidad Mínima Baja Media Media

Cuándo usar cada uno

  • INT / BIGINT: sistemas con una sola base de datos, IDs que no se exponen o se protegen con autorización, y tablas con muchas escrituras.
  • GUID secuencial: sistemas distribuidos, sincronización sin conexión, datos que se fusionan entre bases.
  • INT interno + GUID público: APIs públicas donde no quieres IDs enumerables pero sí índices eficientes.

Cuándo no preocuparse

En tablas pequeñas o con pocas inserciones (catálogos, configuración, usuarios de un sistema interno), la diferencia de rendimiento es irrelevante. Elige lo que sea más cómodo para el sistema y ahorra la discusión para las tablas que reciben miles de filas por minuto.

Conclusión

No hay un ganador universal. int es compacto y rápido; GUID es flexible y no revela nada. Si eliges GUID como clave clustered en SQL Server, asegúrate de que sea secuencial para SQL Server, no solo para .NET. Y si necesitas lo mejor de los dos, separar el identificador interno del público es una solución probada y simple de mantener.

Leyton Manuel Espitia Diaz

Escrito por

Leyton Manuel Espitia Diaz

CEO y fundador de Nakanaba

Desarrollador de software y fundador de Nakanaba. Lidera la arquitectura de Vitrin: backend en .NET, aplicaciones en Angular y .NET MAUI, SQL Server y servicios en Cloudflare.

¿Tu base de datos se volvió lenta?

Revisamos consultas, índices y modelo de datos para que tu sistema responda rápido aunque los datos sigan creciendo.

Ver desarrollo a medida