- UUID v4 tiene un alto grado de aleatoriedad, lo que provoca ineficiencia en los índices y exceso de I/O; al usarse como clave primaria en PostgreSQL, causa una degradación del rendimiento
- Debido a las inserciones aleatorias, se vuelven frecuentes las divisiones de página (page splits) y la fragmentación de índices, lo que aumenta el tamaño del WAL y genera latencia de escritura
- Un UUID ocupa 16 bytes, el doble que un
bigint, lo que reduce la tasa de aciertos de caché y lleva a desperdicio de memoria
- A menudo se confunde con un identificador de seguridad, pero según la RFC 4122, UUID no es un mecanismo de seguridad para evitar adivinación
- Para nuevas bases de datos se recomienda usar claves basadas en secuencias enteras y, si no hay alternativa, optar por UUID v7 ordenado por tiempo
Problemas de rendimiento de UUID v4
- Las bases de datos en PostgreSQL que usan UUID v4 como clave primaria han mostrado de forma consistente durante los últimos 10 años degradación del rendimiento y exceso de I/O
- UUID v4 genera 122 bits aleatorios, por lo que no permite ordenamiento eficiente en índices
- Como las inserciones no se guardan en páginas secuenciales, se produce acceso aleatorio y también se requieren búsquedas ineficientes en actualizaciones y eliminaciones
- Los índices B-Tree asumen datos ordenados, pero UUID v4 no tiene orden, así que la eficiencia de inserción es baja
- Cada inserción se escribe en una página arbitraria, lo que provoca frecuentes divisiones de páginas intermedias
- Esto genera latencia de escritura y aumento del WAL
Estructura de UUID y alternativas
- UUID es un identificador de 128 bits (16 bytes), y en PostgreSQL se almacena como tipo
uuid binario
- UUID v4 se basa en bits aleatorios, mientras que UUID v7 incluye un timestamp en los primeros 48 bits, lo que mejora la eficiencia de los índices
- PostgreSQL 18 (previsto para 2025) incluirá soporte nativo para UUID v7
- UUID v7 permite ordenamiento cronológico, mejorando la densidad de páginas y la eficiencia de caché
Motivos para elegir UUID y sus límites
- UUID se usa cuando se necesita generación de identificadores sin colisiones en entornos con múltiples clientes o microservicios
- Ejemplo: generar IDs al mismo tiempo en varias instancias de base de datos
- Sin embargo, la RFC 4122 indica que “no se debe asumir que UUID es difícil de adivinar”, por lo que no es adecuado como identificador de seguridad
- La probabilidad de colisión llega al 50% al generar 2.71×10¹⁸ UUID, así que en la práctica las colisiones son poco probables, pero el costo en rendimiento es alto
Ineficiencia de espacio e I/O de UUID
- UUID ocupa el doble que
bigint (8 bytes) y cuatro veces más que int (4 bytes)
- En tablas grandes, esto se traduce en más almacenamiento y en mayores tiempos de respaldo y restauración
- Resultados del experimento de densidad de páginas de índice
- índice
integer: 97.64%
- índice UUID v4: 79.06%
- índice UUID v7: 90.09%
- En una prueba de Cybertec se confirmó que, al consultar un índice UUID v4, hubo 8.5 millones de accesos adicionales a páginas y un 31229% más de I/O
- Bajo las mismas condiciones, el índice
bigint tuvo 27,332 accesos a búfer, mientras que UUID v4 tuvo 8,562,960
Impacto en caché y memoria
- UUID, por su distribución aleatoria, reduce la tasa de aciertos de la caché de búfer (cache hit ratio)
- Hay que cargar más páginas en caché, y las páginas necesarias se expulsan (eviction) con frecuencia
- La menor eficiencia de caché provoca latencia en las consultas y mayor uso de memoria
- Para mantener el rendimiento se recomienda usar reconstrucción periódica de índices (
REINDEX CONCURRENTLY) o pg_repack
Cómo mitigar el impacto en el rendimiento
- Ampliar memoria: se recomienda contar con RAM equivalente a 4 veces el tamaño de la base de datos (ejemplo: DB de 25 GB → 128 GB de memoria)
- Ajustar
work_mem: asignar más memoria a operaciones de ordenamiento puede mejorar el rendimiento
- En entornos Rails, se puede usar la configuración
implicit_order_column para ordenar por un campo como created_at en lugar de UUID
- Con el comando
CLUSTER se puede reorganizar la tabla según un campo ordenable, aunque requiere un bloqueo exclusivo
Recomendación: claves enteras y secuencias
- Para bases de datos nuevas se recomienda usar claves basadas en secuencias enteras
integer (4 bytes) permite unos 2 mil millones de valores, y bigint (8 bytes) ofrece muchos más valores únicos
- Para la mayoría de las aplicaciones de negocio,
integer es suficiente; en servicios de gran escala, conviene usar bigint
- Una alternativa realista a UUID v4 es usar UUID v7 o la extensión
sequential_uuids
Resumen
- UUID v4 provoca ineficiencia en índices, alto I/O y baja eficiencia de caché debido a su aleatoriedad
- No puede usarse como identificador de seguridad y además implica desperdicio de espacio
- Las claves de secuencia enteras son más adecuadas para la mayoría de las aplicaciones
- Si es indispensable usar UUID, se debe elegir UUID v7 ordenado por tiempo
- Conviene evitar usar
gen_random_uuid() como clave primaria en PostgreSQL
1 comentarios
Comentarios en Hacker News
Este es un ejemplo típico de optimización prematura
Meter datos dentro de un identificador permanente es algo casi prohibido en la gestión de datos
Si pones la fecha de nacimiento en un ID, como en el número de identificación noruego, luego pueden surgir casos de inmigrantes cuya fecha de nacimiento se registró mal, o problemas porque hay demasiadas personas con fecha 1 de enero y se agotan los números
En la época de los catálogos en fichas tenía sentido mezclar datos e identificadores para reducir el costo de búsqueda, pero hoy, con bases de datos potentes, ya no hace falta
El problema fue usar el 1 de enero para cumpleaños desconocidos, no el hecho de poner la fecha dentro de la clave
Si hubieran usado un valor no válido como 00 o 99, no habría habido conflicto
Poner una marca de tiempo en un UUID no busca darle significado, sino optimizar el rendimiento
Las claves que aumentan en orden temporal reducen el costo de reescritura del B-tree y mejoran el rendimiento de inserción en la BD
“No metas datos en un identificador permanente” es solo una regla general; según el caso, se puede aceptar cierto trade-off
Por ejemplo, si usas un hash md5 como UUID para construir un índice, habrá fragmentación, pero en un nivel manejable
Elegir entre UUID aleatorio y UUID basado en tiempo puede generar diferencias de rendimiento no de milisegundos, sino de segundos
En bases de datos grandes, el sharding y la distribución son imprescindibles, así que UUID puede funcionar mejor que autoincrement
Si el formato es DDMMYYXXXXX, debería cubrir hasta 100 mil personas; me pregunto si de verdad puede concentrarse tanta gente en esa fecha
Probablemente sería una situación especial, como una llegada masiva de refugiados en un año concreto
No se debería usar UUID como si fuera un token de seguridad
Es peligroso usarlo como mecanismo de seguridad solo porque es difícil de adivinar
El propósito de un valor aleatorio no es solo evitar la adivinación, sino también ocultar la relación entre IDs consecutivos
Según el tipo de BD, la estrategia de PK cambia por completo
En Postgres, una PK aleatoria es ineficiente, pero en bases distribuidas como Cockroach o Spanner, una clave monótonamente creciente puede causar el problema de hot shards
UUIDv7 tiene bits altos ordenables y bits bajos aleatorios, así que permite obtener distribución entre nodos y eficiencia de almacenamiento local al mismo tiempo
En una BD típica no shardeda, las claves aleatorias provocan fragmentación del B-tree
Pero si hay muchas consultas por rango, una clave aleatoria sale perdiendo
Al final hay que elegir según las características de la carga de trabajo
El artículo señala bien las desventajas de usar UUIDv4 como PK, pero el método de ofuscación de enteros que propone no parece apto para un servicio real
En una BD pequeña, UUIDv7 parece un compromiso razonable
Porque no quiero que se exponga la hora de generación
Mientras la aleatoriedad de UUIDv4 no cause problemas de rendimiento por el volumen de datos, v4 es la opción más segura
Hay una pequeña filtración de información, pero a nivel operativo es suficientemente ambigua
Por ejemplo, convertirla con AES-128 y luego codificarla en base64 para que parezca un ID de video de YouTube
Veo muchas empresas durante procesos de due diligence técnica, y la posibilidad de hacer sharding rápido es clave para el crecimiento de una empresa
Si todas las tablas usan UUID, puedes escalar en el momento de shardear sin cambiar la estructura
Eso da una ventaja de escalabilidad mucho mayor que la pequeña pérdida de espacio y tiempo
Al final, como el modelo de datos era complejo, la migración en sí fue difícil independientemente de si había UUID o no
En nuestra app ciframos la PK entera para que parezca un UUID
Si se expone un ID secuencial, se puede estimar la cantidad de clientes o lanzar un ataque de diccionario
Con IDs cifrados, si falla el descifrado se puede detectar de inmediato un intento de escaneo
Decir “2 mil millones son suficientes” es peligroso
Todo DBA tiene al menos una historia de pesadilla que empezó con una decisión así
El artículo dice que “los valores aleatorios se ordenan de forma ineficiente”, pero en realidad el ordenamiento por bytes sí es posible
Lo que pasa es que, como las claves aleatorias no se insertan en secuencia, el B-tree se rebalancea con frecuencia y eso degrada el rendimiento
La PK entera hacía que el índice cupiera bien en memoria, mientras que UUIDv4 requería más accesos a páginas y aumentaba la latencia
Este artículo parece otro caso de optimización prematura donde la solución apareció antes que el problema
UUIDv4 está perfectamente bien en la mayoría de los casos
Los problemas de rendimiento deberían evaluarse cuando realmente aparezcan
En resumen, en Postgres UUIDv7 muestra un rendimiento ligeramente mejor que v4
En versiones recientes ya se puede usar UUIDv7 sin plugins
uuidv7(), aunque todavía no está claro si las extensiones ofrecen más funcionalidades