Datos y bases de datos

Diseño de bases de datos y normalización: de las entidades a la FNBC

Principiante9 min de lecturaActualizado

Esta página se tradujo del inglés con ayuda de IA y se cotejó con el original. Leer la versión en inglés

La normalización no es una ceremonia académica: es una forma mecánica de eliminar la redundancia que provoca las anomalías de actualización. Esta guía modela un esquema pequeño, deduce sus dependencias funcionales y lo lleva de una hoja de cálculo sin normalizar hasta la FNBC; después trata las claves subrogadas frente a las naturales, la integridad referencial, la desnormalización deliberada y las señales de que un diseño tiene problemas.

Lo que aprenderás

  • Identificar entidades, relaciones, claves candidatas y cardinalidades a partir de la descripción de un dominio de negocio
  • Escribir dependencias funcionales y usarlas para detectar redundancia
  • Llevar una tabla desde la forma no normalizada a la 1FN, la 2FN, la 3FN y la FNBC
  • Elegir entre claves subrogadas y naturales y garantizar la integridad con restricciones
  • Decidir cuándo está justificada la desnormalización y cómo hacerla de forma segura

El diseño de la base de datos es donde se deciden los problemas futuros de un proyecto. Una aplicación se puede reescribir en un fin de semana; un esquema que lleva tres años en producción, con millones de filas y una docena de consumidores, no. La buena noticia es que casi toda esta disciplina se puede aprender y es bastante mecánica: describe el dominio con honestidad, anota qué valores determinan qué otros valores y deja que esas dependencias te indiquen dónde dividir las tablas.

Esta guía desarrolla un único ejemplo pequeño de principio a fin. El dominio es una conferencia: los asistentes se inscriben en sesiones, las sesiones se celebran en salas y los ponentes presentan temas. Es deliberadamente anodino, porque lo interesante no es el dominio, sino la redundancia que aparece cuando lo registras sin cuidado.

Entidades, relaciones y claves

Empieza por nombrar las cosas de las que habla el negocio. Cada sustantivo con existencia propia y atributos propios es una entidad candidata: asistente, sesión, sala, ponente. Cada verbo que las conecta es una relación: un asistente se inscribe en una sesión; una sesión se celebra en una sala.

La cardinalidad de la relación decide cómo se almacena:

  • Uno a muchos: una sala acoge muchas sesiones y una sesión está en una sola sala. Pon la clave foránea en el lado «muchos»: session.room_id.
  • Muchos a muchos: un asistente se inscribe en muchas sesiones y una sesión tiene muchos asistentes. Esto necesita su propia tabla, llamada normalmente tabla de unión o asociativa, cuya clave primaria es el par de claves foráneas. Los atributos de la propia relación (la hora de inscripción, si el asistente llegó a presentarse) también van ahí.
  • Uno a uno: realmente poco frecuente. Suele indicar dos entidades que podrían ser una sola tabla, o una extensión opcional que prefieres mantener aparte por tamaño o por control de acceso.

Una clave candidata es un conjunto mínimo de columnas cuyos valores son únicos en todas las filas y nunca nulos. Una tabla puede tener varias; la que eliges como clave primaria es una cuestión de comodidad, no de verdad. attendee.email y attendee.badge_number podrían ser ambas claves candidatas.

Conviene definir pronto dos términos, porque la normalización se formula con ellos. Una columna es un atributo primo si pertenece a alguna clave candidata, y un atributo no primo en caso contrario. Cada una de las formas normales que siguen es una regla sobre cómo pueden depender los atributos no primos de los primos.

Dependencias funcionales

Una dependencia funcional, escrita A → B, significa que dos filas cualesquiera que coinciden en A deben coincidir en B. Es una afirmación sobre el negocio, no sobre los datos que tengas en un momento dado. «Dos sesiones en la misma sala tienen la misma capacidad de sala» es una dependencia; «ahora mismo no hay dos asistentes con el mismo apellido» es una casualidad.

Supón que alguien te entrega esta hoja de cálculo, exportada como una única tabla:

attendee_email | attendee_name | session_code | session_title | room | room_capacity | starts_at           | topics
[email protected]| Ana Ruiz      | S12          | Query plans   | R2   | 120           | 2026-05-04 09:00    | sql, performance
[email protected]| Ana Ruiz      | S19          | Sharding      | R1   | 300           | 2026-05-04 11:00    | nosql, scaling
[email protected]| Bob Neri      | S12          | Query plans   | R2   | 120           | 2026-05-04 09:00    | sql, performance

Las dependencias que confirma el negocio son:

attendee_email                -> attendee_name
session_code                  -> session_title, room, starts_at
room                          -> room_capacity
(attendee_email, session_code)-> everything (the composite key)

La redundancia se ve ahora como una regla, no como una corazonada: como session_code → room, cada inscripción adicional en S12 repite «R2» y «120». Esa repetición es la causa de las tres anomalías clásicas. La anomalía de actualización: renombrar una sesión obliga a editar muchas filas, y si se te escapa una, la tabla se contradice. La anomalía de inserción: no puedes registrar la capacidad de una sala hasta que alguien se inscriba en una sesión que se celebre allí. La anomalía de borrado: cancelar la última inscripción en S12 borra incluso el hecho de que S12 existe.

Aplicar las formas normales

Cada forma normal elimina un tipo de dependencia. Alcanzar una forma implica cumplir también todas las anteriores.

Primera forma normal

La 1FN exige que cada columna contenga un único valor atómico de un solo tipo y prohíbe los grupos repetitivos. La columna topics no la cumple: mete una lista dentro de una cadena, de modo que no puedes indexarla, hacer joins sobre ella, restringirla ni contar temas sin analizar texto. Las columnas llamadas phone1, phone2, phone3 suspenden la misma prueba en la dirección contraria.

La solución es una tabla:

CREATE TABLE session_topic (
  session_code  varchar(8) NOT NULL REFERENCES session(code) ON DELETE CASCADE,
  topic         varchar(40) NOT NULL,
  PRIMARY KEY (session_code, topic)
);

Una pregunta habitual: ¿una columna jsonb o de tipo array rompe la 1FN? Formalmente sí; en la práctica, depende. Guardar en una columna de documento contenidos realmente opacos y variables es una decisión de ingeniería razonable; guardar datos estructurados por los que filtras y haces joins habitualmente es esquivar la 1FN con pasos extra.

Segunda forma normal

La 2FN solo se aplica cuando la clave primaria es compuesta. Prohíbe la dependencia parcial: un atributo no primo que depende de una parte de la clave en lugar de la clave completa. Aquí la clave es (attendee_email, session_code), y sin embargo session_title depende solo de session_code, y attendee_name solo de attendee_email.

Dividir según esas dependencias da tres tablas —attendee, session y la tabla de unión de inscripciones—, y esta última conserva únicamente lo que depende de la clave completa, como signed_up_at.

Tercera forma normal

La 3FN prohíbe la dependencia transitiva: un atributo no primo determinado por otro atributo no primo. En la tabla session se cumple session_code → room y room → room_capacity, así que la capacidad depende transitivamente de la clave. Muévela:

CREATE TABLE room (
  code      varchar(8) PRIMARY KEY,
  capacity  integer NOT NULL CHECK (capacity > 0)
);

CREATE TABLE session (
  code      varchar(8) PRIMARY KEY,
  title     varchar(120) NOT NULL,
  room_code varchar(8) NOT NULL REFERENCES room(code),
  starts_at timestamptz NOT NULL
);

Ahora la capacidad se guarda una sola vez. Corregirla es una actualización de una sola fila que no puede dejar la base de datos en un estado incoherente.

Forma normal de Boyce-Codd

La FNBC endurece la 3FN: todo determinante debe ser una clave candidata, incluidos los determinantes de atributos primos. La diferencia solo aparece cuando las claves candidatas se solapan.

Toma una tabla que registra qué ponente cubre qué tema en una sesión, con la regla de que un mismo ponente siempre cubre un único tema:

speaker_assignment(session_code, topic, speaker)
  (session_code, topic)   -> speaker      -- candidate key
  (session_code, speaker) -> topic        -- candidate key
  speaker                 -> topic        -- determinant that is NOT a key

La tabla está en 3FN —topic es primo, así que no hay dependencia transitiva sobre un atributo no primo—, pero el dato ponente-tema se repite en cada sesión en la que aparece ese ponente. Descomponla en speaker(name, topic) y session_speaker(session_code, speaker). Sé honesto con la contrapartida: esta descomposición es sin pérdida, pero no conserva la clave compuesta original como restricción de una sola tabla, y esa es una de las razones por las que la 3FN es a veces el punto de parada pragmático.

La cuarta forma normal va un paso más allá y separa los hechos multivaluados independientes. Si una sesión tuviera a la vez una lista de temas y una lista de patrocinadores sin relación entre sí, meter ambas en una misma tabla produciría una fila por cada combinación. Separarlas en dos tablas es la 4FN, y en cuanto la has visto una vez resulta casi de sentido común.

Claves e integridad referencial

Una clave natural es un dato con significado: un ISBN, un código de país, una dirección de correo. Una clave subrogada es un valor generado sin significado: una columna identity, una secuencia, un UUID.

Las claves naturales se leen bien en las consultas y ahorran un join cuando la tabla hija solo necesita el código (ORDER BY country_code no necesita ninguna búsqueda). Fallan cuando el identificador «inmutable» resulta ser mutable, cuando es lo bastante largo como para inflar cada índice que lo referencia o cuando un organismo externo lo redefine.

Las claves subrogadas son estables y compactas, y por eso son la opción por defecto más común. Usa una columna identity generada por la base de datos (GENERATED ALWAYS AS IDENTITY en PostgreSQL y en Oracle 12c o posterior), salvo que necesites crear los identificadores antes de la inserción o combinar datos de varios sistemas; en ese caso, un UUID encaja bien, y un generador de UUID resulta útil para sembrar filas de prueba. La regla crítica cuando usas una clave subrogada: declara igualmente una restricción de unicidad sobre la clave natural, o la tabla guardará sin inmutarse la misma entidad real dos veces.

Elijas lo que elijas, deja que la base de datos haga cumplir las reglas. Las claves foráneas garantizan que una inscripción no pueda referenciar una sesión inexistente; las restricciones NOT NULL y CHECK codifican hechos que la validación de la aplicación pasa por alto bajo concurrencia; UNIQUE frena duplicados que ningún código de aplicación puede evitar de forma fiable en cuanto corre en dos servidores. Elige el comportamiento de borrado a conciencia: ON DELETE CASCADE para filas que no pueden existir sin su padre, RESTRICT para todo lo que debería confirmar una persona y SET NULL solo donde el null signifique de verdad «ya no está vinculado». Indexa todas las columnas de clave foránea, o borrar un padre recorrerá la tabla hija entera; la guía de índices y planes de ejecución explica por qué.

Desnormalizar a propósito

Desnormalizar significa guardar un hecho dos veces a sabiendas para abaratar una lectura. Es una técnica legítima con un requisito de entrada: un mecanismo que mantenga coherentes las copias.

Entre los casos razonables están guardar un order_total calculado y mantenido por un trigger o por la misma transacción que escribe las líneas; copiar el precio histórico en una línea de pedido, que en realidad no es redundancia porque el precio actual del producto es otro hecho distinto; mantener una vista materializada o una tabla de resumen para un dashboard; y los esquemas en estrella de la analítica, cuyas tablas de dimensiones se desnormalizan a propósito porque los datos se cargan, no se editan (consulta business intelligence).

Los casos poco razonables comparten un rasgo: nada mantiene la copia fiel. La ciudad de un cliente copiada en cada pedido «para los informes», que nadie actualiza, se convierte en silencio en ficción.

Señales de alarma en un esquema

  • Entidad-atributo-valor. Una tabla (entity_id, attribute_name, value) promete flexibilidad infinita y entrega datos sin tipo, reglas imposibles de restringir y autojoins en cada consulta. Úsala solo para campos definidos de verdad por el usuario, y mejor aún, opta por una columna jsonb con una restricción CHECK de validación.
  • La tabla ancha. Ochenta columnas, casi todas nulas en casi todas las filas, suelen significar varias entidades compartiendo una tabla o una jerarquía de herencia aplanada sin pensar.
  • Todo admite null. Si casi todas las columnas permiten null, el esquema ha dejado de documentar el dominio. Cada columna nullable debería tener un significado explícito para su null.
  • Booleanos que deberían ser estados. is_active, is_cancelled e is_archived en una misma fila pueden codificar combinaciones imposibles. Una única columna status con una restricción CHECK, no.
  • Grupos de columnas repetidos. address_line_1 a address_line_5, o q1_total a q4_total, son filas que se hacen pasar por columnas.
  • Sin claves foráneas «por rendimiento». El coste de una clave foránea es una consulta al índice por escritura; el coste de los datos huérfanos es un impuesto permanente sobre cada consulta y cada desarrollador, y además priva al optimizador de información, como explica la guía de buenas prácticas de SQL.

Cuando hay un ORM encima, el esquema sigue siendo lo primero: los mapeos de entidades deben describir un buen diseño relacional, no dictarlo, algo que se trata en el mapeo de entidades JPA.

Puntos clave

  • Modela primero las entidades y las relaciones, y después escribe las dependencias funcionales; la redundancia es consecuencia de las dependencias, así que son ellas las que te dicen dónde dividir.
  • La 1FN elimina los grupos repetitivos, la 2FN las dependencias parciales de una clave compuesta, la 3FN las dependencias transitivas, y la FNBC exige que todo determinante sea una clave candidata.
  • La redundancia se manifiesta como anomalías de actualización, inserción y borrado: fallos concretos, no quejas de estilo.
  • Las claves subrogadas son la opción estable por defecto, pero mantén siempre una restricción de unicidad sobre la clave natural para que no puedan colarse duplicados.
  • Deja que las restricciones hagan cumplir el modelo: claves foráneas con reglas de borrado elegidas a conciencia, NOT NULL, CHECK y UNIQUE.
  • Desnormaliza solo para una consulta concreta, después de medir y únicamente con un mecanismo que mantenga correcto el valor duplicado.

Fuentes y lecturas recomendadas

Preguntas frecuentes

¿Basta con la tercera forma normal para una aplicación real?
Para la inmensa mayoría de los esquemas transaccionales, sí. La 3FN elimina la redundancia responsable de casi todas las anomalías de actualización que te encontrarás en la práctica, y la forma normal de Boyce-Codd solo se diferencia de ella en tablas con claves candidatas compuestas que se solapan, algo poco habitual fuera de los libros de texto. Apunta a la 3FN en todas partes, comprueba la FNBC donde se solapen claves compuestas y detente ahí, salvo que una anomalía concreta demuestre lo contrario.
¿La normalización hace más lentas las consultas?
Cambia filas más anchas por más joins. Los optimizadores modernos combinan tablas indexadas con mucha eficiencia, y unas tablas más estrechas implican más filas por página y menos I/O, así que los esquemas normalizados a menudo se leen más rápido, no más despacio. Las escrituras son casi siempre más rápidas, porque cada hecho se guarda una sola vez. Mide antes de suponer: un plan de ejecución te dirá si el coste real está en un join o en un recorrido completo.
¿Toda tabla debe tener una columna id autogenerada?
No de forma automática. Una clave subrogada es la opción por defecto adecuada para entidades cuyo identificador natural es inestable, largo o está controlado por terceros. Las tablas puramente de unión entre dos entidades no suelen necesitar ninguna columna extra: el par de claves foráneas ya es una clave primaria perfecta, y añadir un id deja pasar pares duplicados a menos que también declares una restricción de unicidad sobre ellos.
¿Cuándo es la desnormalización la respuesta correcta?
Cuando puedes nombrar la consulta que acelera, has medido que la forma normalizada es demasiado lenta y cuentas con un mecanismo que mantiene la copia correcta. Guardar el total calculado de un pedido, cachear un nombre para mostrar o construir una tabla de informes son decisiones razonables. Copiar un atributo mutable en una segunda tabla sin nada que mantenga ambas sincronizadas no lo es.