Cuando alguien empieza a diseñar bases de datos sin conocer las reglas de normalización, el resultado suele parecerse a una hoja de cálculo: columnas que se repiten, celdas con varios valores separados por comas, grupos de datos mezclados en la misma tabla. Todo parece funcionar hasta que intentas consultar, actualizar o escalar esos datos.
La Primera Forma Normal (abreviada 1FN o 1NF en inglés) es el punto de partida de cualquier diseño relacional correcto. Es la regla más básica de la normalización y, sin embargo, es la que más frecuentemente se viola cuando se diseña sin experiencia.
En este artículo aprenderás exactamente qué es la 1FN, cuáles son sus reglas, cómo reconocer cuándo una tabla la viola y cómo corregirla, todo con ejemplos concretos y visuales.
Qué es la Primera Forma Normal
Una tabla está en Primera Forma Normal cuando cumple cuatro condiciones:
- Atomicidad: cada celda contiene un único valor indivisible. No puede haber listas, conjuntos ni múltiples valores separados por comas en una sola celda.
- Homogeneidad: todos los valores de una columna son del mismo tipo de dato.
- Unicidad de columnas: cada columna tiene un nombre único. No puede haber dos columnas con el mismo nombre ni grupos de columnas repetidos como
telefono1,telefono2,telefono3. - Clave primaria: cada fila es única e identificable de forma inequívoca por una clave primaria.
La idea central es que la base de datos relacional no es una hoja de cálculo. Una celda almacena un valor y un solo valor. Si necesitas guardar varios teléfonos de un cliente, no los metes todos en una celda ni creas columnas repetidas: creas filas adicionales o una tabla separada.
Violación 1: valores múltiples en una celda
Es la violación más común y la más fácil de cometer. Ocurre cuando guardas varios valores en una sola celda, ya sea separados por comas, por punto y coma, por barras o cualquier otro separador.
Tabla que viola la 1FN:
| id_cliente | nombre | telefonos | categorias_interes |
|---|---|---|---|
| 1 | Ana García | 612345678, 911234567 | Electrónica, Libros, Ropa |
| 2 | Luis Pérez | 698765432 | Deportes |
| 3 | María López | 677889900, 934567890, 655443322 | Libros, Jardín |
Esta tabla tiene problemas graves:
- Consultar un teléfono específico es costoso. Para saber si el número 911234567 pertenece a algún cliente tendrías que buscar dentro de strings con LIKE o funciones de texto, lo cual es lento e impreciso.
- Contar los teléfonos por cliente es imposible de forma directa. No puedes hacer
COUNT(telefonos)porque hay varios valores en una celda. - Añadir un teléfono significa modificar una celda. Lo correcto en una base de datos relacional es insertar una fila, no editar texto dentro de una celda.
- Ordenar o filtrar por categoría es inmanejable. ¿Cómo obtienes todos los clientes interesados en "Libros"? Necesitarías LIKE '%Libros%', que es frágil y no usa índices.
Cómo corregirlo: una fila por valor
La solución para valores múltiples es crear una tabla separada donde cada valor ocupa su propia fila.
Tabla clientes (corregida):
| id_cliente (PK) | nombre |
|---|---|
| 1 | Ana García |
| 2 | Luis Pérez |
| 3 | María López |
Tabla telefonos_clientes (nueva):
| id (PK) | id_cliente (FK) | telefono | tipo |
|---|---|---|---|
| 1 | 1 | 612345678 | móvil |
| 2 | 1 | 911234567 | fijo |
| 3 | 2 | 698765432 | móvil |
| 4 | 3 | 677889900 | móvil |
| 5 | 3 | 934567890 | fijo |
| 6 | 3 | 655443322 | trabajo |
Tabla categorias_interes (nueva):
| id_cliente (FK) | categoria |
|---|---|
| 1 | Electrónica |
| 1 | Libros |
| 1 | Ropa |
| 2 | Deportes |
| 3 | Libros |
| 3 | Jardín |
Ahora buscar todos los clientes interesados en Libros es una consulta directa y eficiente:
SELECT c.nombre
FROM clientes c
JOIN categorias_interes ci ON ci.id_cliente = c.id
WHERE ci.categoria = 'Libros';Violación 2: columnas repetidas
Esta violación ocurre cuando se crean varias columnas para almacenar el mismo tipo de información, numerándolas para distinguirlas: telefono1, telefono2, telefono3.
Tabla que viola la 1FN:
| id_empleado | nombre | habilidad1 | habilidad2 | habilidad3 | habilidad4 |
|---|---|---|---|---|---|
| 1 | Carlos Ruiz | Python | JavaScript | SQL | NULL |
| 2 | Sofía Mora | Java | Docker | NULL | NULL |
| 3 | Pedro Díaz | Python | Machine Learning | TensorFlow | Pandas |
Los problemas son predecibles y graves:
- ¿Cuántas habilidades puede tener un empleado? La estructura actual dice que cuatro como máximo. Si alguien tiene cinco, hay que alterar la tabla y agregar una columna más, lo cual rompe código y consultas existentes.
- Los NULL desperdician espacio y complican las consultas.
- Buscar todos los empleados que saben Python requiere buscar en cuatro columnas distintas:
WHERE habilidad1 = 'Python' OR habilidad2 = 'Python' OR habilidad3 = 'Python' OR habilidad4 = 'Python'. Frágil, repetitivo y no escalable. - El orden de las columnas no tiene sentido semántico. ¿Por qué Python está en habilidad1 para Carlos y también en habilidad1 para Pedro? ¿Qué significa esa posición?
Cómo corregirlo: tabla separada con relación
Tabla empleados (corregida):
| id_empleado (PK) | nombre |
|---|---|
| 1 | Carlos Ruiz |
| 2 | Sofía Mora |
| 3 | Pedro Díaz |
Tabla empleado_habilidades (nueva):
| id_empleado (FK) | habilidad | nivel |
|---|---|---|
| 1 | Python | avanzado |
| 1 | JavaScript | intermedio |
| 1 | SQL | avanzado |
| 2 | Java | avanzado |
| 2 | Docker | intermedio |
| 3 | Python | avanzado |
| 3 | Machine Learning | avanzado |
| 3 | TensorFlow | intermedio |
| 3 | Pandas | avanzado |
Ahora la consulta para encontrar empleados con Python es simple, rápida y usa índices:
SELECT e.nombre, eh.nivel
FROM empleados e
JOIN empleado_habilidades eh ON eh.id_empleado = e.id
WHERE eh.habilidad = 'Python';Y si un empleado aprende una nueva habilidad, simplemente insertas una fila. No hay que alterar la estructura de la tabla.
Violación 3: valores no atómicos compuestos
Otra forma de violar la 1FN es guardar un valor que en realidad está compuesto por partes distintas y que se necesitará consultar por separado.
Tabla que viola la 1FN:
| id_pedido | cliente | direccion_entrega |
|---|---|---|
| 1 | Ana García | Calle Mayor 15, 3ºB, 28001, Madrid, España |
| 2 | Luis Pérez | Av. Diagonal 432, 08037, Barcelona, España |
El campo direccion_entrega mezcla calle, número, piso, código postal, ciudad y país en una sola celda. El problema aparece cuando necesitas:
- Filtrar todos los pedidos enviados a Madrid.
- Agrupar pedidos por código postal para optimizar rutas de reparto.
- Validar que el código postal tiene el formato correcto.
- Mostrar solo la ciudad en un informe.
Todo eso requiere parsear el texto, lo cual es costoso, frágil y proclive a errores si el formato de la dirección no es siempre igual.
Cómo corregirlo: separar en columnas atómicas
| id_pedido | cliente | calle | numero | piso | codigo_postal | ciudad | pais |
|---|---|---|---|---|---|---|---|
| 1 | Ana García | Calle Mayor | 15 | 3ºB | 28001 | Madrid | España |
| 2 | Luis Pérez | Av. Diagonal | 432 | NULL | 08037 | Barcelona | España |
Ahora cada parte de la dirección es consultable, filtrable e indexable de forma independiente.
Violación 4: ausencia de clave primaria
Sin una clave primaria, no hay forma de identificar una fila de forma única. Eso significa que no puedes actualizar un registro específico con certeza de que estás modificando el correcto, y no puedes crear relaciones entre tablas de forma confiable.
Tabla que viola la 1FN (sin clave primaria):
| nombre | ciudad | |
|---|---|---|
| Ana García | ana@mail.com | Madrid |
| Luis Pérez | luis@mail.com | Barcelona |
| Ana García | ana@mail.com | Sevilla |
¿Son la segunda y tercera fila la misma persona que se mudó, o dos personas distintas llamadas igual con el mismo email? Sin clave primaria, es imposible saberlo con certeza.
Cómo corregirlo
La solución más sencilla es agregar una columna de identificador único autoincremental:
CREATE TABLE clientes (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
ciudad VARCHAR(50)
);| id (PK) | nombre | ciudad | |
|---|---|---|---|
| 1 | Ana García | ana@mail.com | Madrid |
| 2 | Luis Pérez | luis@mail.com | Barcelona |
En este caso, la restricción UNIQUE en email también garantiza que no puede haber dos registros con el mismo correo, lo cual resuelve el duplicado anterior.
Ejemplo completo: hoja de cálculo a base de datos en 1FN
Veamos un caso realista. Una empresa lleva el registro de sus pedidos en una hoja de Excel así:
| Fecha | Cliente | Contacto | Productos comprados | Cantidades | Total |
|---|---|---|---|---|---|
| 2024-06-01 | TechCorp S.L. | Ana López, 612000111 | Laptop Pro, Mouse, Teclado | 2, 5, 5 | 3089.93 |
| 2024-06-03 | DataSystems | Juan Mora, 698111222 | Monitor 27", Hub USB | 3, 10 | 1399.87 |
Esta hoja viola la 1FN de múltiples formas:
Contactomezcla nombre y teléfono en una celda.Productos compradostiene múltiples valores en una celda.Cantidadestiene múltiples valores que deben emparejarse con los productos por posición (frágil e imposible de consultar).- No hay clave primaria.
Aplicando la 1FN, el diseño correcto en SQL sería:
-- Empresas clientes
CREATE TABLE empresas (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(150) NOT NULL
);
-- Contactos de cada empresa
CREATE TABLE contactos (
id INT AUTO_INCREMENT PRIMARY KEY,
id_empresa INT NOT NULL,
nombre VARCHAR(100) NOT NULL,
telefono VARCHAR(20),
FOREIGN KEY (id_empresa) REFERENCES empresas(id)
);
-- Catálogo de productos
CREATE TABLE productos (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
precio DECIMAL(10, 2) NOT NULL
);
-- Cabecera del pedido
CREATE TABLE pedidos (
id INT AUTO_INCREMENT PRIMARY KEY,
id_empresa INT NOT NULL,
fecha DATE NOT NULL,
FOREIGN KEY (id_empresa) REFERENCES empresas(id)
);
-- Líneas del pedido: una fila por producto, valor atómico
CREATE TABLE lineas_pedido (
id_pedido INT NOT NULL,
id_producto INT NOT NULL,
cantidad INT NOT NULL,
precio_unit DECIMAL(10, 2) NOT NULL,
PRIMARY KEY (id_pedido, id_producto),
FOREIGN KEY (id_pedido) REFERENCES pedidos(id),
FOREIGN KEY (id_producto) REFERENCES productos(id)
);Con este diseño los datos del ejemplo quedan así:
empresas:
| id | nombre |
|---|---|
| 1 | TechCorp S.L. |
| 2 | DataSystems |
contactos:
| id | id_empresa | nombre | telefono |
|---|---|---|---|
| 1 | 1 | Ana López | 612000111 |
| 2 | 2 | Juan Mora | 698111222 |
pedidos:
| id | id_empresa | fecha |
|---|---|---|
| 1 | 1 | 2024-06-01 |
| 2 | 2 | 2024-06-03 |
lineas_pedido:
| id_pedido | id_producto | cantidad | precio_unit |
|---|---|---|---|
| 1 | 1 | 2 | 1299.99 |
| 1 | 2 | 5 | 45.50 |
| 1 | 3 | 5 | 89.99 |
| 2 | 4 | 3 | 349.99 |
| 2 | 5 | 10 | 29.90 |
Ahora cualquier consulta es directa, eficiente y expresiva:
-- Calcular el total del pedido 1
SELECT SUM(cantidad * precio_unit) AS total
FROM lineas_pedido
WHERE id_pedido = 1;
-- Productos más vendidos
SELECT p.nombre, SUM(lp.cantidad) AS unidades_vendidas
FROM lineas_pedido lp
JOIN productos p ON p.id = lp.id_producto
GROUP BY p.nombre
ORDER BY unidades_vendidas DESC;
-- Todos los pedidos de TechCorp
SELECT pe.fecha, SUM(lp.cantidad * lp.precio_unit) AS total
FROM pedidos pe
JOIN lineas_pedido lp ON lp.id_pedido = pe.id
JOIN empresas e ON e.id = pe.id_empresa
WHERE e.nombre = 'TechCorp S.L.'
GROUP BY pe.id, pe.fecha;Un caso especial: ¿cuándo es aceptable un valor compuesto?
La 1FN exige atomicidad, pero la definición de "atómico" depende del contexto y de cómo se van a usar los datos. Si guardas una dirección en una columna direccion y nunca necesitarás filtrar por ciudad, código postal o calle de forma independiente, podría ser aceptable guardarla completa.
La pregunta clave es: ¿alguna vez necesitaré consultar, filtrar, ordenar o actualizar partes de este valor por separado? Si la respuesta es sí, debes atomizar. Si la respuesta es no, puedes mantenerlo compuesto.
Sin embargo, en la duda, siempre es mejor separar. Es mucho más fácil concatenar valores en una consulta que parsear y dividir texto almacenado incorrectamente.
-- Si separas, siempre puedes concatenar cuando lo necesites
SELECT CONCAT(calle, ' ', numero, ', ', codigo_postal, ' ', ciudad) AS direccion_completa
FROM pedidos;
-- Pero si guardas junto, no puedes separar de forma confiable
SELECT ciudad FROM pedidos; -- imposible si está todo en un stringLista de verificación: ¿tu tabla cumple la 1FN?
Antes de dar por válida una tabla, hazte estas preguntas:
- ¿Alguna celda puede contener una lista de valores separados por comas, punto y coma u otro delimitador?
- ¿Hay columnas que se llaman igual pero con número al final: campo1, campo2, campo3?
- ¿Alguna columna mezcla dos tipos de información distintos, como nombre y teléfono juntos?
- ¿Puedo identificar cada fila de forma única con una clave primaria?
- ¿Alguna columna podría tener valores de tipos distintos según la fila, como texto en unas filas y números en otras?
Si la respuesta a cualquiera de las primeras tres es sí, o la cuarta es no, la tabla viola la Primera Forma Normal y necesita ser rediseñada.
Conclusión
La Primera Forma Normal es el fundamento sobre el que se construye todo diseño relacional correcto. Sus reglas son simples en concepto: valores atómicos, sin columnas repetidas, clave primaria en cada tabla. Pero su impacto en la calidad, mantenibilidad y rendimiento de la base de datos es enorme.
Cada vez que sientas la tentación de guardar varios valores en una celda separados por comas, o de crear columnas como item1, item2, item3, recuerda que estás sacrificando la capacidad de consultar y mantener esos datos de forma eficiente. La solución siempre es la misma: una fila por valor, una columna por tipo de dato.
La 1FN es el primer paso. Una vez que tu diseño la cumple, el siguiente objetivo es aplicar la Segunda y Tercera Forma Normal para eliminar redundancias más sutiles. Y para entender el contexto completo del diseño relacional, te recomendamos leer nuestro artículo sobre cómo funciona el modelo entidad-relación y los diagramas ER, que es el paso previo a la normalización.
No hay comentarios todavía. Sé el primero en compartir tu opinión.