Skip to content

2.15 Normalización

El modelado de datos es el proceso de definir la estructura y organización de los datos en una base de datos relacional eficiente, no redundante y fácil de mantener. Implica identificar las entidades, sus atributos y las relaciones entre ellas, así como aplicar principios de normalización para optimizar el diseño de la base de datos.

¿Qué es el modelado de datos?

El modelado de datos implica identificar las entidades (tablas), sus atributos (columnas) y las relaciones entre ellas. Un modelo de datos bien diseñado facilita la comprensión de los datos, mejora el rendimiento de las consultas y asegura que se cumplan las reglas de negocio.

Entidades y atributos

Las entidades son objetos o conceptos del mundo real que queremos representar en la base de datos. Cada entidad se convierte en una tabla, y sus atributos se convierten en columnas de esa tabla.

Por ejemplo, en un sistema de gestión de pedidos, podríamos tener entidades como "Clientes", "Pedidos" y "Productos". Cada una de estas entidades tendría sus propios atributos:

  • Clientes: ID, Nombre, Email, Dirección
  • Pedidos: ID, Cliente_ID, Producto, Cantidad, Fecha
  • Productos: ID, Nombre, Precio, Stock

Relaciones entre entidades

Las relaciones entre entidades definen cómo se conectan las tablas entre sí. Existen tres tipos principales de relaciones:

  • Uno a uno (1:1): Cada registro en una tabla se relaciona con un único registro en otra tabla. Por ejemplo, un usuario puede tener un único perfil asociado.
  • Uno a muchos (1:N): Un registro en una tabla puede estar relacionado con múltiples registros en otra tabla. Por ejemplo, un cliente puede tener múltiples pedidos.
  • Muchos a muchos (N:M): Los registros en una tabla pueden estar relacionados con múltiples registros en otra tabla y viceversa. Por ejemplo, un estudiante puede estar inscrito en múltiples cursos y un curso puede tener múltiples estudiantes. Para manejar este tipo de relaciones, se suele crear una tabla intermedia que contenga las claves primarias de ambas tablas relacionadas.

Claves primarias y foráneas

Las claves primarias son atributos que identifican de manera única cada registro en una tabla. Por ejemplo, el ID de un cliente o el ID de un pedido o producto.

Las claves foráneas son atributos que establecen una relación entre dos tablas, apuntando a la clave primaria de otra tabla. Por ejemplo, en la tabla "Pedidos", el atributo Cliente_ID sería una clave foránea que hace referencia al ID de la tabla "Clientes".

En esta explicación hablaremos de atributos clave para referirnos tanto a claves primarias como foráneas, y de atributos no clave para referirnos a los demás atributos de una tabla.

Normalización

La normalización es el proceso de organizar los datos en tablas para reducir la redundancia y mejorar la integridad de los datos. Implica dividir las tablas en entidades más pequeñas y establecer relaciones entre ellas.

Formas normales

Existen varias formas normales, cada una con sus propias reglas para organizar los datos:

  • Primera forma normal (1NF): Elimina los grupos repetitivos y asegura que cada columna contenga valores atómicos.
  • Segunda forma normal (2NF): Elimina la dependencia parcial, asegurando que todos los atributos dependen completamente de la clave primaria.
  • Tercera forma normal (3NF): Elimina la dependencia transitiva, asegurando que los atributos no clave no dependen de otros atributos no clave.
  • Forma normal de Boyce-Codd (BCNF): Una versión más estricta de la tercera forma normal que elimina todas las dependencias funcionales no triviales.

Hay formas normales adicionales (4NF, 5NF, etc.), pero en la práctica, la mayoría de las bases de datos se normalizan hasta la 3NF o BCNF.

Advertencia sobre la normalización

Aunque puede sonar muy complicado, la normalización en la vida real no es tan difícil como parece. En la mayoría de los casos, con aplicar las tres primeras formas normales (1NF, 2NF y 3NF) es suficiente para tener una base de datos bien estructurada.

Si tu base de datos no usa claves primarias compuestas, y tus claves primarias son autoincrementales, es muy probable que tu base de datos ya esté en 3NF sin que te hayas dado cuenta.

Esto no siempre es posible, hay tablas que por su naturaleza no pueden ser normalizadas más allá de cierto punto sin perder eficiencia o funcionalidad. En esos casos, es importante evaluar los trade-offs entre normalización y rendimiento según las necesidades específicas de la aplicación.

1NF (Primera Forma Normal)

Una tabla está en 1NF si cumple con las siguientes condiciones:

  • Todos los atributos contienen valores atómicos (indivisibles), es decir, no hay listas o conjuntos de valores en una sola columna.
  • Cada columna contiene valores del mismo tipo de datos.

Veamos una tabla que no cumple con la 1NF:

CREATE TABLE movies (
    id,
    titulo,
    generos
);
INSERT INTO movies (id, titulo, generos) VALUES
(1, 'Inception', 'Sci-Fi, Action'),
(1, 'The Dark Knight', 'Action, Drama'),
('3', 'Interstellar', 'Sci-Fi, Drama');

Esta tabla presenta varios problemas que violan la 1NF:

  1. La columna generos contiene múltiples valores separados por comas, lo que significa que no es atómica. Esto provocará dificultades al intentar realizar consultas o análisis basados en géneros específicos, ¿cómo buscar todas las películas de "Action" si los géneros están almacenados como una cadena de texto? Podríamos usar LIKE, pero esto no es eficiente ni escalable a largo plazo.
  2. No hay una clave primaria definida, lo que lleva a inconsistencias en los datos.
  3. La columna id no tiene un tipo de dato definido, lo que puede causar inconsistencias en los datos almacenados y dificultar las operaciones de comparación o búsqueda.

Para normalizar esta tabla a 1NF, debemos asegurarnos de que cada columna contenga valores atómicos y definir una clave primaria adecuada. Podemos crear una tabla separada para los géneros y establecer una relación entre las películas y sus géneros:

DROP TABLE IF EXISTS movies;
DROP TABLE IF EXISTS genres;
DROP TABLE IF EXISTS movie_genres;
CREATE TABLE movies (
    id INTEGER PRIMARY KEY,
    titulo TEXT
);

CREATE TABLE genres (
    id INTEGER PRIMARY KEY,
    nombre TEXT
);

CREATE TABLE movie_genres (
    movie_id INTEGER,
    genre_id INTEGER,
    FOREIGN KEY (movie_id) REFERENCES movies(id),
    FOREIGN KEY (genre_id) REFERENCES genres(id)
);

INSERT INTO movies (id, titulo) VALUES
(1, 'Inception'),
(2, 'The Dark Knight'),
(3, 'Interstellar');
INSERT INTO genres (id, nombre) VALUES
(1, 'Sci-Fi'),
(2, 'Action'),
(3, 'Drama');
INSERT INTO movie_genres (movie_id, genre_id) VALUES
(1, 1),
(1, 2),
(2, 2),
(2, 3),
(3, 1),
(3, 3);

SELECT movies.id, movies.titulo, genres.nombre
FROM movies
JOIN movie_genres ON movies.id = movie_genres.movie_id
JOIN genres ON movie_genres.genre_id = genres.id;

Ahora, cada género está representado como un registro separado en la tabla genres, y la tabla movie_genres establece una relación muchos a muchos entre películas y géneros. Esto cumple con la 1NF al garantizar que todos los atributos contienen valores atómicos y que cada columna contiene valores del mismo tipo de datos.

+----+-----------------+--------+
| id |     titulo      | nombre |
+----+-----------------+--------+
| 1  | Inception       | Sci-Fi |
| 1  | Inception       | Action |
| 2  | The Dark Knight | Action |
| 2  | The Dark Knight | Drama  |
| 3  | Interstellar    | Sci-Fi |
| 3  | Interstellar    | Drama  |
+----+-----------------+--------+

Ahora la tabla está en 1NF, ya que cada columna contiene valores atómicos y del mismo tipo de datos, y hemos definido claves primarias adecuadas para cada tabla.

Además, gracias a esta estructura, ahora podemos realizar consultas más eficientes y precisas, como buscar todas las películas de un género específico o listar todos los géneros asociados a una película determinada.

Esta tabla, además, ya cumple con la 2NF y la 3NF!

2NF (Segunda Forma Normal)

La segunda forma normal (2NF) se basa en la 1NF y añade la condición de que todos los atributos no clave deben depender completamente de la clave primaria.

Para esto:

  • La tabla debe estar en 1NF.
  • No debe haber dependencias parciales, es decir, ningún atributo no clave debe depender solo de una parte de una clave primaria compuesta.

Si tu tabla tiene una clave primaria simple (no compuesta), entonces automáticamente está en 2NF si ya está en 1NF, ya que no puede haber dependencias parciales en ese caso.

Veamos una tabla que cumple con 1NF pero no con 2NF, para esto necesitaremos una tabla con clave primaria compuesta:

DROP TABLE IF EXISTS inscripciones;
CREATE TABLE inscripciones (
    estudiante_id INTEGER,
    asignatura_id INTEGER,
    profesor TEXT,
    departamento TEXT,
    nota INTEGER,
    PRIMARY KEY (estudiante_id, asignatura_id)
);

INSERT INTO inscripciones (estudiante_id, asignatura_id, profesor, departamento, nota) VALUES
(1, 101, 'Juan Pérez', 'Matemáticas', 9),
(2, 101, 'Juan Pérez', 'Matematicas', 8),
(3, 101, 'J. Pérez', 'Matemáticas', 7),
(1, 102, 'María López', 'Lengua', 6),
(2, 103, 'Carlos Ruiz', 'Historia', 5),
(3, 103, 'Carlos Ruiz', 'Historia', 6);

SELECT * FROM inscripciones;

Esta tabla cumple con la 1NF:

  1. Todos los atributos contienen valores atómicos.
  2. Cada columna contiene valores del mismo tipo de datos.

Pero vemos claramente los problemas de redundancia y dependencia parcial:

  1. Estamos repitiendo el nombre del profesor y el departamento para cada inscripción en una asignatura. ¿Y si el profesor se cambia? Tendríamos que actualizar múltiples registros, lo que puede llevar a inconsistencias.
  2. El atributo profesor y departamento dependen solo de asignatura_id, no tienen relación con estudiante_id. Esto viola la 2NF.

Para normalizar esta tabla a 2NF, debemos eliminar las dependencias parciales creando tablas separadas para los profesores y las asignaturas:

DROP TABLE IF EXISTS profesores;
DROP TABLE IF EXISTS asignaturas;
DROP TABLE IF EXISTS grupos;
DROP TABLE IF EXISTS inscripciones;

-- Tabla de profesores
CREATE TABLE profesores (
    profesor_id INTEGER PRIMARY KEY,
    nombre TEXT
);

-- Tabla de asignaturas
CREATE TABLE asignaturas (
    asignatura_id INTEGER PRIMARY KEY,
    nombre TEXT
);

-- Cada grupo relaciona una asignatura con un profesor
CREATE TABLE grupos (
    grupo_id INTEGER PRIMARY KEY,
    asignatura_id INTEGER,
    profesor_id INTEGER,
    FOREIGN KEY (asignatura_id) REFERENCES asignaturas(asignatura_id),
    FOREIGN KEY (profesor_id) REFERENCES profesores(profesor_id)
);

-- Inscripciones: qué estudiante está en qué grupo
CREATE TABLE inscripciones (
    estudiante_id INTEGER,
    grupo_id INTEGER,
    nota INTEGER,
    PRIMARY KEY (estudiante_id, grupo_id),
    FOREIGN KEY (grupo_id) REFERENCES grupos(grupo_id)
);
INSERT INTO profesores (profesor_id, nombre) VALUES
(1, 'Juan Pérez'),
(2, 'María López'),
(3, 'Carlos Ruiz');

INSERT INTO asignaturas (asignatura_id, nombre) VALUES
(101, 'Matemáticas'),
(102, 'Lengua'),
(103, 'Historia');

INSERT INTO grupos (grupo_id, asignatura_id, profesor_id) VALUES
(1, 101, 1),
(2, 102, 2),
(3, 103, 3);

INSERT INTO inscripciones (estudiante_id, grupo_id, nota) VALUES
(1, 1, 9),
(2, 1, 8),
(3, 1, 7),
(1, 2, 6),
(2, 3, 5),
(3, 3, 6);

SELECT inscripciones.estudiante_id, asignaturas.nombre AS asignatura, profesores.nombre AS profesor, inscripciones.nota
FROM inscripciones
JOIN grupos ON inscripciones.grupo_id = grupos.grupo_id
JOIN asignaturas ON grupos.asignatura_id = asignaturas.asignatura_id
JOIN profesores ON grupos.profesor_id = profesores.profesor_id;

Ahora cada tabla tiene una única responsabilidad:

  • La tabla profesores almacena información sobre los profesores.
  • La tabla asignaturas almacena información sobre las asignaturas.
  • La tabla grupos relaciona asignaturas con profesores.
  • La tabla inscripciones relaciona estudiantes con grupos y sus notas.
+---------------+-------------+-------------+------+
| estudiante_id | asignatura  |  profesor   | nota |
+---------------+-------------+-------------+------+
| 1             | Matemáticas | Juan Pérez  | 9    |
| 2             | Matemáticas | Juan Pérez  | 8    |
| 3             | Matemáticas | Juan Pérez  | 7    |
| 1             | Lengua      | María López | 6    |
| 2             | Historia    | Carlos Ruiz | 5    |
| 3             | Historia    | Carlos Ruiz | 6    |
+---------------+-------------+-------------+------+

Una regla sencilla a seguir es usar claves simples siempre que sea posible, y usar claves compuestas cuando hagamos tablas intermedias para relaciones muchos a muchos. No siempre podrás seguir esta regla dependiendo de la naturaleza de los datos, pero en el 90% de los casos te salvará de problemas de normalización.

Esta tabla, además, ya cumple con la 3NF, ya que no existen dependencias transitivas entre los atributos no clave.

3NF (Tercera Forma Normal)

La tercera forma normal (3NF) se basa en la 2NF y añade la condición de que no debe haber dependencias transitivas entre los atributos no clave.

Debemos cumplir con lo siguiente:

  • La tabla debe estar en 2NF.
  • Ningún atributo no clave debe depender de otro atributo no clave.

Veamos un ejemplo de una tabla que cumple con 2NF pero no con 3NF:

DROP TABLE IF EXISTS alumnos;
DROP TABLE IF EXISTS asignaturas;
DROP TABLE IF EXISTS matriculas;

CREATE TABLE alumnos (
    id_alumno INTEGER PRIMARY KEY,
    nombre TEXT NOT NULL,
    id_ciudad INTEGER NOT NULL,
    ciudad TEXT NOT NULL
);

CREATE TABLE asignaturas (
    id_asignatura INTEGER PRIMARY KEY,
    nombre TEXT NOT NULL,
    id_profesor INTEGER NOT NULL,
    profesor TEXT NOT NULL
);

CREATE TABLE matriculas (
    id_alumno INTEGER,
    id_asignatura INTEGER,
    nota INTEGER,
    PRIMARY KEY (id_alumno, id_asignatura),
    FOREIGN KEY (id_alumno) REFERENCES alumnos(id_alumno),
    FOREIGN KEY (id_asignatura) REFERENCES asignaturas(id_asignatura)
);

INSERT INTO alumnos (id_alumno, nombre, id_ciudad, ciudad) VALUES
(1, 'Ana', 1, 'Madrid'),
(2, 'Luis', 1, 'Madrid'),
(3, 'Marta', 2, 'Barcelona');

INSERT INTO asignaturas (id_asignatura, nombre, id_profesor, profesor) VALUES
(1, 'Bases de Datos', 1, 'Diego Mariscal'),
(2, 'Redes', 2, 'María López'),
(3, 'Seguridad', 1, 'Diego Mariscal');

INSERT INTO matriculas (id_alumno, id_asignatura, nota) VALUES
(1, 1, 8),
(1, 2, 9),
(2, 1, 6),
(3, 3, 10);

Analiza esta tabla, vemos que:

  1. ¿Y si queremos cambiar el nombre de un profesor? Tendríamos que actualizar múltiples registros en la tabla asignaturas, lo que puede llevar a inconsistencias, y en una tabla con gran cantidad de datos, esto puede ser ineficiente o complicar demasiado las actualizaciones.
  2. El atributo profesor depende de id_profesor, no de la clave primaria completa. Esto viola la 3NF.

Para normalizar esta tabla a 3NF, debemos eliminar las dependencias transitivas creando tablas separadas para las ciudades y los profesores:

DROP TABLE IF EXISTS alumnos;
DROP TABLE IF EXISTS ciudades;
DROP TABLE IF EXISTS asignaturas;
DROP TABLE IF EXISTS profesores;
DROP TABLE IF EXISTS matriculas;

CREATE TABLE ciudades (
    id_ciudad INTEGER PRIMARY KEY,
    nombre TEXT NOT NULL
);

CREATE TABLE alumnos (
    id_alumno INTEGER PRIMARY KEY,
    nombre TEXT NOT NULL,
    id_ciudad INTEGER NOT NULL,
    FOREIGN KEY (id_ciudad) REFERENCES ciudades(id_ciudad)
);

CREATE TABLE profesores (
    id_profesor INTEGER PRIMARY KEY,
    nombre TEXT NOT NULL
);

CREATE TABLE asignaturas (
    id_asignatura INTEGER PRIMARY KEY,
    nombre TEXT NOT NULL,
    id_profesor INTEGER NOT NULL,
    FOREIGN KEY (id_profesor) REFERENCES profesores(id_profesor)
);

CREATE TABLE matriculas (
    id_alumno INTEGER,
    id_asignatura INTEGER,
    nota INTEGER,
    PRIMARY KEY (id_alumno, id_asignatura),
    FOREIGN KEY (id_alumno) REFERENCES alumnos(id_alumno),
    FOREIGN KEY (id_asignatura) REFERENCES asignaturas(id_asignatura)
);

INSERT INTO ciudades (id_ciudad, nombre) VALUES
(1, 'Madrid'),
(2, 'Barcelona');

INSERT INTO alumnos (id_alumno, nombre, id_ciudad) VALUES
(1, 'Ana', 1),
(2, 'Luis', 1),
(3, 'Marta', 2);

INSERT INTO profesores (id_profesor, nombre) VALUES
(1, 'Diego Mariscal'),
(2, 'María López');

INSERT INTO asignaturas (id_asignatura, nombre, id_profesor) VALUES
(1, 'Bases de Datos', 1),
(2, 'Redes', 2),
(3, 'Seguridad', 1);

INSERT INTO matriculas (id_alumno, id_asignatura, nota) VALUES
(1, 1, 8),
(1, 2, 9),
(2, 1, 6),
(3, 3, 10);

De nuevo, puedes ver que cada tabla tiene una única responsabilidad:

  • La tabla ciudades almacena información sobre las ciudades.
  • La tabla profesores almacena información sobre los profesores.
  • La tabla asignaturas relaciona asignaturas con profesores.
  • La tabla alumnos relaciona alumnos con ciudades.
  • La tabla matriculas relaciona alumnos con asignaturas y sus notas.

En general, puedes observar que llegar a 3NF no es más que seguir las mismas reglas que para llegar a 2NF, pero prestando atención a las dependencias entre los atributos no clave.

De nuevo, haciendo uso de claves simples siempre que sea posible, y claves compuestas solo para tablas intermedias de relaciones muchos a muchos, te ayudará a evitar problemas de normalización.

Resumen

Una clave única por tabla, claves compuestas para tablas intermedias, y dividir las tablas según las dependencias entre atributos es la clave para un buen diseño de base de datos relacional.

Normalizar hasta 3NF es generalmente suficiente para la mayoría de las aplicaciones, asegurando que los datos estén organizados de manera eficiente y que se mantenga la integridad de los mismos.