Tutorial de Diseño de Bases de Datos MySQL: Del Esquema a Producción
MySQL es la base de datos de código abierto más desplegada en el mundo. Este tutorial cubre las decisiones de diseño específicas de MySQL — motores de almacenamiento, tipos de datos, conjuntos de caracteres, indexación, claves foráneas y estrategias de migración — para llevar tu esquema del primer borrador a estar listo para producción.
MySQL impulsa más aplicaciones web que cualquier otra base de datos relacional. Es la "M" en el stack LAMP, el predeterminado en innumerables entornos de hosting compartido y sigue siendo la base de datos elegida por muchos sistemas de producción de alto tráfico. Pero MySQL tiene particularidades importantes y decisiones de diseño que difieren de PostgreSQL o SQLite — y conocerlas es esencial para construir un esquema que tenga buen rendimiento y evite errores sutiles.
Este tutorial recorre el diseño de esquemas específico de MySQL desde el diseño inicial hasta el despliegue en producción, cubriendo cada punto de decisión que importa.
Motores de Almacenamiento: Siempre Usa InnoDB
MySQL soporta múltiples motores de almacenamiento, pero el único que deberías usar para tablas de aplicaciones en 2026 es InnoDB. InnoDB soporta claves foráneas, transacciones, bloqueo a nivel de fila y recuperación ante fallos. El motor heredado MyISAM carece de soporte para claves foráneas y usa bloqueo a nivel de tabla, lo que lo hace inadecuado para cualquier aplicación con escrituras concurrentes.
InnoDB es el predeterminado desde MySQL 5.5, por lo que no deberías necesitar especificarlo explícitamente. Sin embargo, si estás trabajando con una base de datos más antigua o migrando desde otro sistema, verifica con SHOW TABLE STATUS que todas las tablas usen InnoDB. Si alguna tabla usa MyISAM, conviértela: ALTER TABLE table_name ENGINE = InnoDB;
Tipos Enteros: INT vs BIGINT
Los tipos enteros de MySQL difieren de los de PostgreSQL en nomenclatura y rango. Usa `INT` (4 bytes, rango con signo -2.1B a 2.1B, sin signo 0 a 4.3B) para la mayoría de IDs y contadores. Usa `BIGINT` (8 bytes, rango con signo ±9.2 trillones) para tablas que crecerán más allá de 2 mil millones de filas. El modificador `UNSIGNED` duplica el rango positivo sin costo de almacenamiento — INT UNSIGNED llega hasta 4,294,967,295 — y es una buena elección para columnas de ID que nunca serán negativas.
Para claves primarias auto-incrementales, la declaración es: id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY. Prefiere BIGINT UNSIGNED AUTO_INCREMENT para cualquier tabla que pueda escalar significativamente. El tipo TINYINT(1) es el booleano convencional de MySQL (0 o 1); los ORMs como Laravel y Doctrine mapean los booleanos de PHP a este tipo automáticamente.
VARCHAR vs TEXT: Una Distinción Crítica
A diferencia de PostgreSQL, MySQL almacena `VARCHAR` y `TEXT` de forma diferente, y la distinción importa para el rendimiento. VARCHAR(n) se almacena en línea con la fila para valores de hasta 65,535 bytes (dentro del límite del formato de fila). TEXT (y MEDIUMTEXT, LONGTEXT) almacena los datos fuera de página y solo mantiene un puntero en línea — esto lo hace más lento de leer para valores pequeños.
La guía práctica: usa VARCHAR(n) para cadenas donde conoces un límite superior razonable — nombres, emails, slugs, códigos de estado. Usa TEXT para contenido verdaderamente abierto — cuerpos de publicaciones de blog, documentos markdown, mensajes de log. Una advertencia importante: MySQL no puede usar una columna TEXT como parte de un índice UNIQUE o un índice regular sin una longitud de prefijo (INDEX (column(191))). Si necesitas indexar la columna, usa VARCHAR(191) (el máximo para utf8mb4 con la longitud de índice predeterminada).
Siempre Usa utf8mb4, No utf8
Este es uno de los problemas más notorios de MySQL. El conjunto de caracteres utf8 de MySQL no es UTF-8 real — solo soporta caracteres que caben en 3 bytes, lo que excluye emojis y muchos scripts no latinos. Siempre usa `utf8mb4`, que es la implementación genuina de UTF-8 de 4 bytes.
Establece el conjunto de caracteres a nivel de servidor, base de datos, tabla y columna. El enfoque más seguro es configurar los valores predeterminados de MySQL:
``sql
CREATE DATABASE myapp
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
Y especificarlo en las columnas VARCHAR y TEXT:
sql
name VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL
Para comparaciones insensibles a mayúsculas/minúsculas (el caso común), usa utf8mb4_unicode_ci. Para comparaciones sensibles a mayúsculas/minúsculas, usa utf8mb4_bin`.
DATETIME vs TIMESTAMP
MySQL tiene dos tipos para almacenar valores de fecha y hora. `TIMESTAMP` almacena valores como una marca de tiempo Unix UTC internamente y convierte hacia/desde la zona horaria local del servidor al leer/escribir. Soporta un rango de 1970 a 2038 — el infame "Problema del Año 2038." `DATETIME` almacena valores tal cual sin conversión de zona horaria, soportando un rango de 1000 a 9999.
Para columnas created_at y updated_at, usa DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP y DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP. Esto evita el límite de 2038 y la confusión de la conversión implícita de zona horaria. Para columnas que almacenan fechas y horas orientadas al usuario, almacénalas en UTC como DATETIME y convierte a la zona horaria del usuario en el código de la aplicación.
Claves Foráneas e InnoDB: La Sintaxis Importa
La sintaxis de clave foránea de MySQL es ligeramente diferente a la de PostgreSQL, y es estricta en ciertas reglas. Las columnas de clave foránea deben tener el mismo tipo y signo que la clave primaria referenciada: si users.id es INT UNSIGNED, entonces orders.user_id también debe ser INT UNSIGNED. La columna de clave foránea debe estar indexada (MySQL no siempre crea este índice automáticamente en todas las versiones).
``sql
CREATE TABLE orders (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id INT UNSIGNED NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'pending',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
INDEX idx_orders_user_id (user_id),
CONSTRAINT fk_orders_user
FOREIGN KEY (user_id) REFERENCES users (id)
ON DELETE RESTRICT
ON UPDATE CASCADE
);
Problemas Comunes de MySQL a Evitar
Varios comportamientos de MySQL toman por sorpresa a los desarrolladores. Truncación silenciosa: las versiones más antiguas de MySQL truncan cadenas que exceden la longitud de VARCHAR sin generar un error en modo no estricto. Habilita el modo estricto: SET sql_mode = 'STRICT_TRANS_TABLES,...' o configúralo en my.cnf. Fechas cero: MySQL permite 0000-00-00 como valor de fecha por defecto; deshabilita esto con NO_ZERO_DATE en sql_mode. Comportamiento de GROUP BY: MySQL históricamente permitía seleccionar columnas no agregadas que no estaban en GROUP BY; el modo ONLY_FULL_GROUP_BY aplica el comportamiento SQL estándar y siempre debería estar habilitado.
Estrategias de Migración para MySQL
Para evolucionar esquemas MySQL en producción, dos enfoques dominan. DDL en línea (soportado desde MySQL 5.6+): muchas operaciones ALTER TABLE en InnoDB se pueden realizar con ALGORITHM=INPLACE, LOCK=NONE, permitiendo que la tabla permanezca legible y escribible durante la migración. Consulta la documentación de MySQL para saber qué operaciones soportan DDL en línea. Para tablas grandes donde no se soporta DDL en línea, usa pt-online-schema-change (Percona Toolkit) o gh-ost (la herramienta de esquema en línea de GitHub) para realizar la migración sin bloquear la tabla.
Diseñando Esquemas MySQL con ER Flow
ER Flow soporta los tipos de datos de MySQL de forma nativa, incluyendo INT, BIGINT UNSIGNED, VARCHAR, TEXT, DATETIME, TIMESTAMP y TINYINT(1) para booleanos. Puedes diseñar tu esquema completo visualmente — tablas, columnas, claves foráneas, índices — y generar SQL de migración compatible con MySQL directamente desde el diagrama ER. Las migraciones generadas usan la sintaxis MySQL correcta incluyendo ENGINE=InnoDB, declaraciones de conjunto de caracteres correctas y creación explícita de índices para columnas de clave foránea. Empieza con el modelo visual, genera la migración y despliega con confianza.