¿Cómo eliges una estrategia de clave primaria: UUID, auto-incremental o un ID distribuido estilo Snowflake?
La opción 'obvia' — un entero auto-incremental — se rompe en silencio en cuanto shardeas, replicas entre regiones o necesitas generar un ID antes de que la fila toque la base de datos.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Cómo eliges una estrategia de clave primaria: UUID, auto-incremental o un ID distribuido estilo Snowflake?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Tres opciones reales, tres garantías distintas
- Auto-incremental / serial (
SERIAL,IDENTITY,AUTO_INCREMENT) — la base de datos asigna el siguiente entero en secuencia. - UUID — un valor de 128 bits, casi siempre generado de forma aleatoria (v4) o, cada vez más, ordenado por tiempo (v7).
- ID estilo Snowflake — un entero de 64 bits ensamblado a partir de un timestamp, un identificador de máquina/worker y un contador de secuencia por milisegundo, popularizado por el servicio Snowflake de Twitter y reutilizado (con variaciones) por Discord, Instagram y Sonyflake de Sony.
#Auto-incremental: simple, hasta que necesitas más de un escritor
CREATE TABLE orders (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,customer_id BIGINT NOT NULL,total_cents INTEGER NOT NULL);
Fortalezas: compacto (8 bytes), monótonamente creciente (excelente para la localidad de inserción del B-tree — las filas nuevas siempre se agregan en el extremo derecho del índice en lugar de dividir páginas al azar), trivialmente ordenable por orden de creación, y amigable para humanos en logs y URLs.
Debilidades: requiere un único punto de coordinación para repartir el siguiente valor, que es exactamente lo que no tienes en cuanto shardeas entre varias instancias de base de datos (dos shards generando independientemente id=4501 es una colisión esperando a fusionarse). También es secuencialmente adivinable/enumerable — /orders/4502 implica trivialmente que existe /orders/4501, lo cual es una superficie de divulgación de información y ataque de enumeración a menos que deliberadamente no lo expongas. Y no puede generarse en la aplicación antes de un insert — no tienes un ID para referenciar en registros relacionados o para devolver a un cliente hasta que la base de datos te lo entregue.
#UUID: sin necesidad de coordinación, a un costo real
CREATE TABLE orders (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- UUID v4, totalmente aleatoriocustomer_id BIGINT NOT NULL,total_cents INTEGER NOT NULL);
Fortalezas: cualquier nodo, en cualquier lugar —servidor de aplicación, cliente móvil, app offline-first— puede generar un ID globalmente único sin coordinación alguna y sin riesgo de colisión con los IDs de ningún otro nodo. No es enumerable. Ideal para fusionar datos provenientes de múltiples fuentes que escriben de forma independiente (sincronización offline, escrituras activo-activo entre regiones, event sourcing).
Debilidades (esta es la parte que los entrevistadores realmente quieren escuchar): un UUID aleatorio (v4) no tiene ninguna relación entre su valor y el orden de inserción, así que conforme se insertan filas, aterrizan en posiciones aleatorias a lo largo del índice B-tree de la primary key en vez de agregarse al final. Eso significa que cada insert tiene una buena probabilidad de tocar una página que no está en el buffer cache y de disparar divisiones de página, lo cual degrada de forma medible el throughput de inserción e infla el índice comparado con una clave secuencial — este es un efecto bien documentado y reproducible, no teórico. También ocupa el doble de almacenamiento que un bigint (16 bytes vs. 8), lo cual se multiplica en cada columna de foreign key que lo referencia y en cada índice.
UUID v7 (y de forma similar, ULID) resuelve el problema de localidad de inserción colocando un timestamp en milisegundos en los bits altos y aleatoriedad solo en los bits bajos — así los valores siguen siendo globalmente únicos e impredecibles en los bits bajos, pero se ordenan aproximadamente por momento de creación y se insertan (aproximadamente) en el extremo correcto del índice, cerrando la mayor parte de la brecha con el auto-incremental sin perder la propiedad de no requerir coordinación.
#ID estilo Snowflake: la respuesta de los sistemas distribuidos
class SnowflakeIdGenerator(private val workerId: Long, private val epoch: Long = 1_700_000_000_000L) {init {require(workerId in 0..1023) { "workerId debe caber en 10 bits" }}private var lastTimestamp = -1Lprivate var sequence = 0L// Diseño (64 bits): 1 bit de signo sin usar | 41 bits timestamp | 10 bits worker id | 12 bits secuencia@Synchronizedfun nextId(): Long {var timestamp = System.currentTimeMillis()if (timestamp == lastTimestamp) {sequence = (sequence + 1) and 0xFFF // secuencia de 12 bits, da la vuelta cada 4096/msif (sequence == 0L) {while (timestamp <= lastTimestamp) timestamp = System.currentTimeMillis()}} else {sequence = 0L}lastTimestamp = timestampreturn ((timestamp - epoch) shl 22) or (workerId shl 12) or sequence}}
Fortalezas: 64 bits (cabe en un BIGINT, la mitad del almacenamiento de un UUID), aproximadamente ordenable por tiempo como el auto-incremental (buena localidad de índice), y —el punto central— se genera sin una secuencia central, porque cada worker solo necesita su propio ID de worker preasignado y la hora actual para producir un ID que ningún otro worker producirá. Por esto es la respuesta estándar a "¿cómo generas IDs entre muchos nodos de escritura/shards independientes sin un cuello de botella?".
Debilidades: la asignación de worker ID es en sí misma un pequeño problema de coordinación (normalmente se resuelve una sola vez, al desplegar, vía configuración o un servicio de coordinación como ZooKeeper/etcd, no por cada ID) — y el comportamiento del reloj importa: si el reloj de un worker retrocede (una corrección de NTP), las implementaciones ingenuas pueden producir un ID duplicado o fuera de orden, así que las implementaciones de producción detectan saltos de reloj hacia atrás y esperan o fallan ruidosamente en lugar de emitir en silencio un ID incorrecto.
#Tabla de decisión
| Situación | Mejor opción | Por qué |
|---|---|---|
| Monolito de un solo escritor, sin sharding planeado | Auto-incremental | Lo más simple y compacto, mejor localidad de índice — no hay un problema distribuido que resolver |
| Los IDs deben generarse en el cliente o sin conexión | UUID (v4 o v7) | No requiere un viaje de ida y vuelta a la base de datos para obtener un ID |
| Fusionar datos de múltiples sistemas que escriben de forma independiente | UUID | Cero coordinación, cero riesgo de colisión sin importar la fuente |
| Escrituras shardeadas/distribuidas de alto volumen, se quiere compacto y ordenable | Estilo Snowflake | 64 bits, ordenado por tiempo, sin cuello de botella central |
| Los IDs se expondrán en URLs públicas y no deben ser enumerables | UUID o Snowflake con ofuscación | El auto-incremental filtra información de secuencia/volumen |
| Tabla con muchas inserciones donde el bloat de índice por claves aleatorias ya es un problema medido | Auto-incremental o UUID v7 | Ambos preservan la localidad de inserción; un UUID v4 puro no |
🧠 Check your understanding
0/3 · 0/3 answered1. ¿Por qué una primary key UUID v4 generada aleatoriamente tiende a perjudicar el rendimiento de inserción en una tabla grande comparada con un entero auto-incremental?
2. ¿Qué problema específico resuelve un ID estilo Snowflake que un auto-incremental simple no puede?
3. ¿Qué cambia UUID v7 respecto a UUID v4 que ayuda al rendimiento de la base de datos?