lección 6
ETL vs ELT: cómo llegan los datos al warehouse
Patrones de carga de datos: staging areas, transformación, carga incremental vs full refresh, e idempotencia.
⏱ 55 min
Ya sabes CÓMO organizar los datos en el warehouse (estrella, hechos, dimensiones). Pero hay una pregunta crucial que no hemos respondido: ¿CÓMO llegan los datos desde la base operacional hasta el warehouse? No es magia — alguien tiene que escribir código que extraiga datos del sistema fuente, los transforme al modelo dimensional y los cargue en las tablas destino. Ese proceso es el ETL (Extract-Transform-Load) o ELT (Extract-Load-Transform), y es probablemente el 70% del trabajo real de un ingeniero de datos.
La analogía: imagina una fábrica de muebles. La madera llega en bruto del bosque (Extract). En el taller la cortan, lijan y pintan (Transform). El mueble terminado se lleva a la tienda (Load). ETL es eso: extraer materia prima, transformarla, y cargar el producto final. ELT cambia el orden: primero llevas la madera bruta a la tienda (Load) y después la transformas allí mismo. ¿Cuándo tiene sentido cada enfoque? Vamos a verlo.
### ETL clásico: transformar ANTES de cargar
ETL fue el paradigma dominante durante 30 años (1990-2020). Herramientas como Informatica, DataStage y Talend extraían datos de las fuentes, los transformaban en un servidor intermedio (el ETL server), y cargaban el resultado limpio en el warehouse. El warehouse solo recibía datos LIMPIOS y MODELADOS.
- 01.Extract: conectar a la fuente (PostgreSQL, API, archivo) y extraer datos crudos
- 02.Transform: en un servidor intermedio, limpiar, desnormalizar, aplicar reglas de negocio, generar surrogate keys
- 03.Load: insertar los datos transformados en las tablas del warehouse (fact + dims)
¿Por qué se hacía así? Porque los warehouses on-premise (Teradata, Oracle DW) tenían capacidad de cómputo LIMITADA y CARA. No querías desperdiciar ciclos de tu carísimo Teradata haciendo limpieza — mejor hacerla fuera y cargar solo lo necesario.
### ELT moderno: cargar CRUDO y transformar en destino
Con la nube (BigQuery, Redshift, Snowflake, Databricks), el compute se volvió elástico y barato. Ya no tiene sentido mantener un servidor ETL intermedio cuando tu warehouse tiene capacidad de sobra para hacer las transformaciones. ELT dice: extrae los datos, cárgalos TAL CUAL en una zona de staging del warehouse, y después usa SQL (o dbt, Spark) para transformarlos DENTRO del warehouse.
- 01.Extract: conectar a la fuente y extraer datos crudos (igual que ETL)
- 02.Load: cargar los datos crudos en tablas de staging del warehouse SIN transformar
- 03.Transform: usar SQL/Spark DENTRO del warehouse para limpiar y modelar hacia las tablas finales
### La staging area: zona de aterrizaje
Tanto en ETL como en ELT, los datos pasan por una staging area (zona de aterrizaje). Es un espacio temporal donde los datos aterrizan desde la fuente SIN transformaciones. La staging es una copia cruda del sistema fuente. ¿Por qué no transformar directamente? Porque si algo sale mal en la transformación, necesitas volver a los datos originales sin volver a extraer de la fuente (que puede ser lento o costoso).
1-- Staging area: réplica cruda de la fuente2-- Naming convention: stg_<sistema_fuente>_<tabla>34CREATE SCHEMA staging;56-- Réplica de la tabla orders del sistema operacional7CREATE TABLE staging.stg_shopify_orders (8 -- Columnas EXACTAS del sistema fuente (sin transformar)9 id VARCHAR(50),10 customer_id VARCHAR(50),11 status VARCHAR(20),12 total_amount VARCHAR(20), -- ¡Viene como string de la API!13 currency VARCHAR(3),14 created_at VARCHAR(30), -- timestamp como string ISO15 updated_at VARCHAR(30),16 -- Metadata del ETL17 _extracted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,18 _source_file VARCHAR(200),19 _batch_id VARCHAR(50)20);2122-- Notas:23-- 1. Tipos de dato GENEROSOS (varchar para todo) — no rechazar datos por tipos24-- 2. Metadata del ETL (_extracted_at, _batch_id) para auditoría25-- 3. Se TRUNCA y recarga cada ejecución (no es histórica)
La staging area es tu red de seguridad. Si la transformación falla, puedes re-ejecutarla sin re-extraer.
### Carga incremental vs Full Refresh
Cuando tu tabla fuente tiene 100 millones de filas, no quieres copiarlas TODAS cada día. La carga incremental solo extrae los datos NUEVOS o MODIFICADOS desde la última ejecución. Pero implementarla correctamente es uno de los retos más difíciles del data engineering.
- Full refresh: DROP + recrear toda la tabla destino cada ejecución. Simple pero lento con tablas grandes.
- Incremental por timestamp: extraer filas con updated_at > última_ejecución. Rápido pero requiere que la fuente tenga un campo updated_at fiable.
- Incremental por ID: extraer filas con id > máximo_id_en_destino. Solo funciona para INSERTs, no detecta UPDATEs.
- CDC (Change Data Capture): capturar el log de cambios de la base fuente (binlog en MySQL, WAL en PostgreSQL). El más completo pero el más complejo.
1-- Patrón: Carga incremental por timestamp23-- 1. Obtener la marca de agua (watermark): último timestamp procesado4SELECT COALESCE(MAX(_extracted_at), '1900-01-01') AS last_watermark5FROM staging.stg_shopify_orders;67-- 2. Extraer solo lo nuevo/modificado de la fuente8-- (esto se haría desde Python/Spark, aquí es pseudocódigo SQL)9INSERT INTO staging.stg_shopify_orders10SELECT *, CURRENT_TIMESTAMP AS _extracted_at11FROM source_shopify.orders12WHERE updated_at > :last_watermark; -- solo cambios desde la última vez1314-- 3. Merge en la tabla destino (UPSERT)15MERGE INTO dim_customer AS target16USING (17 SELECT DISTINCT customer_id, name, city, segment18 FROM staging.stg_shopify_orders19) AS source20ON target.customer_id = source.customer_id AND target.is_current = TRUE21WHEN MATCHED AND (target.city != source.city OR target.segment != source.segment)22 THEN UPDATE SET is_current = FALSE, valid_to = CURRENT_DATE - 123WHEN NOT MATCHED24 THEN INSERT (customer_id, name, city, segment, is_current, valid_from, valid_to)25 VALUES (source.customer_id, source.name, source.city, source.segment, TRUE, CURRENT_DATE, '9999-12-31');
Carga incremental + SCD Tipo 2 combinados. Este es el patrón real que usarás en producción.
### Idempotencia: la propiedad más importante de tu ETL
Un proceso es idempotente si ejecutarlo 1 vez produce el MISMO resultado que ejecutarlo 5 veces. ¿Por qué importa? Porque en producción, las cosas fallan. La red se cae a mitad de una carga. Un job se ejecuta dos veces por un bug en el scheduler. Si tu ETL no es idempotente, tendrás datos duplicados, conteos inflados o registros faltantes.
1-- ❌ NO IDEMPOTENTE: si se ejecuta 2 veces, duplica datos2INSERT INTO fact_sales (date_key, customer_key, product_key, amount)3SELECT date_key, customer_key, product_key, amount4FROM staging.stg_sales5WHERE date_key = 20240315;67-- ✅ IDEMPOTENTE: borra y recarga la partición del día8-- Si se ejecuta 2 veces, el resultado es idéntico9DELETE FROM fact_sales WHERE date_key = 20240315;10INSERT INTO fact_sales (date_key, customer_key, product_key, amount)11SELECT date_key, customer_key, product_key, amount12FROM staging.stg_sales13WHERE date_key = 20240315;1415-- ✅ ALTERNATIVA IDEMPOTENTE: MERGE/UPSERT con clave natural16MERGE INTO fact_sales AS target17USING staging.stg_sales AS source18ON target.order_number = source.order_number19 AND target.product_key = source.product_key20WHEN MATCHED THEN UPDATE SET amount = source.amount21WHEN NOT MATCHED THEN INSERT (date_key, customer_key, product_key, amount)22VALUES (source.date_key, source.customer_key, source.product_key, source.amount);
DELETE + INSERT por partición es el patrón idempotente más simple y robusto.
Patrón que uso SIEMPRE: particionar la fact table por fecha (date_key) y hacer DELETE + INSERT de la partición del día. Es idempotente por definición: no importa cuántas veces ejecutes el job del 15 de marzo, siempre borra y recarga SOLO el 15 de marzo. Simple, predecible, auditable.
### El flujo completo: de staging a estrella
- 01.EXTRACT: Python/Spark extrae datos de las fuentes (PostgreSQL, APIs, archivos) y los deposita en staging
- 02.STAGE: Los datos crudos aterrizan en tablas staging.stg_* (tipos generosos, sin validación)
- 03.VALIDATE: Queries de validación comprueban que los datos staging están completos y razonables
- 04.TRANSFORM DIMS: SQL actualiza las dimensiones (SCD Tipo 1/2/3 según corresponda)
- 05.TRANSFORM FACTS: SQL genera las filas de la fact table haciendo JOIN con las dims para resolver surrogate keys
- 06.LOAD: INSERT (o DELETE+INSERT idempotente) de los hechos transformados en la fact table final
- 07.AUDIT: Registrar en una tabla de log qué se procesó, cuántas filas, si hubo errores
El fallo más común en ETLs: no manejar las dependencias de orden. Las dimensiones DEBEN cargarse ANTES que los hechos. ¿Por qué? Porque la fact table necesita los surrogate keys de las dimensiones. Si cargas la fact table antes de actualizar dim_customer con un cliente nuevo, esa FK será NULL o errónea. El orden es siempre: staging → dimensiones → hechos.
## ejercicios
Diseñar staging tables para un e-commerce
Diseña las tablas de staging para cargar datos desde una API de Shopify. Necesitas: orders, customers y products. Recuerda: tipos generosos, metadata ETL, sin transformaciones. 💡 Cómo saber si tus tres staging tables valen (antes de abrir la solución): (1) Cuenta los tipos distintos que has usado — si hay más de VARCHAR, TEXT y TIMESTAMP (solo en _extracted_at), estás validando en el sitio equivocado. (2) Las tres tablas tienen que acabar en las MISMAS columnas de metadatos. (3) Prueba mental: el API empieza a mandar importes con coma en vez de punto. ¿Tu tabla los acepta?
Implementar carga incremental idempotente
Escribe el SQL para cargar fact_sales del día 2024-03-15 de forma idempotente. Los datos están en staging.stg_transformed_line_items (una fila por línea de pedido, ya desmontada del JSON de orders). Necesitas resolver surrogate keys haciendo JOIN con las dimensiones. 💡 Cómo saber si tu carga vale: (1) Ejecútala mentalmente dos veces seguidas — ¿la segunda vez duplica filas? Si sí, te falta el DELETE. (2) Cuenta los JOINs: uno por cada dimensión que necesite surrogate key. (3) Prueba mental: un pedido tiene un customer_id que aún no está en dim_customer. ¿Tu carga lo pierde o lo marca como -1?
Ordenar los pasos del ETL correctamente
Tu pipeline ETL tiene estos pasos desordenados. Numéralos en el orden correcto de ejecución y explica por qué. 💡 Cómo saber si tu orden vale (antes de mirar la solución): para cada paso, pregúntate «¿qué necesito que ya haya pasado?» Los hechos necesitan las claves de las dimensiones → dimensiones antes. La validación necesita datos en staging → extraer antes. Si tu orden cumple las dependencias, es correcto. Y si dudas entre dos pasos que no se necesitan mutuamente (dim_customer y dim_product), da igual cuál va primero.
Hacer idempotente un ETL con bugs
Este ETL tiene un bug: si se ejecuta 2 veces el mismo día, duplica datos. Corrígelo para que sea idempotente. Ejecútalo DOS VECES y comprueba que la segunda vez no duplica. 💡 Resultado esperado: las dos ejecuciones tienen que dar exactamente 5 filas y 241 segundos en total. Si te da 8 filas o 10 filas, te falta el DELETE. Si te da 4 filas, sigues con INNER JOIN y has perdido la visita de U3 (usuario que no está en la dimensión).
💡 Resultado esperado
date_key | user_key | page_key | duration_seconds 20240315 | -1 | 100 | 44 20240315 | 10 | 100 | 30 20240315 | 10 | 101 | 95
Regístrate para guardar tu progreso.
## comentarios
Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.
Inicia sesión para comentar y responder.
cargando comentarios...