UUID v1 vs v4 vs v7: elegir una clave primaria de BD

Publicado el 2026-04-13 8 min de lectura

Resumen (TL;DR)

Por decirlo sin rodeos: usar UUID v4 como clave primaria de una tabla con inserts calientes es un disparo al pie. Revisé hace poco una carga de trabajo PostgreSQL 16 donde events.id tenía como valor por defecto gen_random_uuid() (un v4), y cada INSERT aterrizaba en una hoja aleatoria del B-tree, enfriando shared_buffers y arrastrando la fragmentación del índice a un dígito alto según pgstattuple. Cambiar el valor por defecto de la columna a un generador v7 —mismo tipo uuid de 16 bytes, mismo índice— redujo la latencia media de INSERT a aproximadamente un tercio de la anterior, y los números de fragmentación se estabilizaron. Elegir una versión de UUID es una decisión de diseño de base de datos, no de criptografía. Un UUID es un valor de 128 bits escrito como dígitos hexadecimales 8-4-4-4-12, con cuatro bits fijos como versión y dos o tres bits fijos como variante. La versión 1 estampa tiempo y un identificador de nodo (históricamente una dirección MAC) en esos 128 bits; aproximadamente ordenable, pero filtra el host. La versión 4 rellena los bits no reservados con datos aleatorios: fuerte imposibilidad de adivinar, pero sin orden, así que las inserciones aterrizan en posiciones arbitrarias del índice. La versión 7, formalizada en RFC 9562 (2024), coloca una marca de tiempo Unix-millisegundo en los bits altos y una cola aleatoria, combinando la seguridad de v4 con la localidad de índice de v1. Para una API pública donde la aleatoriedad opaca realmente importa, v4 sigue siendo el valor por defecto. Casi en todos los demás casos, v7 es la mejor clave primaria. v1 es legacy: aceptado por las bases de datos, pero no debería ser una elección nueva.

Antecedentes y conceptos

Un UUID tiene 128 bits. Por convención se imprime como 32 dígitos hexadecimales agrupados con guiones: xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx. El dígito M contiene la versión (1, 4, 7, etc.) y los bits altos de N contienen la variante. Todo lo demás es específico de la versión.

La versión 1 fue diseñada para la unicidad a través de máquinas y tiempo. Su marca de tiempo de 60 bits cuenta intervalos de 100 nanosegundos desde 1582-10-15, un campo de secuencia de reloj maneja retrocesos del reloj, y un ID de nodo de 48 bits era originalmente la dirección MAC de red. La consecuencia de privacidad es directa: un UUID v1 acuñado en tu portátil codifica la MAC de tu portátil, y un uuid -d de una línea lo invierte. Las librerías modernas a veces aleatorizan el ID de nodo para evitar la filtración, pero muchas siguen la regla original.

La versión 4 son 122 bits de aleatoriedad con 6 bits fijos para versión y variante. Asume un RNG fuerte (el crypto.randomUUID() del navegador o gen_random_uuid() de PostgreSQL). Las colisiones son efectivamente imposibles a cualquier escala realista; la cota del cumpleaños da aproximadamente una probabilidad de uno en un billón tras generar unos mil millones de v4. La desventaja es que v4 sucesivos están descorrelacionados, así que insertarlos en un índice toca páginas aleatorias, perjudicando la localidad de caché y la amplificación de escritura (WAL más full-page writes en Postgres).

La versión 7 es parte de RFC 9562, publicada en 2024, que obsoletó a RFC 4122 y añadió las versiones 6, 7 y 8. Un UUID v7 almacena una marca de tiempo Unix en milisegundos de 48 bits en sus bits altos, seguida del tag de versión, un campo rand_a pequeño, el tag de variante, y una cola rand_b de 62 bits. El efecto práctico es que los valores v7 generados dentro del mismo milisegundo son adyacentes en orden, y los valores a través de milisegundos se ordenan cronológicamente. La cola aleatoria aún proporciona entropía más que suficiente para unicidad dentro de un milisegundo. PostgreSQL 18 trae uuidv7() nativo; en versiones anteriores puedes usar la extensión pg_uuidv7 o generarlos en la capa de aplicación (Node uuid 9.x, Python uuid6).

Los bits de variante importan porque distinguen la familia RFC 9562 / 4122 de los UUID legacy de Microsoft y Apollo. Para esta guía, asume la variante RFC (el primer dígito hex del grupo N es 8, 9, a o b).

El formato de almacenamiento es una preocupación aparte. El tipo nativo uuid de PostgreSQL 16 almacena el valor como 16 bytes; MySQL normalmente usa BINARY(16) o CHAR(36): la forma de cadena duplica el almacenamiento y hace que las comparaciones sean carácter a carácter en vez de byte a byte. La elección de versión y la de formato de almacenamiento interactúan: ordenar v7 como valor binario es barato y correcto, ordenarlo como cadena hex es correcto pero más lento, y ordenar v4 no tiene sentido sin importar el almacenamiento.

Comparación y datos

Propiedadv1v4v7
Entrada de generaciónTimestamp + secuencia de reloj + ID de nodo122 bits aleatoriosTimestamp Unix ms de 48 bits + cola aleatoria
PrivacidadFiltra el ID de nodo (a menudo la MAC)Sin información de host o tiempoFiltra tiempo de creación en ms, sin host
Ordenable por tiempoSí (pero orden de bytes ≠ orden temporal sin reorganización)NoSí: orden lexicográfico coincide con orden cronológico
Localidad de índiceModeradaPobre (inserts aleatorios en todo el B-tree)Buena (casi monótona)
Caso de uso típicoSistemas legacy, algunos IDs COM/WindowsIDs de API pública, tokens de sesión, saltsLogs de eventos, inserts de alto volumen, tablas paginadas por tiempo
EntropíaBaja (la mayoría de bits son tiempo/nodo)Alta (unos 122 bits)Cola alta (unos 74 bits), baja colisión dentro de ms

Un modelo mental aproximado: v4 maximiza la imposibilidad de adivinar a costa del comportamiento del índice, v7 mantiene suficiente imposibilidad de adivinar para la mayoría de aplicaciones mientras restaura el patrón de insertar al final que las bases de datos aman, y v1 es un artefacto histórico a reconocer pero no a elegir.

Escenarios reales

Escenario 1 — Log de eventos con muchas adiciones. Esta es exactamente la carga del anecdotario inicial. Una tabla que ingiere millones de filas al día y normalmente se consulta como “últimas 24 horas, ordenado por tiempo” se beneficia directamente de v7. Las nuevas filas aterrizan al final del índice de clave primaria, así que las páginas calientes permanecen calientes y los range scans sobre rangos temporales mapean a segmentos contiguos del índice. Migrar de v4 a v7 aquí suele reducir la latencia de escritura y la fragmentación del índice sin cambiar ningún código de consulta; típicamente un cambio de valor por defecto de columna de una sola línea.

Escenario 2 — IDs públicos de cara al usuario. Enlaces compartidos como /orders/{id} deben ser imposibles de adivinar para que los visitantes no puedan enumerar pedidos de otros usuarios. v4 es el valor por defecto seguro. Si también quieres los beneficios de v7, ten en cuenta que un v7 revela su marca de tiempo de creación al milisegundo, lo que puede estar bien para pedidos pero podría filtrar señales de negocio (p. ej., volumen exacto de checkout por minuto) en contextos más sensibles. El compromiso que he recomendado a equipos es un patrón de doble ID: v7 internamente para la clave primaria, y un v4 separado o slug aleatorio corto expuesto al mundo externo. Mantienes el comportamiento del índice sin filtrar timing externamente.

Escenario 3 — Sistemas multi-región o sharded. El prefijo de timestamp de v7 significa que dos regiones generando UUID en el mismo milisegundo se intercalarán limpiamente por tiempo, pero dentro de un milisegundo no hay garantía de orden entre regiones. Si necesitas un orden más estricto entre regiones, ULID (timestamp de 48 bits + aleatoriedad de 80 bits codificada como Crockford Base32) tiene propiedades casi idénticas y una forma textual más compacta de 26 caracteres. Los IDs estilo Snowflake van más allá al incluir un ID explícito de máquina para orden por nodo (el diseño original de Twitter y la variante de Discord son ambos de 64 bits), a costa de requerir coordinación para asignar esos IDs.

Errores comunes

“Los UUID siempre son lentos en bases de datos.” Son más lentos que un int de 4 bytes en términos brutos, pero el costo real viene de la fragmentación por inserts aleatorios en índices B-tree, que v7 elimina en gran medida. Almacenar UUID como 16 bytes en vez de una cadena de 36 caracteres reduce el tamaño del índice a la mitad y acelera las comparaciones. Muchos benchmarks de “los UUID son lentos” son en realidad “v4 almacenado como CHAR(36) en MySQL es lento”.

“v4 es la única versión segura.” La cola aleatoria de v7 sigue siendo un gran pool de entropía, y para la mayoría de aplicaciones —referencias de sesión, IDs de API— es lo suficientemente imposible de adivinar como para que un atacante no pueda enumerarlos. Donde la adivinabilidad importa es el prefijo de timestamp: v7 revela cuándo se creó la fila. Si eso es aceptable (y normalmente lo es), v7 es una elección razonable incluso para IDs externos.

“Los UUID deben almacenarse como cadenas.” La forma de cadena son 36 caracteres (32 hex más 4 guiones) vs 16 bytes binarios. Binario es más compacto y se ordena correctamente como bytes, lo que importa para v1 con su orden de bytes no monótono y para v7 donde el orden de bytes debería alinearse con el tiempo. El tipo uuid de PostgreSQL ya almacena el valor como 16 bytes, así que no hay nada extra en qué pensar ahí.

“La filtración de MAC de v1 no importa porque nadie mira.” La MAC en un UUID v1 es una inversión conocida: uuid -d y cualquier herramienta forense la extraen. Si tus UUID aparecen en URLs, tickets de soporte o dumps de logs compartidos con terceros, eso es una divulgación de información real.

Lista de verificación

  1. ¿Aparecerá este UUID en un índice de base de datos que recibe inserts frecuentes? Prefiere v7. Recurre a v4 solo si la imposibilidad de adivinar el tiempo de creación es esencial.
  2. ¿El UUID es visible para usuarios finales o partners? Cualquier versión funciona; solo confirma que la filtración de timestamp de v7 es aceptable para el contexto.
  3. ¿Estás en Postgres? Almacena como uuid (16 bytes). En MySQL, usa BINARY(16) a menos que la compatibilidad de cadena supere el costo de tamaño.
  4. ¿Necesitas garantías de orden entre múltiples generadores? v7 solo no basta; considera ULID con el mismo prefijo de tiempo, o un ID estilo Snowflake con un ID explícito de máquina.
  5. ¿Hay v1 aún en el código? Documenta la filtración de MAC y planea una migración cuando el esquema lo permita.
  6. ¿Estás generando UUID del lado del cliente? Usa una librería que llame a un RNG criptográficamente fuerte (crypto.randomUUID() en navegadores modernos para v4; las librerías v7 normalmente envuelven el mismo RNG).

Herramienta relacionada

El generador de UUID de Patrache Studio produce UUID v4 y v7 localmente, así que los valores generados nunca se registran en un servicio de terceros. Los UUID casi siempre viajan dentro de payloads JSON; Formateo, validación y esquema JSON en la práctica cubre los patrones de esquema que mantienen esos IDs bien tipados al moverse entre servicios. Cuando necesitas una forma textual compacta —por ejemplo, un ID corto de 22 caracteres derivado de un UUID de 16 bytes— las reglas en Base64 y codificación URL: propósito, trampas, uso correcto explican por qué Base64URL en vez de Base64 estándar es la elección correcta.

Referencias