ULID frente a UUID: identificadores únicos que se pueden ordenar por fecha

Un ULID (Universally Unique Lexicographically Sortable Identifier) ocupa los mismos 128 bits que un UUID, pero reserva los primeros 48 para una marca de tiempo. Eso hace que los identificadores se ordenen por fecha de creación sin columnas auxiliares. Michael Drogalis, creador de ShadowTraffic, lo ha recordado en una publicación de LinkedIn con una recomendación directa: si usas UUID, plantéate cambiarlos por ULID. Los comentarios dejaron claro que la decisión tiene más matices de los que parece.

Este artículo es una adaptación del análisis publicado en administraciondesistemas.com, ampliado con el punto de vista de quien escribe código y diseña esquemas.

Anatomía de un ULID

Un ULID se compone de 48 bits de marca de tiempo (milisegundos desde 1970) y 80 bits aleatorios. Se escribe con 26 caracteres en Base32 de Crockford, sin guiones, sin distinguir mayúsculas de minúsculas y sin las letras que se confunden fácilmente (I, L, O y U). Frente a los 36 caracteres de un UUID con guiones, resulta más corto y se selecciona entero con un doble clic.

Drogalis lo explica con el ejemplo 01HX2AY3TN3M5ZFA5TG2WZJZ6G: los 10 primeros caracteres (01HX2AY3TN) son la marca de tiempo y los 16 restantes (3M5ZFA5TG2WZJZ6G) la parte aleatoria. Como la codificación conserva el orden, para saber cuál de dos ULID es más antiguo basta una comparación alfabética de cadenas, sin decodificar nada.

Un detalle curioso: el esquema que acompaña la publicación original es ilustrativo y sus cifras no cuadran del todo. Si decodificas ese prefijo con el alfabeto de Crockford, el resultado es el 4 de mayo de 2024 a las 17:37:54 UTC. Es fácil comprobarlo:

ALFABETO = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"

def ulid_a_ms(ulid):
    valor = 0
    for c in ulid[:10]:
        valor = valor * 32 + ALFABETO.index(c)
    return valor

import datetime
ms = ulid_a_ms("01HX2AY3TN3M5ZFA5TG2WZJZ6G")
print(datetime.datetime.fromtimestamp(ms / 1000, datetime.timezone.utc))
# 2024-05-04 17:37:54.517000+00:00

El problema real: UUIDv4 como clave primaria

La ventaja que más pesa aparece cuando el identificador se usa como clave primaria. Con un UUIDv4 como índice agrupado, cada inserción cae en un punto aleatorio del árbol B. El motor tiene que dividir páginas, reorganizar y mantener en memoria buena parte del índice para no ir a disco a cada escritura. Con un identificador que crece con el tiempo, los registros nuevos se añaden casi siempre al final y el índice se mantiene compacto.

El efecto es más visible en motores que agrupan la tabla por la clave primaria, como InnoDB en MySQL o SQL Server con índice clustered. En PostgreSQL la tabla es un montón (heap) y el impacto se concentra en el índice de la clave, pero sigue existiendo: más páginas tocadas, más escritura en WAL y más bloat.

ULID o UUIDv7

Conviene saber que el propio estándar UUID ya incluye una alternativa equivalente. La RFC 9562, publicada en 2024, define UUIDv7, que también empieza por una marca de tiempo Unix de 48 bits en milisegundos. La diferencia está en la representación y el ecosistema:

Aspecto

ULID

UUIDv7

Tamaño

128 bits

128 bits

Marca de tiempo

48 bits, ms

48 bits, ms

Texto

26 caracteres, Base32

36 caracteres, hexadecimal con guiones

Especificación

Proyecto abierto en GitHub

Estándar (RFC 9562)

Columna nativa UUID

No directamente (texto de 26 o binario de 16 bytes)

Sí, por ejemplo en PostgreSQL

Si tu esquema ya está lleno de columnas de tipo UUID, pasar a UUIDv7 suele ser un cambio mucho menos traumático que migrar a ULID: mantienes el tipo, las claves foráneas y las herramientas. Si empiezas de cero y valoras un identificador corto, legible en URLs y fácil de copiar, el ULID sigue siendo atractivo. Si lo guardas, hazlo en binario de 16 bytes; como texto de 26 caracteres desperdicias espacio en cada índice secundario.

Lo que se discutió en los comentarios

No todo el mundo compró la idea. Peter Mahon, ingeniero de fiabilidad (SRE), no veía ventajas claras frente a un UUIDv4 acompañado de una columna con la fecha en UTC, más allá del ahorro de espacio. Además advirtió de que incrustar la marca de tiempo cede algo de privacidad: cualquiera que vea el identificador sabe cuándo se creó el registro. Sean Michael Kelly respondió que un UUIDv4 más una fecha son dos datos donde muchas veces solo cabe uno, y que el ULID evita combinarlos. Pascal Lüscher resumió el problema del índice agrupado, y Ben Meier destacó el detalle práctico del doble clic.

Cuándo no conviene

  • Identificadores expuestos al público. Si la fecha de alta de un usuario o de un pedido es información sensible, un ID con marca de tiempo la filtra. Además, si el volumen de registros por milisegundo es bajo, un atacante puede acotar mucho el espacio de búsqueda.
  • Orden entre máquinas. Los relojes de distintos servidores no están perfectamente sincronizados, así que el orden entre ULID generados en nodos diferentes no es fiable al milisegundo. Dentro de un mismo proceso, la especificación prevé un modo monótono que incrementa la parte aleatoria cuando se generan varios en el mismo milisegundo.
  • Cuando ya tienes una columna de fecha. Si necesitas created_at de todos modos, el ULID no te lo quita, solo te permite ordenar sin él.

Entonces, ¿cambiar o no?

Mi lectura: el consejo de «cambia tus UUID por ULID» es demasiado general. Lo que de verdad importa es dejar de usar identificadores totalmente aleatorios como clave primaria en tablas con muchas escrituras. Para eso valen ULID y UUIDv7; en proyectos nuevos con bases de datos que ya soportan UUIDv7, tiene sentido preferirlo por ser estándar. Y si el identificador va a verse en una URL pública y la fecha es sensible, mantén un UUIDv4 como identificador externo y usa el ordenable solo internamente.

Preguntas frecuentes

¿Qué es un ULID? Un identificador único de 128 bits que combina una marca de tiempo de 48 bits y 80 bits aleatorios, ordenable por fecha de creación.

¿En qué se diferencia de un UUIDv4? El UUIDv4 es totalmente aleatorio. El ULID incluye la fecha de creación, se escribe en 26 caracteres sin guiones y se ordena alfabéticamente por tiempo.

¿ULID o UUIDv7? Ambos empiezan por una marca de tiempo de 48 bits. UUIDv7 es un estándar compatible con las columnas UUID; ULID usa una representación más compacta.

Fuentes

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP