StackPractices
intermediate Por Mathias Paulenko

Data Lake vs Data Warehouse — Guía de Arquitectura

Cómo los data lakes almacenan datos crudos a escala: zonas medallion, lakehouse, ETL vs ELT, particionamiento y cuándo elegir un lake sobre un warehouse.

Visión General

Un Data Lake es un repositorio de almacenamiento centralizado que contiene datos estructurados, semi-estructurados y no estructurados a cualquier escala. A diferencia de un Data Warehouse, que almacena datos procesados con schema-on-write en tablas rígidas, un Data Lake guarda datos crudos en su formato nativo sobre object storage barato (S3, GCS, ADLS) y aplica el schema al leerlos.

Lo que típicamente aterriza en un lake: eventos de clickstream y de producto, logs de aplicaciones y de acceso, streams de CDC exportados desde bases OLTP, telemetría de IoT y archivos crudos de partners o vendors — todo lo que es demasiado grande, demasiado desordenado o demasiado temprano en su ciclo de vida para un schema curado.

Esa flexibilidad trae trampa incluida: el mismo lake que permite a un data scientist explorar clickstream crudo también permite a cada equipo volcar lo que tenga sin catálogo, responsable ni política de retención. Sin gobernanza, los lakes se degradan en “data swamps” — colecciones de archivos que nadie puede encontrar, en las que nadie confía y que nadie se atreve a borrar.

Esta guía se queda en el lado práctico de todo eso: decidir cuándo un lake es la elección correcta, cómo funcionan las capas Bronze/Silver/Gold, qué formato de almacenamiento y estrategia de particionamiento elegir, y cómo encajan las piezas en un pipeline de eventos real. Para la evolución que añade tablas ACID sobre el storage del lake, consulta la guía de Arquitectura Lakehouse.

Cuándo Usar un Data Lake

  • Necesitas almacenar tipos de datos diversos: JSON, CSV, Parquet, imágenes, videos, logs
  • Las cargas de machine learning requieren datos crudos sin procesar para feature engineering
  • El volumen de datos excede lo que un warehouse puede retener económicamente: el object storage es un orden de magnitud más barato por terabyte
  • Quieres diferir el diseño del schema hasta que los datos se consuman
  • Los datos históricos deben retenerse baratos para análisis futuro o cumplimiento normativo

Cuándo NO usarlo: si tus datos ya están estructurados y tus consumidores son analistas de negocio ejecutando dashboards en SQL, un warehouse es el mejor punto de partida: tablas curadas, consultas OLAP rápidas, gobernanza incorporada. Un lake delante de esa carga añade una capa de transformación que no necesitas. Y nunca uses un lake para OLTP: no tiene transacciones, ni actualizaciones a nivel de fila (salvo que añadas una capa lakehouse), ni garantías de latencia.

Data Lake vs Data Warehouse

DimensiónData LakeData Warehouse
Tipos de datosTodos (estructurados, semi, no estructurados)Solo estructurados
SchemaSchema-on-readSchema-on-write
UsuariosCientíficos de datos, ingenieros ML, analistasAnalistas de negocio, herramientas BI
Rendimiento de consultaVariable, optimizado para batchRápido, optimizado para OLAP
CostoAlmacenamiento bajo, cómputo altoAlmacenamiento alto, cómputo optimizado
Calidad de datosCrudos, pueden no estar validadosCurados, validados, confiables
EscalaPetabyte+Terabyte a petabyte

La mayoría de las organizaciones terminan con ambos: el lake guarda los datos crudos y de ML, el warehouse sirve las cargas de BI curadas. El factor que suele decidirlo es quién consume los datos: equipos de analistas que solo saben SQL suelen estar mejor servidos por un warehouse, mientras que los ingenieros que necesitan acceso crudo son donde un lake da fruto.

El panorama de costos favorece al lake para retención: S3 cuesta unos $23/TB-mes en almacenamiento estándar (menos con tiers de ciclo de vida a Infrequent Access o Glacier), mientras el storage del warehouse suele costar varias veces eso; el cómputo del warehouse se factura por rendimiento, no por estar ocioso. El intercambio se invierte en cargas intensivas de consulta: las consultas ad-hoc al lake escanean más datos, así que una carga de BI ocupada sobre Athena o Trino puede costar más al mes que un warehouse bien dimensionado. Regla práctica: barato de guardar, caro de consultar; pon las superficies analíticas calientes en Gold o en el warehouse.

El Lakehouse

La elección entre lake y warehouse era antes una decisión dura de uno u otro; lleva un tiempo difuminándose. Los formatos de tabla como Delta Lake, Apache Iceberg y Apache Hudi añaden transacciones ACID, enforcement de schema y time travel directamente sobre el storage del lake, convirtiendo archivos de objetos en tablas reales que los motores de consulta (Trino, Spark, Athena) pueden actualizar y fusionar.

De ahí sale el “lakehouse”: economía de storage de lake con fiabilidad casi de warehouse. No elimina el warehouse (las capas de servicio curadas siguen existiendo) pero corrige los dos peores problemas del lake: sin transacciones y sin garantías de schema. La guía de Arquitectura Lakehouse cubre el patrón en profundidad.

Entre los tres formatos: Delta Lake tiene la integración con Spark más suave y es el default en Databricks; Iceberg tiene el soporte de motores más amplio (Trino, Flink y Snowflake lo leen nativamente) y mejor evolución de particiones; Hudi apunta a cargas de CDC intensivas en upserts. Salvo que tengas una restricción específica, elige el que tu motor de consulta lea mejor.

Capas de Arquitectura

La arquitectura medallion organiza un lake en zonas, cada una con un nivel de procesamiento distinto:

Zona Raw (Bronze)
    ├── Datos sin procesar de las fuentes
    ├── Retenidos en formato nativo
    └── Almacenamiento barato, largo plazo

Zona Limpia (Silver)
    ├── Datos deduplicados y validados
    ├── Transformaciones básicas aplicadas
    └── Schemas tipados forzados

Zona Curada (Gold)
    ├── Agregados listos para negocio
    ├── Optimizados para rendimiento de consulta
    └── Usados por herramientas BI y aplicaciones
flowchart diagram: Sources[

Las reglas que hacen que esto funcione:

  • Bronze es solo-append. Nunca modifiques ni borres datos crudos: son tu fuente de replay cuando se descubre un bug downstream. Reténlos indefinidamente (o según la política de cumplimiento).
  • Silver es tipada y deduplicada. Aquí es donde fuerzas schemas, descartas filas malformadas y deduplicas por claves naturales. Casi todas las consultas analíticas deberían pegarle a Silver, no a Bronze.
  • Gold está construida con propósito. Cada tabla Gold existe para un consumidor: un dashboard, un feature set de ML, un reporte regulatorio. Una tabla Gold que nadie consulta es trabajo tirado.

La promoción entre zonas es un job programado, no un flujo automático: un proceso de ingesta (Firehose, un conector CDC, una app productora) escribe en Bronze continuamente, luego jobs batch o streaming ejecutan Bronze→Silver en un horario (por hora o diario para batch, micro-batches para streaming) y Silver→Gold según la cadencia de reportes. Cada promoción es una puerta de calidad; los datos solo suben tras pasar la validación, por eso “datos malos en Gold” casi siempre significa que una regla de validación se saltó, no que la arquitectura falló.

Consultar el Lake

El lake en sí son solo archivos; el motor de consulta es lo que lo hace usable:

MotorModeloMejor Para
AthenaServerless, pago por escaneoShops de AWS, consultas ad-hoc y programadas
TrinoSQL distribuido federadoUnir datos del lake con warehouses, APIs, otros lakes
Spark SQLCómputo batchTransformaciones pesadas que alimentan zonas
DremioSQL + reflectionsDashboards acelerados directo sobre el lake
DuckDBEmbebido, localPrototipado, analítica de un solo nodo sobre Parquet

Athena o Trino apuntando a Silver cubre la exploración ocasional sin problema. Si un dashboard golpea el lake cada pocos segundos, promueve esos datos a Gold, o al warehouse, en lugar de pagar por escaneo para siempre.

ETL vs ELT

PatrónFlujoMejor Para
ETLExtract → Transform → LoadData warehouses, requerimientos estrictos de schema
ELTExtract → Load → TransformData lakes, flexibilidad de schema-on-read

Los lakes tiran hacia ELT: aterrizas los datos crudos primero y los transformas en zonas cuando de verdad hacen falta. El paso de transformación es donde se concentra la mayor parte del costo de cómputo; la guía de ETL Pipeline profundiza en la orquestación.

# Patrón ELT — carga cruda, transforma bajo demanda
from pyspark.sql import SparkSession

spark = SparkSession.builder.appName("DataLakeELT").getOrCreate()

# Extract: Leer logs JSON crudos desde S3
raw_df = spark.read.json("s3://datalake/bronze/events/2026/10/")

# Load: Almacenar como Parquet en la zona Silver
raw_df.write.parquet("s3://datalake/silver/events/", mode="overwrite")

# Transform: Aplicar schema y agregaciones en lectura
cleaned_df = spark.read.parquet("s3://datalake/silver/events/")
cleaned_df.createOrReplaceTempView("events")

daily_metrics = spark.sql("""
    SELECT
        DATE(timestamp) AS date,
        event_type,
        COUNT(*) AS event_count,
        COUNT(DISTINCT user_id) AS unique_users
    FROM events
    WHERE timestamp >= '2026-10-01'
    GROUP BY DATE(timestamp), event_type
""")

daily_metrics.write.parquet("s3://datalake/gold/daily_metrics/")

Formatos de Almacenamiento

FormatoTipoCaso de Uso
CSVTextoIntercambio, legible por humanos
JSONSemi-estructuradoAPIs, datos anidados
ParquetColumnarConsultas analíticas, compresión
AvroBasado en filasStreaming, evolución de schema
ORCColumnarCargas de trabajo Hive/Spark
Delta LakeCapa de tablaTransacciones ACID sobre lakes

Los defaults que cubren la mayoría de los casos:

  • Parquet para cualquier cosa analítica. El layout columnar hace que las consultas lean solo las columnas que necesitan: un SELECT date, event_type salta el resto del archivo. Comprime bien y todos los motores importantes lo leen de forma nativa.
  • Avro en el borde de ingesta. Basado en filas, compacto y diseñado para evolución de schema: el estándar para pipelines Kafka-a-lake.
  • Delta o Iceberg cuando necesitas actualizaciones. Deletes, merges y sentencias UPDATE requieren un formato de tabla sobre los archivos; el Parquet plano es inmutable.

Particionamiento y Layout de Archivos

Que una consulta acabe leyendo 50 archivos o 50.000 depende casi por completo del particionamiento. La convención habitual son directorios estilo hive (s3://lake/silver/events/dt=2026-10-18/) para que los motores puedan podar particiones en tiempo de consulta.

  • Elige como clave de partición la columna por la que la gente realmente filtra, casi siempre la fecha del evento. Las columnas de alta cardinalidad (user_id, country en un producto global) crean miles de particiones diminutas y empeoran las cosas.
  • Vigila los tamaños de archivo. Miles de archivos de menos de 1–10 MB destruyen el rendimiento: cada archivo es un open/read/close en Spark o Trino. Programa compactaciones con OPTIMIZE (Delta) o un job de reescritura; el objetivo son archivos de 128–512 MB.
  • Mantén rutas deterministas. zona/entidad/dt=YYYY-MM-DD/ es suficiente; anidar más profundo compra poco y complica las políticas de ciclo de vida.

Un ejemplo trabajado: 50M de eventos/día a ~1 KB cada uno son ~50 GB/día de JSON crudo. Particionado por dt, una consulta de “últimos 7 días” poda a ~350 GB en lugar de escanear todo el lake. Si cada partición diaria se escribe como archivos Parquet de ~200 MB, son ~250 archivos por día, bien dentro del rango saludable. Los mismos datos escritos como archivos de 5 MB significan ~10.000 aperturas por partición, que es exactamente la trampa de los archivos pequeños.

Ejemplo de Delta Lake

from delta import configure_spark_with_delta_pip
from pyspark.sql import SparkSession

builder = SparkSession.builder.appName("DeltaLakeExample") \
    .config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension")
spark = configure_spark_with_delta_pip(builder).getOrCreate()

# Escribir con garantías ACID
df.write.format("delta").mode("overwrite").save("/datalake/silver/orders")

# Time travel — consultar a partir de una versión específica
spark.read.format("delta").option("versionAsOf", 5).load("/datalake/silver/orders")

# Evolución de schema
df_with_new_column.write.format("delta") \
    .mode("append") \
    .option("mergeSchema", "true") \
    .save("/datalake/silver/orders")

mergeSchema maneja los cambios aditivos. Para cambios que rompen — renombres, cambios de tipo — versiona la tabla (orders_v1, orders_v2) y migra los consumidores explícitamente en lugar de mutar in situ.

Gobernanza y el Data Swamp

Sin nadie al mando, un lake sin gobernanza suele acabar con forma de swamp en unos seis meses. Cinco controles mantienen el swamp a raya.

  1. Un catálogo de datos (AWS Glue, Unity Catalog, DataHub): cada dataset registrado ahí lleva un responsable, un schema y una descripción legible. Los datasets que nunca entran al catálogo, a efectos prácticos, no existen. Una entrada mínima de catálogo se ve así: silver/events — responsable: analytics-team, formato: delta, partición: dt, pii: user_id hasheado, retención: 2 años.
  2. Responsable por dataset — cada dataset tiene una persona nombrada que responde por su calidad y su ciclo de vida. Los datos que nadie posee son datos que tarde o temprano se borran.
  3. Políticas de retención por zona (Bronze conservado según ventanas de cumplimiento, Silver uno o dos años, Gold mientras los consumidores lo usen). El storage es barato; averiguar qué datos siguen siendo válidos, no.
  4. Puertas de calidad en la ingesta: validación de schema y chequeos de nulls, para que nada llegue a Silver sin verificar. Herramientas como Great Expectations o dbt tests (mira la receta de transformaciones de modelos dbt) automatizan esto.
  5. Control de acceso por zona: Bronze a menudo contiene PII en forma cruda; restríngela a los jobs de ingesta y a un grupo pequeño de data engineering. Silver y Gold son donde los analistas y las aplicaciones obtienen acceso, idealmente a tablas donde los campos sensibles ya están hasheados o eliminados. Dar lectura amplia sobre Bronze “por comodidad” es cómo ocurren las filtraciones.

La gobernanza es también la conexión con la guía de Data Mesh: una vez que los equipos son dueños de sus dominios, el catálogo y las convenciones de zonas son lo que mantiene los datos de dominio descubribles en toda la organización.

Escenario Detallado: Pipeline de Eventos con Spark y Delta Lake

Imagina una plataforma e-commerce moviendo 50M de eventos al día: los eventos de clickstream y compra entran por Kinesis, aterrizan en S3, los jobs de Spark los promueven entre zonas, y las consultas de BI corren contra Gold vía Athena.

Sistema: Plataforma de analítica de e-commerce
Volumen: 50M eventos/día (clicks, vistas, compras)
Almacenamiento: S3 con zonas Bronze/Silver/Gold
Motor: EMR con Spark 3.5

Arquitectura:
  Fuentes -> Kinesis -> S3 Bronze (JSON crudo)
  S3 Bronze -> Spark job -> S3 Silver (Parquet + Delta)
  S3 Silver -> Spark job -> S3 Gold (agregados)
  S3 Gold -> Athena/Trino -> herramientas BI

Paso 1: Ingesta a Bronze
  Kinesis Firehose escribe JSON crudo a s3://lake/bronze/events/dt=2026-07-11/
  Partición por fecha (dt) para consultas eficientes
  Sin transformación: datos exactamente como llegan

Paso 2: Bronze a Silver (limpieza)
  $ spark-submit --master yarn \
      --num-executors 20 \
      --executor-memory 8g \
      bronze_to_silver.py

  # bronze_to_silver.py
  raw = spark.read.json("s3://lake/bronze/events/dt=2026-07-11/")
  cleaned = raw.filter("user_id is not null") \
      .withColumn("event_time", to_timestamp("timestamp")) \
      .withColumn("event_date", to_date("event_time")) \
      .dropDuplicates(["event_id"])
  cleaned.write.format("delta") \
      .mode("append") \
      .partitionBy("event_date") \
      .save("s3://lake/silver/events/")

Paso 3: Silver a Gold (agregados)
  silver = spark.read.format("delta").load("s3://lake/silver/events/")
  daily = silver.groupBy("event_date", "event_type") \
      .agg(count("*").alias("total_events"),
           countDistinct("user_id").alias("unique_users"))
  daily.write.format("delta") \
      .mode("overwrite") \
      .option("overwriteSchema", "true") \
      .save("s3://lake/gold/daily_events/")

Paso 4: Optimización
  # Compactar archivos pequeños semanalmente
  delta_table = DeltaTable.forPath(spark, "s3://lake/silver/events/")
  delta_table.optimize().compact(minFileSize="10MB")

  # Vacuum: eliminar versiones viejas (retener 7 días)
  delta_table.vacuum(retentionHours=168)

Monitoreo:
  - CloudWatch: duración de jobs, uso de memoria, archivos de salida
  - Alerta si Silver tiene > 1000 archivos pequeños (< 1MB)
  - Alerta si Gold no se actualiza en 24h
  - Calidad de datos: Great Expectations valida schema y nulls en Silver

Costo mensual estimado:
  S3 storage (50TB): ~$1,150
  EMR (20 ejecutores x 4h/día): ~$800
  Athena (consultas BI): ~$200
  Total: ~$2,150/mes

Para programar jobs como estos, la receta de scheduling de DAGs en Airflow cubre la capa de orquestación, y la agregación groupBy en Spark tiene más patrones de transformación.

La primera vez que un job de promoción falla en producción, poder re-ejecutar desde Bronze es lo que mantiene pequeño el incidente. Si una transformación de Silver sale con un bug (una clave de dedupe equivocada, un lío de zona horaria), arreglas el job, borras las particiones malas de Silver y re-ejecutas desde Bronze. Nada downstream necesita re-ingerirse porque los eventos crudos siguen ahí intactos. También es por esto que Bronze se mantiene inmutable, con una ventana de retención marcada por el cumplimiento y no por el costo de storage.

Lo Que Funciona

  • Aterriza crudo primero, transforma después: mantén Bronze inmutable para que cualquier bug downstream sea re-ejecutable
  • Un formato por zona: JSON/Avro en Bronze, Parquet o Delta en Silver y Gold; mezclar formatos por zona multiplica las herramientas
  • Particiona por patrón de consulta, no por fuente: particiones diarias dt= para datos de eventos; omite el particionamiento por debajo de ~1 GB/día
  • Cataloga todo: un dataset sin responsable ni schema es el swamp de mañana
  • Compacta en un horario: OPTIMIZE semanal para las tablas Silver calientes, mensual para Gold
  • Registra la versión ejecutable: el repo companion tiene los scripts del pipeline funcionando, enlazado en Lecturas Adicionales abajo

Errores Comunes

  • El data swamp: volcar todo sin catalogar, gobernar ni políticas de retención
  • Sin estrategia de particionamiento: consultar lakes sin particionar es dolorosamente lento; particiona por fecha y/o región
  • Usar lakes para OLTP: los lakes son para analítica, no para cargas transaccionales
  • Ignorar la gobernanza de datos: sin catálogos de metadatos y controles de acceso, los lakes se vuelven inusables
  • El problema de los archivos pequeños: escribir miles de archivos diminutos mata el rendimiento de consulta; compacta regularmente
  • Promover datos crudos directo a dashboards: las tablas Gold existen para que los consumidores nunca toquen datos sin validar
  • Borrar Bronze para ahorrar costos: la zona cruda es la única fuente de replay; usa políticas de ciclo de vida sobre particiones viejas en su lugar

Lecturas Adicionales

Preguntas frecuentes

¿Puedo consultar un Data Lake con SQL?

Sí. Motores de consulta como Athena, Presto/Trino, Dremio y Spark SQL proveen interfaces SQL sobre el almacenamiento del lake. Con un formato de tabla como Delta o Iceberg, la experiencia se acerca a la de un warehouse, incluyendo joins, updates y time travel.

¿Es un Data Lake un reemplazo de un Data Warehouse?

No exactamente. Muchas organizaciones usan ambos: lakes para datos crudos/ML, warehouses para datos curados de BI. Las arquitecturas lakehouse (Delta Lake, Iceberg) difuminan esta línea añadiendo tablas ACID al storage del lake, pero la mayoría de los equipos mantienen una capa de servicio curada para los dashboards.

¿Cómo evito que mi lake se convierta en un swamp?

Implementa cuatro controles: un catálogo de datos para el descubrimiento, políticas de retención y archivado por zona, chequeos de calidad en la ingesta y un responsable claro por dataset. Si un dataset no está registrado con un dueño, trátalo como borrable.

¿Cuál es la diferencia entre Bronze, Silver y Gold?

Bronze son datos crudos, solo-append, exactamente como llegaron: tu fuente de replay. Silver es deduplicada, validada y con schema tipado: la zona que la mayoría de las consultas deberían golpear. Gold son agregados listos para negocio construidos para consumidores específicos como dashboards o features de ML.

¿Debería usar Parquet o Delta Lake?

Empieza con Parquet para datos analíticos solo-append. Pasa a Delta Lake (o Iceberg) cuando necesites updates, deletes, garantías ACID o time travel: los formatos de tabla se sientan sobre los archivos Parquet y añaden la capa transaccional.

¿Qué tamaño deberían tener las particiones?

Apunta a particiones que produzcan archivos de 128–512 MB. Las particiones diarias por fecha son el default seguro para datos de eventos; las fuentes de muy alto volumen pueden necesitar dt+hora, mientras que los datos de bajo volumen por debajo de ~1 GB/día a menudo funcionan mejor sin particionar.

¿Cuándo debo compactar archivos en el lake?

Compacta cuando el número de archivos pequeños (< 10MB) supera los ~1000 por partición. Los archivos pequeños degradan el rendimiento porque Spark/Trino deben abrir cada archivo individualmente. Ejecuta OPTIMIZE semanalmente para tablas Silver y mensualmente para Gold. Después de compactar, ejecuta VACUUM para eliminar versiones viejas y liberar espacio.

¿Cómo manejo la evolución de schema en un Data Lake?

Usa formatos que soporten evolución de schema: Parquet, Avro o Delta Lake. Con Delta Lake, la opción mergeSchema añade columnas nuevas sin romper lecturas existentes. Para cambios mayores (renombrar columnas, cambiar tipos), usa versionado de tablas: events_v1, events_v2. Documenta el cambio en el catálogo de datos. Los consumidores deben referenciar nombres de columna, no posiciones, para evitar roturas.