UUID vs ULID vs IDs seriales: aprende cómo cada opción afecta indexación, ordenamiento, sharding y flujos seguros de exportación/importación en proyectos reales.

Elegir un ID parece aburrido en la primera semana. Luego lanzas, los datos crecen y esa decisión "simple" aparece por todas partes: índices, URLs, logs, exportaciones e integraciones.
La verdadera pregunta no es "¿cuál es el mejor?" sino "¿qué dolor quiero evitar más adelante?" Los IDs son difíciles de cambiar porque se copian en otras tablas, se cachean en clientes y otros sistemas llegan a depender de ellos.
Cuando el ID no encaja con la evolución del producto, normalmente lo ves en varios sitios:
Siempre hay un intercambio entre la conveniencia inmediata y la flexibilidad futura. Los enteros seriales son fáciles de leer y suelen ser rápidos, pero pueden filtrar el número de registros y complicar la fusión de conjuntos de datos. Los UUIDs aleatorios son fantásticos para unicidad entre sistemas, pero son más costosos para índices y menos amigables para humanos. Los ULIDs buscan unicidad global con un orden aproximado por tiempo, pero siguen teniendo trade-offs de almacenamiento y herramientas.
Una forma útil de pensarlo: ¿para quién es principalmente el ID?
Si el ID es principalmente para humanos (soporte, debugging, ops), los más cortos y fáciles de escanear suelen ganar. Si es para máquinas (escrituras distribuidas, clientes sin conexión, sistemas multi-región), la unicidad global y evitar colisiones importan más.
Cuando la gente debate "UUID vs ULID vs serial IDs", en realidad están eligiendo cómo se etiqueta de forma única cada fila. Esa etiqueta afecta lo fácil que es insertar, ordenar, fusionar y mover datos más tarde.
Un ID serial es un contador. La base de datos entrega 1, luego 2, luego 3, y así sucesivamente (a menudo almacenado como integer o bigint). Es fácil de leer, barato de almacenar y normalmente rápido porque las filas nuevas caen al final del índice.
Un UUID es un identificador de 128 bits que parece aleatorio, como 3f8a.... En la mayoría de configuraciones puede generarse sin pedirle a la base de datos el siguiente número, así que diferentes sistemas pueden crear IDs de forma independiente. El intercambio es que las inserciones con apariencia aleatoria pueden hacer que los índices trabajen más y ocupen más espacio que un simple bigint.
Un ULID también tiene 128 bits, pero está diseñado para ordenarse aproximadamente por tiempo. Los ULIDs más nuevos suelen ordenarse después de los más antiguos, manteniendo unicidad global. A menudo obtienes parte del beneficio de "generar en cualquier lugar" de los UUIDs con un comportamiento de orden más amistoso.
Resumen sencillo:
Los IDs seriales son comunes en apps de una sola base de datos y herramientas internas. Los UUIDs aparecen cuando los datos se crean en múltiples servicios, dispositivos o regiones. Los ULIDs son populares cuando los equipos quieren generación distribuida de IDs pero aún les importa el orden para paginación o consultas de "lo más reciente".
Una clave primaria suele respaldarse con un índice (a menudo un B-tree). Piensa en ese índice como una guía telefónica ordenada: cada nueva fila necesita una entrada colocada en el lugar correcto para que las búsquedas sigan siendo rápidas.
Con IDs aleatorios (UUIDv4 clásico), las entradas nuevas caen por todo el índice. Eso significa que la base de datos toca muchas páginas de índice, divide páginas con más frecuencia y hace escrituras adicionales. Con el tiempo obtienes más churn en el índice: más trabajo por inserción, más fallos de caché y índices más grandes de lo esperado.
Con IDs mayormente crecientes (serial/bigint, o IDs ordenados por tiempo como muchos ULIDs), la base de datos puede normalmente anexar nuevas entradas cerca del final del índice. Esto es más cache-friendly porque las páginas recientes se mantienen calientes y las inserciones tienden a ser más suaves a altas tasas de escritura.
El tamaño de la clave importa porque las entradas del índice no son gratis:
Las claves más grandes significan que caben menos entradas por página de índice. Eso suele llevar a índices más profundos, más páginas leídas por consulta y más RAM necesaria para mantener velocidad.
Si tienes una tabla de "events" con inserciones constantes, una clave primaria UUID aleatoria puede empezar a sentirse más lenta antes que una clave bigint, incluso si las búsquedas de una sola fila siguen pareciendo bien. Si esperas escrituras pesadas, el coste de indexación suele ser la primera diferencia real que notas.
Si has construido "Cargar más" o scroll infinito, ya has sentido el dolor de IDs que no se ordenan bien. Un ID "se ordena bien" cuando ordenarlo ofrece un orden estable y significativo (a menudo tiempo de creación) para que la paginación sea predecible.
Con IDs aleatorios (como UUIDv4), las filas nuevas quedan dispersas. Ordenar por id no coincide con el tiempo, y la paginación por cursor tipo "dame elementos después de este id" se vuelve poco fiable. Normalmente vuelves a created_at, que está bien, pero hay que usarlo con cuidado.
Los ULIDs están diseñados para ordenarse aproximadamente por tiempo. Si ordenas por ULID (como cadena o en su forma binaria), los elementos más nuevos suelen venir después. Eso facilita la paginación por cursores porque el cursor puede ser el último ULID visto.
ULID ayuda con un orden natural aproximado por tiempo para feeds, cursores más simples y menos inserciones aleatorias que con UUIDv4.
Pero ULID no garantiza orden temporal perfecto cuando se generan muchos IDs en la misma milésima de segundo en varias máquinas. Si necesitas orden exacto, aún quieres una marca temporal real.
created_at sigue siendo mejorOrdenar por created_at suele ser más seguro cuando rellenas datos históricos, importas registros antiguos o necesitas desempates claros.
Un patrón práctico es ordenar por (created_at, id), donde id sirve solo como desempate.
Sharding significa dividir una base de datos en varias más pequeñas para que cada shard contenga parte de los datos. Los equipos suelen hacerlo más adelante, cuando una sola base de datos es difícil de escalar o se vuelve un punto único de fallo.
Tu elección de ID puede hacer que el sharding sea manejable o doloroso.
Con IDs secuenciales (auto-increment serial o bigint), cada shard generará con gusto 1, 2, 3.... El mismo ID puede existir en varios shards. La primera vez que necesites fusionar datos, mover filas o construir características entre shards, te encontrarás con colisiones.
Puedes evitar colisiones con coordinación (un servicio central de IDs, o rangos por shard), pero eso añade piezas móviles y puede convertirse en un cuello de botella.
UUIDs y ULIDs reducen la necesidad de coordinación porque cada shard puede generar IDs independientemente con un riesgo extremadamente bajo de duplicados. Si crees que alguna vez dividirás datos entre bases de datos, este es uno de los argumentos más fuertes contra las secuencias puras.
Un compromiso común es añadir un prefijo de shard y luego usar una secuencia local en cada shard. Puedes almacenarlo en dos columnas o empaquetarlo en un solo valor.
Funciona, pero crea un formato de ID personalizado. Cada integración debe entenderlo, el orden deja de significar tiempo global sin lógica adicional, y mover datos entre shards puede requerir reescribir IDs (lo que rompe referencias si esos IDs se comparten).
Hazte una pregunta temprano: ¿alguna vez necesitarás combinar datos de múltiples bases de datos y mantener referencias estables? Si la respuesta es sí, planifica IDs globalmente únicos desde el día uno o reserva tiempo para una migración posterior.
La exportación e importación es donde la elección de ID deja de ser teórica. En el momento en que clonas prod a staging, restauras un backup o fusionas datos de dos sistemas, descubres si tus IDs son estables y portables.
Con IDs seriales, normalmente no puedes reproducir inserciones en otra base de datos y esperar que las referencias permanezcan intactas a menos que conserves los números originales. Si importas solo un subconjunto de filas (por ejemplo, 200 clientes y sus pedidos), debes cargar las tablas en el orden correcto y mantener las claves primarias exactas. Si algo se renumera, las claves foráneas se rompen.
Los UUIDs y ULIDs se generan fuera de la secuencia de la base de datos, así que son más fáciles de mover entre entornos. Puedes copiar filas, mantener los IDs y las relaciones seguirán apuntando correctamente. Esto ayuda al restaurar backups, hacer exportaciones parciales o fusionar datasets.
Ejemplo: exportar 50 cuentas de producción para depurar un problema en staging. Con claves primarias UUID/ULID, puedes importar esas cuentas más las filas relacionadas (proyectos, facturas, logs) y todo seguirá apuntando al padre correcto. Con IDs seriales, a menudo terminas construyendo una tabla de traducción (old_id -> new_id) y reescribiendo claves foráneas durante la importación.
Para importaciones masivas, lo básico importa más que el tipo de ID:
Puedes tomar una decisión sólida rápidamente si te centras en lo que dolerá más tarde.
Escribe tus riesgos futuros principales. Eventos concretos ayudan: dividir en múltiples bases de datos, fusionar datos de otro sistema, escrituras offline, copias frecuentes de datos entre entornos.
Decide si el orden del ID debe coincidir con el tiempo. Si quieres "lo más reciente primero" sin columnas extra, ULID (o UUIDv7) encaja bien. Si te basta ordenar por created_at, UUIDs y serial IDs funcionan.
Estima volumen de escrituras y sensibilidad del índice. Si esperas inserciones intensas y tu índice de clave primaria es el que más sufre, un BIGINT serial suele ser lo más amable con B-tree. Los UUIDs aleatorios tienden a causar más churn.
Elige un predeterminado y documenta excepciones. Manténlo simple: un predeterminado para la mayoría de tablas y una regla clara para desviarte (a menudo: IDs públicos vs internos).
Deja margen para cambiar. Evita codificar significado en los IDs, decide dónde se generan (BD vs app) y mantén las restricciones explícitas.
La mayor trampa es elegir un ID porque está de moda y luego descubrir que choca con cómo consultas, escalas o compartes datos. La mayoría de problemas aparecen meses después.
Fallos comunes:
123, 124, 125, la gente puede adivinar registros cercanos y sondear tu sistema.Señales de advertencia para abordar temprano:
Elige un tipo de clave primaria y mantenlo en la mayoría de tablas. Mezclar tipos (bigint en un sitio, UUID en otro) complica joins, APIs y migraciones.
Estima el tamaño del índice a la escala esperada. Las claves anchas significan índices primarios más grandes y más memoria/IO.
Decide cómo vas a paginar. Si paginas por ID, asegúrate de que el ID tenga un orden predecible (o acepta que no lo tenga). Si paginas por timestamp, indexa created_at y úsalo consistentemente.
Prueba tu plan de importación con datos parecidos a producción. Verifica que puedas recrear registros sin romper claves foráneas y que las re-importaciones no generen IDs nuevos silenciosamente.
Escribe tu estrategia de colisiones. ¿Quién genera el ID (BD o app) y qué pasa si dos sistemas crean registros offline y luego sincronizan?
Asegúrate de que las URLs públicas y los logs no filtren patrones que te importen (conteo de registros, tasa de creación, pistas de shard interno). Si usas IDs seriales, asume que la gente puede adivinar IDs cercanos.
Un fundador lanza un CRM simple: contactos, oportunidades, notas. Una base de datos Postgres, una web app y el objetivo principal es lanzar.
Al principio, una clave primaria serial bigint parece perfecta. Las inserciones son rápidas, los índices se mantienen ordenados y es fácil de leer en logs.
Un año después, un cliente pide exportes trimestrales para una auditoría y el fundador empieza a importar leads de una herramienta de marketing. Los IDs que eran internos ahora aparecen en CSVs, emails y tickets de soporte. Si dos sistemas usan 1, 2, 3..., las fusiones se complican. Terminas añadiendo columnas de origen, tablas de mapeo o reescribiendo IDs durante la importación.
En el segundo año aparece una app móvil. Necesita crear registros offline y sincronizar después. Ahora necesitas IDs que puedan generarse en el cliente sin hablar con la base de datos y quieres bajo riesgo de colisión cuando los datos lleguen de diferentes entornos.
Un compromiso que suele envejecer bien:
Si dudas entre UUID, ULID y serial IDs, decide según cómo se moverán y crecerán tus datos.
Picks en una frase para casos comunes:
bigint serial como clave primaria.Mezclar suele ser la mejor respuesta. Usa bigint serial para tablas internas que nunca salen de tu base de datos (tablas de join, jobs en segundo plano) y usa UUID/ULID para entidades públicas como usuarios, organizaciones, facturas y cualquier cosa que puedas exportar, sincronizar o referenciar desde otro servicio.
Si construyes en Koder.ai (koder.ai), vale la pena decidir tu patrón de IDs antes de generar muchas tablas y APIs. El modo de planificación y los snapshots/rollback de la plataforma facilitan aplicar y validar cambios de esquema temprano, mientras el sistema sigue siendo lo bastante pequeño como para cambiar con seguridad.
Empieza por el dolor futuro que quieres evitar: inserciones lentas por escrituras aleatorias en índices, paginación complicada, migraciones riesgosas o colisiones de IDs durante importaciones y fusiones. Si esperas que los datos se muevan entre sistemas o se creen en varios lugares, por defecto usa un ID globalmente único (UUID/ULID) y separa las preocupaciones de orden temporal.
Un bigint serial es una buena elección cuando tienes una única base de datos, las escrituras son intensas y los IDs permanecen internos. Es compacto, eficiente para índices B-tree y fácil de leer en logs. La desventaja principal es que es difícil fusionar datos más adelante sin colisiones, y si se expone públicamente puede revelar el número de registros.
Elige UUIDs cuando los registros puedan crearse en varios servicios, regiones, dispositivos u clientes offline y quieras un riesgo de colisión extremadamente bajo sin coordinación. Los UUIDs también funcionan bien como IDs públicos porque son difíciles de adivinar. El intercambio habitual es índices más grandes y patrones de inserción más aleatorios frente a claves secuenciales.
ULIDs tienen sentido cuando quieres IDs que puedan generarse en cualquier lugar y que, además, se ordenen generalmente por tiempo de creación. Esto simplifica la paginación por cursores y reduce el problema de inserciones “aleatorias” que suele ocurrir con UUIDv4. Aun así, no trates ULID como una marca temporal perfecta; usa created_at cuando necesites orden estricto o seguridad al rellenar datos históricos.
Sí, especialmente con UUIDv4 aleatorios en tablas con muchas escrituras. Las inserciones aleatorias se dispersan por el índice de la clave primaria, provocando más splits de páginas, mayor churn de caché y índices más grandes con el tiempo. Normalmente lo notarás primero en tasas sostenidas de inserción más lentas y mayores necesidades de memoria/IO, más que en búsquedas de una sola fila.
Ordenar por un ID aleatorio (como UUIDv4) no coincide con el tiempo de creación, así que los cursores "después de este id" no siguen una línea temporal estable. La solución fiable es paginar por created_at y añadir el ID como desempate, por ejemplo (created_at, id). Si quieres paginar solo por ID, un ID ordenable por tiempo como ULID suele ser más sencillo.
Las IDs secuenciales colisionan entre shards porque cada shard generará 1, 2, 3... de forma independiente. Puedes evitar colisiones con coordinación (rangos por shard o un servicio central de IDs), pero eso añade complejidad operativa y puede convertirse en un cuello de botella. UUIDs/ULIDs reducen la necesidad de coordinación porque cada shard puede generar IDs de forma segura por su cuenta.
UUIDs/ULIDs son más fáciles porque puedes exportar filas, importarlas en otro sitio y mantener las referencias intactas sin renumerar. Con IDs seriales, las importaciones parciales suelen requerir una tabla de traducción (old_id -> new_id) y reescribir claves foráneas cuidadosamente, lo cual es fácil de estropear. Si clonas entornos o fusionas datasets con frecuencia, los IDs globales ahorran mucho tiempo.
Un patrón común es usar dos IDs: una clave primaria interna compacta (serial bigint) para joins y eficiencia de almacenamiento, y además un ID público inmutable (ULID o UUID) para URLs, APIs, exportaciones y referencias entre sistemas. Esto mantiene la base de datos rápida y facilita integraciones y migraciones. La clave es tratar el ID público como estable y no reciclarlo ni reinterpretarlo.
Decídelo pronto y aplícalo de forma consistente en tablas y APIs. En Koder.ai, decide tu estrategia de IDs por defecto en el modo de planificación antes de generar muchas tablas y endpoints, y usa snapshots/rollback para validar cambios mientras el proyecto todavía es pequeño. La parte más difícil no es crear nuevos IDs, sino actualizar claves foráneas, cachés, logs y payloads externos que siguen referenciando los anteriores.