Saltar al contenido
AldeaCode Logo
Generador UUID / MySQL Generadores 100% local

UUIDs como clave primaria en MySQL: UUID(), UUID_TO_BIN y fragmentación

MySQL tiene la función UUID() desde 5.0, pero almacenar el resultado bien costó hasta MySQL 8.0. La forma ingenua quema disco y destroza el B-tree; la moderna son dos llamadas a función y una columna binaria.

Deja de usar CHAR(36) para UUIDs

Lo evidente es CHAR(36) porque es lo que devuelve UUID(): un string de 36 caracteres con guiones. Funciona, y es un derroche.

Un UUID son 128 bits, o sea 16 bytes. CHAR(36) guarda la forma textual: 36 bytes más overhead de charset, 108 bytes para utf8mb4. Es alrededor de 7 veces más almacenamiento, y cada página del B-tree cabe 7 veces menos filas. Lookups, range scans, joins, todo sufre.

La columna correcta es BINARY(16). Guarda los 128 bits crudos y nada más. Se comparan byte a byte sin reglas de collation, lo cual también es más rápido.

UUID_TO_BIN y BIN_TO_UUID hacen el round-trip

MySQL 8.0 añadió dos helpers para puentear strings UUID y almacenamiento binario:

SELECT UUID_TO_BIN('1a2b3c4d-5e6f-4a8b-9c0d-1e2f3a4b5c6d');
SELECT BIN_TO_UUID(@bin);

Las dos funciones aceptan un segundo argumento opcional: el swap flag. Con UUID_TO_BIN(uuid, 1) MySQL reordena los bytes para que el componente de tiempo quede al principio del valor binario. El motivo es localidad de índice, ver siguiente sección.

MySQL anterior a 8.0 no las trae. Las puedes emular con UNHEX(REPLACE(uuid, '-', '')) y INSERT(INSERT(...)) para reintroducir guiones, pero las builtin son más limpias y ya deberías estar en 8.0.

UUIDv4 fragmenta tu índice primario

UUID() en MySQL es UUIDv1 por defecto (timestamp + node ID). El consejo pre-8.0 solía decir "usa UUIDv4 porque v1 filtra la MAC". MySQL 8.0 permite insertar valores tipo v4 generados desde tu aplicación, pero el problema de fragmentación corta del otro lado.

Una clave primaria UUIDv4 es esencialmente aleatoria. Cada INSERT cae en una página al azar del índice clustered. InnoDB tiene que cargar la página, modificarla, posiblemente partirla, escribirla de vuelta. Tras unos millones de filas tienes un índice con hojas medio vacías y cada insert un fallo de buffer pool.

Un UUIDv1 con el swap flag (UUID_TO_BIN(UUID(), 1)) pone el timestamp delante. Las filas nuevas se agrupan al final del índice, igual que un BIGINT autoincrement. Mantienes la propiedad distribuida del UUID y dejas de odiar tus INSERTs.

Cuándo gana el BIGINT autoincrement

Si tu tabla es puramente interna, no replica entre regiones, nunca da un ID al cliente antes del insert, nunca se federa con otro sistema, BIGINT AUTO_INCREMENT sigue siendo la opción más barata y rápida. Ocho bytes, secuencial, indexa perfecto, soportado en todas partes.

Los UUIDs ganan su coste cuando necesitas: generación de ID en cliente (sin round-trip antes de tener la fila), merge de datos entre shards o regiones, seguridad por IDs no adivinables, o cero colisiones entre servicios. Si nada de eso aplica, quédate con BIGINT. Si aplica aunque sea uno, usa BINARY(16) con UUID_TO_BIN(UUID(), 1) y tienes lo mejor de ambos.

Ejemplo completo

sql
-- MySQL 8.0+. UUID v1 con swap flag para localidad de índice.
CREATE TABLE orders (
  id BINARY(16) PRIMARY KEY,
  amount DECIMAL(10, 2) NOT NULL,
  created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
);

-- Insert con un UUID v1 fresco (timestamp en los primeros bytes)
INSERT INTO orders (id, amount)
VALUES (UUID_TO_BIN(UUID(), 1), 19.99);

-- Lectura como string legible
SELECT BIN_TO_UUID(id, 1) AS id, amount, created_at
FROM orders
ORDER BY created_at DESC
LIMIT 10;

-- Búsqueda por string UUID (por ejemplo, viniendo de una request HTTP)
SELECT BIN_TO_UUID(id, 1) AS id, amount
FROM orders
WHERE id = UUID_TO_BIN('1a2b3c4d-5e6f-11ee-9c0d-1e2f3a4b5c6d', 1);

¿Solo necesitas el resultado?

Cuando estás poblando un script de migración o pegando un UUID fresco en un YAML, generarlo en MySQL es exagerado. El generador online de aldeacode.com produce valores conformes con RFC 9562 vía Web Crypto API al instante, y los puedes pasar por UUID_TO_BIN al insertar.

Abrir Generador de UUID v4 →

Preguntas frecuentes

¿Por qué mi columna UUID ocupa 108 bytes por fila?

La declaraste CHAR(36) sobre charset utf8mb4. Cada carácter reserva hasta 3 bytes más overhead. Cambia la columna a BINARY(16) y usa UUID_TO_BIN / BIN_TO_UUID en los bordes. La migración es un ALTER TABLE más un UPDATE.

¿Es seguro activar swap_flag en UUID_TO_BIN sobre datos existentes?

Solo si migras las filas existentes. El flag cambia el orden de bytes del valor guardado. Mezclar filas con flag-on y flag-off en la misma columna hace que BIN_TO_UUID devuelva basura para la mitad equivocada. Elige uno y reescribe la columna entera con UPDATE en una sola transacción.

¿Uso UUID() o genero los UUIDs en mi aplicación?

En la aplicación, casi siempre. Generar en MySQL implica que la fila existe antes de que la app sepa el ID, lo cual complica idempotencia, reintentos y event sourcing. Genera con crypto.randomUUID en Node, uuid.uuid4 en Python, java.util.UUID en Java, y luego INSERT con UUID_TO_BIN.