Ir al contenido principal

Desnormalización en bases de datos: Cuándo y cómo utilizarlo

Descubre cómo la desnormalización mejora el rendimiento de lectura al reducir las uniones y simplificar las consultas. Comprende las ventajas e inconvenientes, las técnicas y los casos de uso que lo convierten en una potente herramienta para los sistemas de análisis y generación de informes.
Actualizado 6 oct 2025  · 15 min de lectura

Heescrito mucho sobre la normalización y por qué es una base tan fiable para la integridad de los datos. No estoy contradiciéndome aquí. Sigo creyendo que un esquema bien normalizado es el punto de partida adecuado para la mayoría de los sistemas transaccionales. Este artículo trata sobre la decisión deliberada de ir en contra de esa pureza en situaciones muy específicas en las que el rendimiento de lectura es más importante que la forma normal estricta.

La desnormalización no es omitir la normalización. Es una optimización del rendimiento que se aplica a un modelo normalizado cuando las consultas reales, los usuarios reales y los SLA reales indican que las combinaciones y los cálculos sobre la marcha son demasiado lentos o costosos. En la práctica, se intercambia una lectura más rápida y consultas más sencillas por más almacenamiento, escrituras más complejas y un trabajo adicional de consistencia.

En este artículo, te mostraré cuándo es útil la desnormalización y cuándo no lo es, y cómo implementarla de forma segura en bases de datos SQL. El objetivo no es abandonar el buen diseño, sino añadir atajos cuando la carga de trabajo lo justifique.

¿Qué es la desnormalización en las bases de datos?

Si estás familiarizado con la normalización y el diseño de bases de datos, aquí tienes la respuesta breve: 

La desnormalización es el acto deliberado de añadir datos redundantes a un esquema previamente normalizado para acelerar las lecturas y simplificar las consultas. Se trata de una optimización del rendimiento específica, ¡no una excusa para saltarse un buen modelado!

Si eres bastante nuevo en el diseño de bases de datos, tal vez te convenga analizar el significado de «normalizado», «desnormalizado» y «no normalizado». Estos tres términos se utilizan con bastante frecuencia en Internet, y es importante no confundirlos.

Normalizado: Los datos se dividen en tablas bien estructuradas que minimizan la redundancia y protegen la integridad ee (véase 3NF/BCNF).

Desnormalizado: Reintroduces la redundancia selectiva con columnas adicionales, valores precalculados o tablas preunidas, además de ese modelo normalizado, para acelerar las lecturas comunes. Con la desnormalización, se mantiene una única fuente de información veraz (las tablas normalizadas) y, a continuación, se conservan una o varias representaciones más rápidas para las rutas más utilizadas, como paneles de control, listados de productos, búsquedas, etc. Intercambias almacenamiento + complejidad de escritura por velocidad de lectura + consultas más sencillas.

Sin normalizar: Datos sin procesar, ad hoc o desordenados, cuya estructura y restricciones nunca se diseñaron adecuadamente. Eso no es lo que estamos haciendo aquí.

Ejemplo en SQL

Aquí tienes un pequeño ejemplo en SQL que te ayudará a visualizar la diferencia entre el modelado normalizado y el desnormalizado.

Modelización normalizada

-- Source of truth
CREATE TABLE customers (
  customer_id   BIGINT PRIMARY KEY,
  name          TEXT NOT NULL,
  tier          TEXT NOT NULL
);

CREATE TABLE orders (
  order_id      BIGINT PRIMARY KEY,
  customer_id   BIGINT NOT NULL REFERENCES customers(customer_id),
  order_total   NUMERIC(12,2) NOT NULL,
  created_at    TIMESTAMP NOT NULL DEFAULT now()
);

-- Typical report needs a join
SELECT c.name, c.tier, SUM(o.order_total) AS revenue
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
GROUP BY c.name, c.tier;

Desnormalizado para una ruta de informes

-- Add redundant fields for faster reads
ALTER TABLE orders
  ADD COLUMN customer_name TEXT,
  ADD COLUMN customer_tier TEXT;

-- Now most reports avoid the join
SELECT customer_name, customer_tier, SUM(order_total) AS revenue
FROM orders
GROUP BY customer_name, customer_tier;

En este ejemplo, mantendrías a los clientes como fuente de verdad y te asegurarías de que orders.customer_name / orders.customer_tier permanecieran sincronizados (por ejemplo, mediante un desencadenador, una tarea CDC o un relleno programado). La misma verdad, dos representaciones, cada una optimizada para una tarea diferente.

¿La desnormalización es un mal diseño?

Existe la idea errónea de que la desnormalización es solo el resultado de un mal diseño. No es así si se mide, se delimita y se mantiene. Sí, la desnormalización a menudo viola las formas normales superiores (ese es el objetivo), pero es intencionada y está respaldada por un plan para preservar la coherencia. Un mal diseño es omitir por completo la normalización o introducir redundancias sin una estrategia de sincronización.

Si deseas obtener más información sobre el diseño de bases de datos , nuestro curso para principiantes sobre estetema es un buen punto de partida.

Normalización frente a desnormalización

Aquí tienes la comparación lado a lado. No es exhaustivo, pero te ayudará a comprender las ventajas e inconvenientes de cada enfoque.

Aspecto

Normalización

Desnormalización

Objetivo principal

Integridad, redundancia mínima, actualizaciones fáciles y correctas.

Lecturas más rápidas, consultas más sencillas en rutas activas

Ideal para

Sistemas OLTP con escrituras/actualizaciones frecuentes

Paneles con gran cantidad de información, páginas de búsqueda/listados, informes/análisis.

Leer rendimiento

A menudo requiere uniones, funciona bien con los índices adecuados.

Menos uniones, puede ser mucho más rápido y predecible.

Complejidad de la escritura

Simple: única fuente de verdad

Más alto: debes actualizar/sincronizar copias redundantes o agregados.

Almacenamiento

Lean

Más grande (columnas/tablas adicionales, vistas precalculadas)

Integridad de los datos

Diseño robusto (restricciones 3NF/BCNF)

Requiere mecanismos para evitar desviaciones (desencadenantes, CDC, trabajos).

Velocidad de cambio

Los cambios de nombre y las actualizaciones de las columnas se localizan.

Los cambios pueden propagarse por los datos duplicados.

Gastos generales operativos

Inferior: menos piezas móviles.

Más alto: políticas de actualización, rellenos, supervisión.

Modos de fallo

N+1 consultas, uniones lentas, índices faltantes

Datos obsoletos, inconsistencia, amplificación de escritura

Evolución del esquema

Previsible, fácil de refactorizar

Necesita planes de migración para representaciones redundantes.

Ejemplos típicos

Pedidos, clientes, transacciones

Listas de productos previnculadas; tablas de ventas agregadas; vistas materializadas/indexadas.

Por qué y cuándo desnormalizar

La desnormalización resulta realmente útil cuando los usuarios reales y las consultas reales se ven bloqueados por uniones, agregaciones o búsquedas repetidas. Una vez que hayas confirmado que la indexación, el ajuste de consultas y el almacenamiento en caché no son suficientes, es posible que desees recurrir a la desnormalización para optimizar la velocidad de lectura en patrones de acceso predecibles.

Cuándo puede ser útil la desnormalización

  • Cargas de trabajo con gran volumen de lectura: latencias p95/p99 dominadas por uniones o agregaciones, CPU dedicada a uniones hash/merge, alta rotación de la caché del búfer.
  • Formas de consulta estables: Los mismos paneles/puntos finales funcionan todo el día con filtros similares (por ejemplo, las ventas de ayer por categoría).
  • Uniones en abanico: Una tabla activa se une a otras 3-5 solo para renderizar una vista de tarjeta o lista.
  • Puntos de agregación: Calculas repetidamente totales, recuentos o valores más recientes en rangos amplios.
  • Presión SLA: El producto necesita respuestas inferiores a 200 ms, mientras que los planes actuales se desbordan al disco o escanean demasiado.

Casos de uso clásicos

  • Paneles de control e informes de BI (OLAP/análisis): Precalcula los agregados diarios/mensuales, mantén vistas materializadas de agrupaciones costosas o almacena tablas de hechos desnormalizadas para segmentos comunes.
  • Comercio electrónico/catálogo y páginas de búsqueda/listado: Duplicar nombre_categoría, nombre_marca, precio_con_impuestoso una proyección de productos preunida para listas y filtros rápidos.
  • CMS/blog/fuentes de noticias: Tienda author_name, tema_principalo renderized_excerpt en la tabla de artículos/publicaciones para evitar uniones o transformaciones en tiempo de ejecución.
  • Feed de actividad y contadores: Mantener like_count, follower_count, o latest_comment_at como atributos derivados en lugar de recalcularlos.
  • Análisis de eventos/registros a gran escala (OLAP/nosql): Aplanar los datos anidados para los almacenes columnares y mantener filas anchas compatibles con las particiones para que los escaneos sean predecibles.

Cuándo no desnormalizar y utilizar primero otra cosa

  • OLTP con gran volumen de escritura y consistencia estricta (pedidos, pagos, ajustes de inventario).
  • El cuello de botella es la falta o la deficiencia de índices, las consultas N+1 o el ORM comunicativo. Arregla eso primero.
  • Campos de alta volatilidad (por ejemplo, disponibilidad de productos que cambia cada segundo) donde la duplicación amplifica la rotación.
  • Equipos sin un plan claro de propiedad y sincronización (desencadenantes, CDC/tareas, políticas de actualización, supervisión de desviaciones). Sin esto, corres el riesgo de hacer más daño que bien.
  • El conjunto de datos es lo suficientemente pequeño como para que un índice de cobertura o una caché ya lo hagan rápido.

Realmente conviene utilizar la desnormalización cuando te proporciona la mayor velocidad de lectura con la menor carga operativa adicional, y solo después de haber descartado soluciones más económicas.

Si te has dado cuenta de que la desnormalización no es lo que necesitas en este momento, ¡no te preocupes! En la siguiente sección exploraremos las alternativas a la desnormalización.

Alternativas a la desnormalización

La desnormalización no es la solución a todos tus problemas. Antes de añadir redundancia, debes sacar todo el partido posible al motor y a tu aplicación. Estas soluciones son más baratas de mantener y, a menudo, ofrecen los mismos beneficios.

1) Indexación

  • Índices compuestos/de cobertura: En tus consultas SQL, coloca primero las columnas de filtro y, a continuación, las columnas GROUP BY /ORDER BY . Incluyo columnas de lista de selección para que el motor pueda atender la consulta solo desde el índice.

  • Índices filtrados/parciales: Indexa solo la parte más importante (por ejemplo, status = 'ACTIVE'), manteniendo el índice pequeño y rápido.

  • Índices de expresión/funcionales: Indexa en LOWER(email) o date_trunc('day', created_at) para evitar escaneos computados.

2) Ajuste de consultas y paginación

  • Evita SELECT *. Recupera solo lo que muestras.

  • Reemplaza las uniones innecesarias con EXISTS/SEMI cuando solo necesites comprobar la presencia.

  • Empuja los predicados hacia abajo: filtra pronto, agrega tarde.

  • Usa la paginación por teclado (WHERE created_at < ? ORDER BY created_at DESC LIMIT 50) para un desplazamiento estable y rápido.

3) Almacenamiento en caché

  • Usa la caché de la aplicación (por ejemplo, Redis) para consultas frecuentes y fragmentos renderizados.
  • Usa El almacenamiento en caché HTTP (ETag/Last-Modified) para páginas públicas y paneles de control.
  • Las cachés de corta duración (30-120 s) suelen eliminar la necesidad de realizar cambios en el esquema.

4) Leer réplicas

  • Descarga las lecturas pesadas a las réplicas. Ideal para paneles de control y exportaciones.

5) Partición y poda

  • Partición de rango/hash de tablas grandes para que tus exploraciones solo afecten a las particiones relevantes (por ejemplo, los últimos 30 días). Por cierto, esto no es desnormalización, simplemente se trata de reducir la cantidad de datos escaneados.

6) Almacenes columnares/OLAP

  • Envía análisis pesados a Snowflake/BigQuery/ClickHouse (a través de ELT/dbt). Mantén OLTP normalizado y deja que el almacén gestione formas amplias y fáciles de escanear.

7) Higiene ORM

  • Matar las consultas N+1 (carga anticipada o por lotes), establece listas de selección sensatas las listas de seleccióny limita tamaños de página. Una capa ORM limpia puede eliminar la necesidad de desnormalizar.

8) Columnas calculadas/generadas (mantener en la base de datos)

  • Deja que la base de datos mantenga los valores derivados (por ejemplo, price_with_tax) como columnas generadas/calculadas o mediante índices de expresión. Esto te permitirá obtener lecturas rápidas sin necesidad de sincronización a nivel de aplicación.

Técnicas de desnormalización

Estas son las formas más comunes de añadir redundancia controlada a un modelo normalizado. Para cada uno, mostraré qué hace, cuándo utilizarlo y cómo mantenerlo sincronizado.

Configuración: supongamos un núcleo normalizado con clientes, pedidos, artículos_de_pedido y productos.

1) Tablas «proyectadas» aplanadas/preunidas

Qué: Crea una tabla que pre-une las columnas que necesitas para lecturas rápidas (por ejemplo, listado de productos o un panel de control de pedidos).

Cuándo: Tu ruta activa une de 3 a 5 tablas de forma predecible.

Cómo (PostgreSQL):

-- Read-optimised projection for a typical orders list
CREATE TABLE orders_projection (
  order_id        BIGINT PRIMARY KEY,
  created_day     DATE NOT NULL,
  customer_id     BIGINT NOT NULL,
  customer_name   TEXT  NOT NULL,
  total_amount    NUMERIC(12,2) NOT NULL
);

-- Initial backfill
INSERT INTO orders_projection (order_id, created_day, customer_id, customer_name, total_amount)
SELECT o.order_id,
       o.created_at::date AS created_day,
       c.customer_id,
       c.name AS customer_name,
       SUM(oi.quantity * oi.unit_price) AS total_amount
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
JOIN order_items oi ON oi.order_id = o.order_id
GROUP BY o.order_id, created_day, c.customer_id, c.name;

-- Index for fast filtering by day/customer
CREATE INDEX ON orders_projection (created_day, customer_id);

Opciones de sincronización:

  • Trabajo posterior a la escritura (poner en cola «order.updated» → volver a calcular la fila)
  • Trabajo por lotes nocturno/15 minutos (dbt/cron)

2) Columnas redundantes (copiar algunos atributos)

Qué: Duplica algunos atributos que se consultan con frecuencia en otra tabla para evitar uniones (por ejemplo, orders.customer_name).

Cuándo: Solo necesitas 1 o 2 valores y no deseas una proyección completa.

Cómo (ejemplo de disparador PostgreSQL):

ALTER TABLE orders
  ADD COLUMN customer_name TEXT,
  ADD COLUMN customer_tier TEXT;

-- Keep the redundant fields correct on insert/update
CREATE OR REPLACE FUNCTION sync_order_customer_fields()
RETURNS TRIGGER AS $
BEGIN
  SELECT name, tier INTO NEW.customer_name, NEW.customer_tier
  FROM customers WHERE customer_id = NEW.customer_id;
  RETURN NEW;
END;
$ LANGUAGE plpgsql;

CREATE TRIGGER trg_orders_sync_customer
BEFORE INSERT OR UPDATE OF customer_id ON orders
FOR EACH ROW EXECUTE FUNCTION sync_order_customer_fields();

-- Propagate customer name/tier changes to existing orders
CREATE OR REPLACE FUNCTION propagate_customer_changes()
RETURNS TRIGGER AS $
BEGIN
  UPDATE orders
  SET customer_name = NEW.name,
      customer_tier = NEW.tier
  WHERE customer_id = NEW.customer_id;
  RETURN NEW;
END;
$ LANGUAGE plpgsql;

CREATE TRIGGER trg_customer_propagate
AFTER UPDATE OF name, tier ON customers
FOR EACH ROW EXECUTE FUNCTION propagate_customer_changes();

Compensación: Las lecturas son sencillas, pero las escrituras se diversifican cuando un cliente realiza una actualización.

3) Atributos derivados (almacenar valores calculados)

Qué: Persiste los valores que de otro modo calcularías sobre la marcha (por ejemplo, price_with_tax , latest_comment_at, item_count).

Cuándo: El cálculo se realiza constantemente y es determinista.

Dos patrones comunes:

  • Columna generada (la base de datos vuelve a calcular al escribir: tienes menos control, no hay referencias entre tablas)
  • Columna almacenada + activador/tarea (tú controlas cuándo y cómo cambia)
-- Generated column example (Postgres 12+; same-table expressions)
ALTER TABLE products
  ADD COLUMN price_with_tax NUMERIC(12,2) GENERATED ALWAYS AS (price * 1.20) STORED;

-- Counter maintained by triggers (likes per post)
ALTER TABLE posts ADD COLUMN like_count INTEGER NOT NULL DEFAULT 0;

CREATE OR REPLACE FUNCTION inc_like_count() RETURNS TRIGGER AS $
BEGIN
  UPDATE posts SET like_count = like_count + 1 WHERE post_id = NEW.post_id;
  RETURN NEW;
END; $ LANGUAGE plpgsql;

CREATE TRIGGER trg_like_insert
AFTER INSERT ON post_likes
FOR EACH ROW EXECUTE FUNCTION inc_like_count();

CREATE OR REPLACE FUNCTION dec_like_count() RETURNS TRIGGER AS $
BEGIN
  UPDATE posts SET like_count = GREATEST(like_count - 1, 0) WHERE post_id = OLD.post_id;
  RETURN OLD;
END; $ LANGUAGE plpgsql;

CREATE TRIGGER trg_like_delete
AFTER DELETE ON post_likes
FOR EACH ROW EXECUTE FUNCTION dec_like_count();

4) Tablas agregadas/resumen

Qué: Precalcula resúmenes como los ingresos diarios, los usuarios activos por día y los pedidos por categoría.

Cuándo: Los paneles repiten los mismos agrupamientos y los escaneos sin procesar son costosos.

CREATE TABLE daily_sales (
  sales_day    DATE PRIMARY KEY,
  gross_amount NUMERIC(14,2) NOT NULL,
  order_count  INTEGER NOT NULL
);

-- Incremental upsert for "yesterday" (run hourly/15-min)
INSERT INTO daily_sales (sales_day, gross_amount, order_count)
SELECT (o.created_at AT TIME ZONE 'UTC')::date AS sales_day,
       SUM(oi.quantity * oi.unit_price)       AS gross_amount,
       COUNT(DISTINCT o.order_id)             AS order_count
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
WHERE o.created_at >= date_trunc('day', now() - interval '1 day')
  AND o.created_at <  date_trunc('day', now())
GROUP BY sales_day
ON CONFLICT (sales_day) DO UPDATE
  SET gross_amount = EXCLUDED.gross_amount,
      order_count  = EXCLUDED.order_count;

Opciones de sincronización: trabajo programado (cron/dbt), actualizaciones en streaming (CDC) o agregadores basados en eventos.

5) Vistas materializadas (resultados de consultas persistentes)

Qué: Almacena el resultado de una consulta costosa como una tabla física que puedas indexar.

Cuándo: El conjunto de resultados es costoso, relativamente estable y puedes tolerar ventanas de actualización.

-- Expensive report
CREATE MATERIALIZED VIEW mv_category_sales AS
SELECT p.category_id,
       date_trunc('day', o.created_at) AS day,
       SUM(oi.quantity * oi.unit_price) AS revenue
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
JOIN products p ON p.product_id = oi.product_id
GROUP BY p.category_id, date_trunc('day', o.created_at);

-- Make it fast to query
CREATE INDEX ON mv_category_sales (category_id, day);

-- Refresh patterns
-- Full, blocking:
REFRESH MATERIALIZED VIEW mv_category_sales;

-- Non-blocking reads (requires unique index on the MV):
-- 1) ensure a unique index exists (e.g., (category_id, day))
-- 2) then:
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_category_sales;

Compensación: Tú gestionarás la cadencia de actualización, ya que la actualización incremental no es nativa en Postgres. Dicho esto, resulta bastante fácil cuando usas dbt o patrones personalizados de «solo añadir + reconstrucción por ventanas».

6) Proyecciones de «documentos» JSON precalculadas.

Qué: Almacena un blob JSON desnormalizado que se ajuste a las necesidades de tu API/UI (por ejemplo, ficha de producto), junto con índices JSON específicos.

Cuándo: Tu API sirve la misma forma repetidamente y tú quieres una lectura.

CREATE TABLE product_cards (
  product_id BIGINT PRIMARY KEY,
  card_json  JSONB NOT NULL
);

-- Build or rebuild card documents
INSERT INTO product_cards (product_id, card_json)
SELECT p.product_id,
       jsonb_build_object(
         'id', p.product_id,
         'name', p.name,
         'brand', b.name,
         'price_with_tax', p.price * 1.20,
         'category', c.name
       )
FROM products p
JOIN brands b    ON b.brand_id = p.brand_id
JOIN categories c ON c.category_id = p.category_id
ON CONFLICT (product_id) DO UPDATE SET card_json = EXCLUDED.card_json;

-- GIN index for JSON queries if needed
CREATE INDEX idx_product_cards_gin ON product_cards USING GIN (card_json);

Opciones de sincronización: reconstrucción basada en eventos cuando se producen cambios en el producto, la marca o la categoría, o actualización frecuente por lotes de los productos actualizados recientemente.

Elegir una estrategia de sincronización (guía rápida)

  • Desencadenantes: Son inmediatos y garantizan la coherencia de las transacciones. Por lo general, son ideales para un pequeño fan-out y un volumen de escritura bajo, pero pueden afectar a la latencia de escritura si se utilizan en exceso.
  • Aplicación de doble escritura: La aplicación escribe tanto el código fuente como la desnormalización. Es sencillo, pero más arriesgado. Puedes mitigar eso con reintentos idempotentes y patrones de bandeja de salida/CDC.
  • CDC → trabajador: Esa estrategia syn es fiable y escalable, ya que propaga los cambios de forma asíncrona. Es ideal cuando la consistencia eventual es aceptable.
  • Tareas programadas: La opción más sencilla para agregados y vistas materializadas. Solo tienes que elegir las ventanas de actualización que se ajusten a tu tolerancia de experiencia de usuario.

Te recomiendo que siempre incorpores la observabilidad en tu solución. No hay nada peor que no estar seguro de si los cambios se han procesado o replicado correctamente. También puede ayudarte a contabilizar tus comprobaciones de deriva y scripts de relleno.

Cómo implementar la desnormalización paso a paso

Como siempre, manipular tu base de datos es bastante arriesgado. Te recomiendo encarecidamente que repliques tu base de datos e intentes implementar primero la desnormalización en la réplica, solo para asegurarte de que todo funciona como esperas. Si tienes un entorno de desarrollo, ¡mucho mejor!

El proceso es bastante sencillo: medir → cambiar lo menos posible → mantenerlo correcto → verificar → supervisar → repetir.

1) Define tu perfil y establece un objetivo de éxito.

  • Captura la consulta exacta (texto + parámetros) y su frecuencia.
  • Registra una línea de base: EXPLAIN (ANALYZE, BUFFERS) plan, latencia p95, CPU/E/S, filas escaneadas.
  • Acuerda un criterio de éxito (por ejemplo, «p95 < 120 ms con un coste de escritura ≤1,2×»).
  • Confirma la tolerancia de consistencia (por ejemplo, «los análisis pueden tener un retraso de 5 minutos»).

2) Elige la desnormalización más pequeña que funcione.

  • Lo ideal es probar con una columna redundante o una tabla resumen en una proyección grande.
  • Dibuja la nueva forma:
    • ¿Columnas redundantes? (cuáles y por qué)
    • ¿Tabla agregada o vista materializada? (grano, claves, ventana de actualización)
  • Decide el modelo de consistencia:
    • Fuerte (desencadenante/transaccional) frente a eventual (CDC/trabajo/actualización de vista materializada).
  • Anota el presupuesto de amplificación de escritura (cuántas escrituras adicionales por evento son aceptables).

3) Diseña la ruta de sincronización.

  • Desencadenantes (fuerte consistencia, pequeño fan-out).
  • CDC/bandeja de salida → trabajador (buena escalabilidad, consistencia eventual):
    • La aplicación escribe en las tablas de origen y en una fila de la bandeja de salida en la misma transacción.
    • Un trabajador lee la bandeja de salida y actualiza el objetivo desnormalizado de forma idempotente.
  • Tareas programadas / Actualización de MV (ideal para agregados):
    • Define la cadencia de actualización, las ventanas y la estrategia de relleno.

La idempotencia no es negociable. Las actualizaciones deben poder repetirse de forma segura (por ejemplo, UPSERT con recálculo determinista). Confía en mí, te lo agradecerás más adelante.

4) Crear, rellenar y validar

  1. Crea estructuras (tablas, columnas, índices, desencadenadores/tareas), pero mantén la aplicación sin cambios.
  2. Relleno desde la fuente de verdad.
  3. Validar la paridad con invariantes:
-- Example parity check: orders_projection vs live join
SELECT COUNT(*) AS mismatches
FROM orders_projection p
JOIN orders o   ON o.order_id = p.order_id
JOIN customers c ON c.customer_id = o.customer_id
WHERE p.customer_name <> c.name
   OR p.total_amount <> (
        SELECT SUM(oi.quantity * oi.unit_price)
        FROM order_items oi WHERE oi.order_id = o.order_id
      );

4. Corregir las discrepancias; volver a ejecutar hasta llegar a cero (o dentro de un margen de error acordado).

5) Corta con cuidado.

  • Envía el cambio de la aplicación detrás de un indicador de función o canario (por ejemplo, el 10 % del tráfico lee desde la ruta desnormalizada).
  • Ejecuta lecturas en segundo plano: calcula el resultado anterior en segundo plano y compara los hash/agregados para un subconjunto de solicitudes.
  • Mantén una reversión rápida (vuelve instantáneamente a la ruta normalizada).

6) Supervisa lo que realmente importa.

Crea un pequeño panel de control en la herramienta de observabilidad que prefieras:

  • Lee: latencia p50/p95, filas leídas, aciertos de búfer, estabilidad del plan de consulta.
  • Escribir: tiempo de escritura adicional, errores de activación/trabajo, retraso en la cola (CDC), marca de tiempo de la última actualización de MV.
  • Calidad de los datos: contadores de desviación (comprobaciones de paridad diarias), número de filas rellenadas, porcentaje de discrepancias.
  • Coste/huellatamaños de las tablas, aumento del índice, crecimiento del tamaño de MV.

7) Operar e iterar

  • Asignar la responsabilidad (equipo/sistema de guardia) y un manual de procedimientos (cómo reconstruir, rellenar, reparar). Esto suele pasarse por alto, pero es importante poder responder lo más rápido posible cuando se produce un incidente. 
  • Reevaluar trimestralmente: ¿sigues necesitando esto? ¿Sigue teniendo la forma adecuada? ¿Podemos simplificarlo? Documenta la decisión y enlázala a los paneles de control.

Ventajas y desventajas de la desnormalización

Hemos tratado los detalles en tabla Normalización frente a desnormalización , así que aquí tienes un breve resumen.

Lo que ganas

  • Lecturas más rápidas y latencias p95/p99 más predecibles.
  • Consultas más sencillas en rutas activas (menos uniones/agregaciones)
  • Menor consumo de CPU/E/S por lectura y consultas analíticas más económicas.

Cuánto cuesta

  • Amplificación de escritura (actualizaciones/inserciones adicionales)
  • Riesgo de coherencia (datos obsoletos/inconsistentes a menos que se sincronicen correctamente)
  • Aumento del almacenamiento (columnas/tablas/vistas duplicadas)
  • Gastos generales operativos (actualizaciones, rellenos, supervisión)
  • Cambiar fan-out (las actualizaciones del esquema/lógica deben reflejarse)

Herramientas y tecnologías que admiten la desnormalización

No todas las bases de datos te ayudan a desnormalizar de la misma manera. Algunos te ofrecen funciones de primera clase para conservar los resultados de consultas costosas, mientras que otros esperan que tú mismo te encargues de ello mediante trabajos, desencadenadores o canalizaciones.

PostgreSQL

Postgres te ofrece varias formas de materializar formas optimizadas para la lectura. 

Las vistas materializadas conservan el resultado de una consulta, por lo que las lecturas son instantáneas. Tú eliges cuándo actualizar (de forma programada o puntual) e incluso puedes actualizar simultáneamente para que los lectores no se vean bloqueados.

Para una duplicación ligera, las columnas generadas y los índices de expresión permiten a la base de datos mantener valores derivados y acelerar los filtros comunes sin necesidad de escribir lógica de sincronización adicional. 

Cuando necesites una fuerte consistencia entre la fuente y una copia desnormalizada, puedes utilizar disparadores para mantener la sincronización (pero recuerda que añaden trabajo a tu ruta de escritura). Para actualizaciones por eventos o por lotes, puedes combinar Postgres con un programador (por ejemplo, pg_cron o un ejecutor de tareas externo) o con replicación lógica/CDC para transmitir los cambios a un trabajador que actualice tus tablas desnormalizadas de forma asíncrona.

Si deseas poner en práctica tus conocimientos y crear tus propias bases de datos y probar la desnormalización, echaun vistazo a nuestro curso de PostgreSQL.

SQL Server

SQL Server se inclina fuertemente por las vistas indexadas, que son una especie de vistas materializadas siempre activas, en las que el motor mantiene la vista sincronizada en cada inserción/actualización/eliminación, por lo que las lecturas son muy rápidas. La otra cara de la moneda es que ahora las escrituras mantienen tanto las tablas base como la vista, por lo que las cargas de trabajo OLTP pesadas pueden ralentizarse. 

Aparte de eso, puedes utilizar columnas calculadas persistentes para cubrir atributos derivados de una sola fila. 

Para la generación de informes a gran escala, los índices de almacenamiento en columnas son una alternativa a la desnormalización: comprimen y escanean grandes conjuntos de datos de manera muy eficiente, lo que puede eliminar la necesidad de duplicar los datos en primer lugar.

Oracle

Las vistas materializadas de Oracle son una opción madura con una actualización « FAST » (solo lo que ha cambiado) que utiliza registros de cambios para actualizar solo lo que ha cambiado, o una actualización « COMPLETE » (todo) cuando no te importa reconstruir todo. 

Con Query Rewrite, el optimizador puede utilizar de forma transparente tu vista materializada cuando alguien consulta las tablas subyacentes, por lo que los equipos obtienen una mayor velocidad sin necesidad de cambiar el SQL de la aplicación. Como siempre, una lectura más rápida implica más trabajo en algún otro aspecto: mantener registros y actualizar vistas aumenta la carga operativa y de escritura.

MySQL / MariaDB

MySQL no tiene vistas materializadas nativas, por lo que la desnormalización suele construirse con tablas físicas y tareas programadas o desencadenadores para mantenerlas actualizadas. 

Las columnas generadas ayudan con valores derivados simples. Esto funciona bien para paneles y resúmenes predecibles, pero, una vez más, vale la pena ser consciente de la cadencia de actualización y la complejidad de los desencadenantes para no sobrecargar accidentalmente las rutas de escritura críticas. Muchos equipos combinan esto con réplicas de lectura para descargar por completo las consultas de informes.

Snowflake / BigQuery (capa de análisis)

Los almacenes columnares están diseñados para datos amplios, fáciles de escanear y desnormalizados. Normalmente, se modelan tablas de hechos amplias con atributos útiles incorporados y, a continuación, se recurre a la partición/agrupación para podar lo que se escanea. 

Ambas plataformas admiten vistas materializadas y tareas/consultas programadas para mantener los agregados actualizados sin tocar el sistema OLTP. A cambio de los costes de almacenamiento y actualización, obtienes lecturas muy predecibles y económicas a gran escala. Es perfecto para paneles de control y BI.

Este curso es un poco más avanzado que nuestro curso de PostgreSQL, por lo que, si ya te sientes cómodo con las bases de datos, no dudes en probar nuestra introducción al modelado de datos con Snowflake.

dbt (modelado y orquestación)

dbt se conoce como la capa de «infraestructura como código» para el análisis desnormalizado. Definís los modelos una vez, elegís una materialización (tabla, vista, incremental) y dejás que dbt se encargue de las compilaciones, las dependencias y las pruebas. Los modelos incrementales son especialmente útiles para tablas resumen en las que solo es necesario añadir y fusionar datos nuevos. ¡Es sin duda una de mis herramientas favoritas!

CDC y canales de datos

Cuando deseas que los cambios de la aplicación fluyan de forma fiable hacia estructuras desnormalizadas, Change Data Capture es la solución ideal. Herramientas como Debezium (autohospedadas) o conectores gestionados (Fivetran, Airbyte) transmiten los cambios a nivel de fila desde OLTP a trabajadores o almacenes que actualizan tablas de proyección, contadores o agregados. Esto suele ser coherente a largo plazo (ideal para paneles y feeds) y se adapta mucho mejor que meter todo en los activadores. Haz que las actualizaciones sean idempotentes, supervisa el retraso y mantén una ruta de relleno para los eventos tardíos o perdidos.

Conclusiones clave y reflexiones finales

La desnormalización es una optimización pragmática que se superpone a un diseño normalizado, no un rechazo del mismo. Estás sacrificando el almacenamiento y la complejidad de escritura a cambio de lecturas más rápidas y sencillas en rutas específicas y bien conocidas.

Recuerda:

  • Comienza normalizado. Utiliza la desnormalización solo cuando una consulta medida y con nombre lo requiera.
  • Agotar primero las soluciones más económicas (índices, reescritura de consultas, almacenamiento en caché, réplicas, OLAP).
  • Elige la desnormalización más pequeña que resulte rentable (una columna redundante, un agregado pequeño o una vista materializada).
  • Sé explícito sobre la frescura (consistencia fuerte frente a consistencia eventual) y crea una sincronización idempotente.
  • Mide antes y después, supervisa las desviaciones y ten a mano un interruptor de reversión.
  • ¡Considera las tablas normalizadas como la fuente de verdad! Las piezas desnormalizadas son copias optimizadas para lectura que puedes reconstruir.

Marie Fayard's photo
Author
Marie Fayard

Soy un líder tecnológico con mentalidad de producto, especializado en el crecimiento de startups en fase inicial, desde el primer prototipo hasta la adaptación del producto al mercado y más allá. Siento una curiosidad infinita por saber cómo utiliza la gente la tecnología, y me encanta trabajar estrechamente con fundadores y equipos multifuncionales para dar vida a ideas audaces. Cuando no estoy creando productos, busco inspiración en nuevos rincones del mundo o me desahogo en el estudio de yoga.

Temas

Aprende con DataCamp

Curso

Introducción a las bases de datos relacionales en SQL

4 h
178.6K
Aprende a crear bases de datos relacionales, una de las formas más eficaces de almacenar datos.
Ver detallesRight Arrow
Comienza el curso
Ver másRight Arrow
Relacionado

blog

Contratos de datos desmitificados: Todo lo que necesitas saber

Lograr la escalabilidad en los sistemas de datos distribuidos y reducir los errores.
Mike Shakhomirov's photo

Mike Shakhomirov

11 min

blog

11 técnicas de visualización de datos para cada caso de uso con ejemplos

Descubra los análisis, técnicas y herramientas más populares para dominar el arte de la visualización de datos.
Javier Canales Luna's photo

Javier Canales Luna

12 min

blog

Las 5 mejores bases de datos vectoriales

Una guía completa de las mejores bases de datos vectoriales. Domina el almacenamiento de datos de alta dimensión, descifra la información no estructurada y aprovecha las incrustaciones de vectores para aplicaciones de IA.
Moez Ali's photo

Moez Ali

14 min

Tutorial

Cómo utilizar un alias SQL para simplificar tus consultas

Explora cómo el uso de un alias SQL simplifica tanto los nombres de las columnas como los de las tablas. Aprende por qué utilizar un alias SQL es clave para mejorar la legibilidad y gestionar uniones complejas.
Allan Ouko's photo

Allan Ouko

Tutorial

Introducción a los disparadores SQL: Guía para desarrolladores

Aprende a utilizar los disparadores SQL para automatizar tareas, mantener la integridad de los datos y mejorar el rendimiento de la base de datos. Prueba ejemplos prácticos como los comandos CREATE, ALTER y DROP en MySQL y Oracle.
Oluseye Jeremiah's photo

Oluseye Jeremiah

Tutorial

Seleccionar varias columnas en SQL

Aprende a seleccionar fácilmente varias columnas de una tabla de base de datos en SQL, o a seleccionar todas las columnas de una tabla en una simple consulta.
DataCamp Team's photo

DataCamp Team

Ver másVer más