Normalización vs desnormalización — ¿cuál es el verdadero tradeoff?
No es una cuestión de estilo — la normalización te da integridad al escribir, la desnormalización te da velocidad al leer, y saber en qué capa de tu arquitectura va cada una es la habilidad real.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
Normalización vs desnormalización — ¿cuál es el verdadero tradeoff?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Dos fuerzas que tiran en direcciones opuestas
Normalización y desnormalización no son realmente "técnicas" que eliges una vez — son una tensión que existe en todo esquema que vayas a diseñar, y por eso este tradeoff aparece en casi toda entrevista backend: pone a prueba si de verdad entiendes por qué un esquema tiene la forma que tiene, no solo si puedes recitar las formas normales de memoria.
#Qué te da realmente la normalización
Normalizar es el proceso de organizar tablas para que cada dato se almacene exactamente una vez, siguiendo (de forma informal) tres reglas conocidas: la primera forma normal elimina los grupos repetidos (cada columna guarda un único valor atómico), la segunda forma normal elimina dependencias parciales sobre parte de una clave compuesta, y la tercera forma normal elimina dependencias transitivas (una columna que no es clave depende de otra columna que tampoco es clave, en lugar de depender de la clave primaria). En la práctica, pasar de una tabla tipo hoja de cálculo sin normalizar a 3FN significa dividir los datos en customers, orders, order_items y products, conectadas por claves foráneas.
La recompensa es integridad al momento de escribir: el nombre de un cliente vive en exactamente una fila, así que es imposible que el mismo cliente aparezca con dos formas distintas de escribir su nombre en pedidos diferentes. El almacenamiento también es más eficiente porque nada está duplicado.
-- Esquema normalizado (3FN)CREATE TABLE customers (customer_id SERIAL PRIMARY KEY,name VARCHAR(200) NOT NULL,email VARCHAR(200) NOT NULL UNIQUE);CREATE TABLE products (product_id SERIAL PRIMARY KEY,name VARCHAR(200) NOT NULL,price_cents INTEGER NOT NULL);CREATE TABLE orders (order_id SERIAL PRIMARY KEY,customer_id INTEGER NOT NULL REFERENCES customers(customer_id),placed_at TIMESTAMPTZ NOT NULL DEFAULT now());CREATE TABLE order_items (order_id INTEGER NOT NULL REFERENCES orders(order_id),product_id INTEGER NOT NULL REFERENCES products(product_id),quantity INTEGER NOT NULL,price_at_sale_cents INTEGER NOT NULL, -- snapshot, ver más abajoPRIMARY KEY (order_id, product_id));
Leer un pedido completo para generar un recibo ahora requiere un join entre cuatro tablas:
SELECT c.name, c.email, p.name, oi.quantity, oi.price_at_sale_centsFROM orders oJOIN customers c ON c.customer_id = o.customer_idJOIN order_items oi ON oi.order_id = o.order_idJOIN products p ON p.product_id = oi.product_idWHERE o.order_id = 42;
#Qué recuperas con la desnormalización
Desnormalizar reintroduce redundancia a propósito para evitar esos joins al momento de leer. Una fila order_items desnormalizada podría llevar directamente customer_name, customer_email y product_name, de modo que un simple SELECT * FROM order_items WHERE order_id = 42 sobre una sola tabla te da todo lo que necesita el recibo, sin ningún join.
-- Desnormalizado: todo lo necesario para un recibo en una sola filaCREATE TABLE order_items_flat (order_id INTEGER NOT NULL,product_id INTEGER NOT NULL,customer_name VARCHAR(200) NOT NULL, -- duplicadocustomer_email VARCHAR(200) NOT NULL, -- duplicadoproduct_name VARCHAR(200) NOT NULL, -- duplicadoquantity INTEGER NOT NULL,price_at_sale_cents INTEGER NOT NULL,PRIMARY KEY (order_id, product_id));
Esa lectura es más rápida, especialmente a escala, porque evita el costo del join y puede resolverse con un solo recorrido de índice. El costo aparece en las escrituras: si un cliente cambia su nombre, o actualizas cada fila histórica de order_items_flat que le pertenece (costoso, y hasta cuestionable — más sobre esto abajo) o aceptas que el nombre queda inconsistente entre filas.
#El matiz que los entrevistadores realmente buscan: corrección, no solo velocidad
Aquí está el matiz que separa una respuesta junior de una senior: price_at_sale_cents en el ejemplo de arriba también es un dato duplicado, copiado de products.price_cents — pero no es optimización prematura, es un requisito de corrección. Si el precio del producto cambia el mes que viene, los pedidos históricos deben seguir mostrando lo que el cliente realmente pagó. Normalizar ese campo y siempre hacer join contra la tabla products viva reescribiría silenciosamente la historia cada vez que cambie un precio. Así que no toda columna duplicada es "desnormalización por rendimiento" — parte de la duplicación es una instantánea deliberada que exige la lógica de negocio, independientemente de cualquier preocupación de velocidad.
#Un término medio práctico
La mayoría de los sistemas en producción terminan en: normalizar el núcleo OLTP por corrección, y luego desnormalizar en cada capa que lo lee — vistas materializadas que se refrescan en un horario o mediante triggers, un modelo de lectura estilo CQRS actualizado de forma asíncrona a partir de eventos de dominio, un índice de búsqueda como Elasticsearch que duplica a propósito una forma aplanada de tus datos. Eso te da lecturas rápidas y desnormalizadas sin que dos sistemas lleguen jamás a estar en desacuerdo sobre lo que dice realmente la fuente de verdad.
Cuando te hagan esta pregunta, nombra el tradeoff explícitamente — velocidad de lectura vs. integridad de escritura y almacenamiento — y luego demuestra que sabes dónde corresponde la desnormalización en una arquitectura real: no en lugar de tu núcleo normalizado, sino en capas por encima de él.