Principios de diseño de bases de datos relacionales para MySQL 8: guía actualizada 2026

El diseño de la base de datos es la decisión más difícil de cambiar después de que una aplicación entra en producción. Un esquema mal pensado genera consultas lentas, datos inconsistentes y migraciones dolorosas. Uno bien diseñado aguanta años de crecimiento sin apenas tocar la estructura. Este artículo, que nació como una guía de principios básicos publicada por MySQL Hispano en 2004 y que ha sido revisada y actualizada por David Carrero en 2026, recorre los principios que siguen siendo válidos y añade las herramientas que MySQL 8 pone a disposición del desarrollador moderno.

Almacenar solo la información necesaria

El «síndrome de la hoja de cálculo» es uno de los errores más comunes en diseño de bases de datos: añadir columnas «por si acaso» en lugar de almacenar únicamente lo que la aplicación va a necesitar. Datos que se pueden calcular a partir de otros no deben almacenarse por separado. El precio de un producto más el IVA no necesita su propia columna si el porcentaje de IVA es un campo de configuración: se calcula en la consulta.

Una preocupación habitual entre quienes empiezan es que añadir columnas después es complicado. En MySQL, ALTER TABLE permite modificar la estructura de una tabla sin perder datos:

ALTER TABLE productos ADD COLUMN popularidad TINYINT UNSIGNED DEFAULT 0;
ALTER TABLE productos ADD COLUMN fecha_alta DATE NOT NULL;
ALTER TABLE clientes DROP COLUMN telefono_fax; -- campo obsoleto

En MySQL 8 con InnoDB, muchas operaciones de ALTER TABLE son «online» (no bloquean lecturas mientras se ejecutan).

No uses SELECT *

La consulta SELECT * FROM tabla es cómoda durante el desarrollo y problemática en producción por varias razones:

  • Transfiere columnas que quizá nunca se usen, aumentando el consumo de red y memoria.
  • Si la tabla cambia (se añade o reordena una columna), el código que accede por posición numérica falla.
  • El optimizador tiene menos información para elegir el plan de ejecución más eficiente.
  • Dificulta la revisión de código: no queda claro qué datos necesita realmente la función que hace la consulta.

La alternativa es siempre explicitar las columnas:

-- En lugar de:
SELECT * FROM productos WHERE activo = 1;

-- Hacer:
SELECT id_producto, nombre, precio, stock FROM productos WHERE activo = 1;

Normalización: evitar la redundancia

La normalización es el proceso de organizar las tablas para eliminar redundancias y dependencias problemáticas. No es necesario memorizar los nombres formales de las formas normales para aplicar sus principios.

El ejemplo clásico es la tabla de CDs con columnas track1, track2, ..., track20. El problema es evidente: ¿cuántas columnas poner? ¿Qué pasa con los álbumes de 22 pistas? La solución es separar los datos en dos tablas relacionadas:

CREATE TABLE album (
  id_album  INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
  titulo    VARCHAR(100) NOT NULL,
  artista   VARCHAR(100) NOT NULL,
  anyo      YEAR NOT NULL
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

CREATE TABLE pista (
  id_pista  INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
  id_album  INT NOT NULL,
  numero    TINYINT UNSIGNED NOT NULL,
  titulo    VARCHAR(100) NOT NULL,
  FOREIGN KEY (id_album) REFERENCES album (id_album) ON DELETE CASCADE
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

El campo id_album en la tabla pista es la relación: permite obtener todas las pistas de un álbum con un JOIN sencillo y almacenar cualquier número de pistas sin cambiar el esquema.

Los cuatro objetivos de la normalización que siguen siendo válidos en 2026:

  • Eliminar la redundancia de datos (el mismo dato no debe estar en dos sitios).
  • Evitar inconsistencias (si el dato está en dos sitios, pueden desincronizarse).
  • Representar toda la información que la aplicación necesita.
  • Facilitar las actualizaciones (modificar un dato en un solo lugar).

Dicho esto, la denormalización calculada (duplicar datos para evitar JOINs costosos en tablas enormes) es una técnica válida cuando el rendimiento lo justifica. Normaliza primero, denormaliza solo cuando tengas datos de rendimiento que lo justifiquen.

Elegir el tipo de dato correcto

El tipo de dato afecta al espacio en disco, al rendimiento de las consultas y a la validez de los datos. Guía actualizada para MySQL 8:

Tipo

Cuándo usarlo

Evitar

TINYINT UNSIGNED

Valores 0-255 (puntuaciones, boolean numérico)

Usar INT cuando TINYINT basta

INT / BIGINT

Claves primarias, contadores

BIGINT por defecto sin necesitarlo

DECIMAL(p,s)

Importes monetarios, porcentajes exactos

FLOAT/DOUBLE para dinero (errores de redondeo)

VARCHAR(n)

Textos de longitud variable con límite conocido

VARCHAR(255) para todo por comodidad

TEXT

Contenido largo sin límite fijo (artículos, descripciones)

TEXT para campos cortos que sí tienen límite

DATE

Solo fecha (cumpleaños, fecha de alta)

DATETIME cuando no necesitas la hora

DATETIME

Fecha y hora en timezone local

TIMESTAMP si el rango 2038 puede ser un problema

JSON

Atributos variables o configuraciones por fila

JSON para datos que debes consultar con índices normales

ENUM

Estados fijos con pocos valores (activo, pendiente, cancelado)

ENUM si el conjunto de valores va a cambiar frecuentemente

Restricciones de integridad: deja que MySQL valide

El artículo original de 2004 recomendaba indexar apropiadamente pero apenas mencionaba las restricciones de integridad. En MySQL 8, las restricciones que siempre debiste usar son:

NOT NULL

Un campo que siempre debe tener valor debe declararse NOT NULL. NULL tiene semántica especial en SQL (cualquier operación con NULL devuelve NULL) y genera código de aplicación más complejo. Define NOT NULL por defecto y usa NULL solo cuando «sin valor» sea un estado válido del negocio.

FOREIGN KEY

Las claves foráneas son la forma en que MySQL garantiza la integridad referencial automáticamente:

ALTER TABLE pedidos
ADD CONSTRAINT fk_pedidos_cliente
FOREIGN KEY (id_cliente) REFERENCES clientes (id_cliente)
ON DELETE RESTRICT
ON UPDATE CASCADE;

ON DELETE RESTRICT evita borrar un cliente que tiene pedidos. ON UPDATE CASCADE propaga los cambios de la PK al campo FK. Esto te ahorra decenas de validaciones en el código de aplicación.

CHECK (MySQL 8.0.16+)

Hasta MySQL 8.0.15, las restricciones CHECK existían en la sintaxis pero MySQL las ignoraba completamente. A partir de MySQL 8.0.16 se evalúan y se aplican:

CREATE TABLE productos (
  id_producto INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
  nombre      VARCHAR(100) NOT NULL,
  precio      DECIMAL(10, 2) NOT NULL,
  stock       INT NOT NULL DEFAULT 0,
  CONSTRAINT chk_precio_positivo CHECK (precio > 0),
  CONSTRAINT chk_stock_no_negativo CHECK (stock >= 0)
) ENGINE = InnoDB;

Intentar insertar un producto con precio negativo lanzará un error a nivel de base de datos, independientemente de si la aplicación valida o no.

Índices: cuándo crear y cuándo no crear

Los índices aceleran las lecturas y penalizan las escrituras (INSERT, UPDATE, DELETE tienen que mantener los índices actualizados). La regla práctica:

  • Indexa las columnas que aparecen frecuentemente en WHERE, ORDER BY, GROUP BY y como columna de join.
  • No indexes columnas con muy baja cardinalidad (por ejemplo, una columna booleana). MySQL preferirá el escaneo completo de tabla.
  • Los índices compuestos son ordenados: el orden de las columnas importa. INDEX (apellido, nombre) acelera búsquedas por apellido solo o por apellido+nombre, pero no por nombre solo.
  • Usa EXPLAIN para verificar que MySQL usa el índice que esperas.
-- Ver índices existentes
SHOW INDEX FROM pedidos;

-- Índice compuesto para consultas típicas de listado
CREATE INDEX idx_pedidos_cliente_fecha
ON pedidos (id_cliente, fecha_pedido DESC);

JSON para datos semi-estructurados

MySQL 5.7 introdujo el tipo de columna JSON con validación automática del formato. MySQL 8 amplió las funciones disponibles considerablemente. Es útil cuando diferentes filas necesitan atributos distintos sin justificar columnas separadas:

-- Tabla de productos con atributos variables por categoría
ALTER TABLE productos ADD COLUMN atributos JSON;

-- Electrónica
UPDATE productos SET atributos = '{"voltaje": 220, "frecuencia": 50}' WHERE id = 1;

-- Ropa
UPDATE productos SET atributos = '{"talla": "XL", "color": "azul"}' WHERE id = 2;

-- Consultar por atributo JSON con índice funcional (MySQL 8.0.13+)
ALTER TABLE productos ADD INDEX idx_talla ((CAST(atributos->>'$.talla' AS CHAR(10))));

Regla: usa JSON para atributos que varían por fila y que rara vez necesitas filtrar. Para datos que filtras frecuentemente, sigue siendo más eficiente una columna normal con índice.

UUID vs INT como clave primaria

El artículo original usaba AUTO_INCREMENT sin cuestionarlo. En arquitecturas modernas (sistemas distribuidos, replicación multi-maestro, APIs que no quieren exponer IDs secuenciales) aparece la opción de UUID. La comparativa:

Criterio

INT AUTO_INCREMENT

UUID (VARCHAR / BINARY)

Tamaño

4 bytes (INT) / 8 bytes (BIGINT)

16 bytes (BINARY) / 36 bytes (VARCHAR)

Rendimiento de índice

Excelente (secuencial)

Peor (aleatorio, fragmenta el B-tree)

Unicidad global

No (depende de la instancia)

Previsibilidad del ID

Predecible (seguridad)

No predecible

Complejidad

Simple

Más complejo (generar, almacenar, mostrar)

En MySQL 8 puedes usar UUID_TO_BIN(UUID(), true) para generar UUIDs ordenados en tiempo (versión 1 reordenada) que fragmentan menos el B-tree. Para la mayoría de aplicaciones web, BIGINT AUTO_INCREMENT sigue siendo la elección correcta.

Convenciones de nombres en 2026

Las convenciones del artículo original siguen siendo válidas. Algunas aclaraciones y añadidos para 2026:

  • Usa snake_case en minúsculas (MySQL distingue mayúsculas/minúsculas según el sistema operativo; minúsculas evitan sorpresas en migraciones entre Linux y Windows).
  • Nombres de tabla en plural (pedidos, clientes, productos), nombres de columna en singular (id_pedido, nombre).
  • Las claves primarias: id_tabla o simplemente id. Elige uno y sé consistente.
  • Las claves foráneas: mismo nombre que la columna a la que apuntan (id_cliente en la tabla pedidos referencia a id_cliente en clientes).
  • Los índices: prefijo descriptivo (idx_ para índices normales, fk_ para claves foráneas, ux_ para unique).
  • Máximo 64 caracteres por nombre (límite de MySQL que no ha cambiado).

Charset: siempre utf8mb4

El artículo original no mencionaba el charset porque en 2004 el problema no era tan evidente. En 2026 no hay excusa: todas las bases de datos deben usar utf8mb4, no utf8 (que en MySQL solo almacena hasta 3 bytes por carácter, excluyendo emojis y muchos caracteres asiáticos):

CREATE DATABASE mi_app
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;

Configúralo también en el servidor para que sea el charset por defecto de las conexiones nuevas.

Imagen: Pexels / Ivan S

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP
ARTÍCULO ANTERIOR

SIGUIENTE ARTÍCULO