Saltar al contenido

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.

  1. 01.Extract: conectar a la fuente (PostgreSQL, API, archivo) y extraer datos crudos
  2. 02.Transform: en un servidor intermedio, limpiar, desnormalizar, aplicar reglas de negocio, generar surrogate keys
  3. 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.

  1. 01.Extract: conectar a la fuente y extraer datos crudos (igual que ETL)
  2. 02.Load: cargar los datos crudos en tablas de staging del warehouse SIN transformar
  3. 03.Transform: usar SQL/Spark DENTRO del warehouse para limpiar y modelar hacia las tablas finales
ETL transforma fuera. ELT carga primero y transforma dentro con SQL. En 2024, ELT domina en la nube.

### 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 fuente
2-- Naming convention: stg_<sistema_fuente>_<tabla>
3
4CREATE SCHEMA staging;
5
6-- Réplica de la tabla orders del sistema operacional
7CREATE 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 ISO
15 updated_at VARCHAR(30),
16 -- Metadata del ETL
17 _extracted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
18 _source_file VARCHAR(200),
19 _batch_id VARCHAR(50)
20);
21
22-- Notas:
23-- 1. Tipos de dato GENEROSOS (varchar para todo) — no rechazar datos por tipos
24-- 2. Metadata del ETL (_extracted_at, _batch_id) para auditoría
25-- 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 timestamp
2
3-- 1. Obtener la marca de agua (watermark): último timestamp procesado
4SELECT COALESCE(MAX(_extracted_at), '1900-01-01') AS last_watermark
5FROM staging.stg_shopify_orders;
6
7-- 2. Extraer solo lo nuevo/modificado de la fuente
8-- (esto se haría desde Python/Spark, aquí es pseudocódigo SQL)
9INSERT INTO staging.stg_shopify_orders
10SELECT *, CURRENT_TIMESTAMP AS _extracted_at
11FROM source_shopify.orders
12WHERE updated_at > :last_watermark; -- solo cambios desde la última vez
13
14-- 3. Merge en la tabla destino (UPSERT)
15MERGE INTO dim_customer AS target
16USING (
17 SELECT DISTINCT customer_id, name, city, segment
18 FROM staging.stg_shopify_orders
19) AS source
20ON target.customer_id = source.customer_id AND target.is_current = TRUE
21WHEN MATCHED AND (target.city != source.city OR target.segment != source.segment)
22 THEN UPDATE SET is_current = FALSE, valid_to = CURRENT_DATE - 1
23WHEN NOT MATCHED
24 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 datos
2INSERT INTO fact_sales (date_key, customer_key, product_key, amount)
3SELECT date_key, customer_key, product_key, amount
4FROM staging.stg_sales
5WHERE date_key = 20240315;
6
7-- ✅ IDEMPOTENTE: borra y recarga la partición del día
8-- Si se ejecuta 2 veces, el resultado es idéntico
9DELETE 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, amount
12FROM staging.stg_sales
13WHERE date_key = 20240315;
14
15-- ✅ ALTERNATIVA IDEMPOTENTE: MERGE/UPSERT con clave natural
16MERGE INTO fact_sales AS target
17USING staging.stg_sales AS source
18ON target.order_number = source.order_number
19 AND target.product_key = source.product_key
20WHEN MATCHED THEN UPDATE SET amount = source.amount
21WHEN 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

  1. 01.EXTRACT: Python/Spark extrae datos de las fuentes (PostgreSQL, APIs, archivos) y los deposita en staging
  2. 02.STAGE: Los datos crudos aterrizan en tablas staging.stg_* (tipos generosos, sin validación)
  3. 03.VALIDATE: Queries de validación comprueban que los datos staging están completos y razonables
  4. 04.TRANSFORM DIMS: SQL actualiza las dimensiones (SCD Tipo 1/2/3 según corresponda)
  5. 05.TRANSFORM FACTS: SQL genera las filas de la fact table haciendo JOIN con las dims para resolver surrogate keys
  6. 06.LOAD: INSERT (o DELETE+INSERT idempotente) de los hechos transformados en la fact table final
  7. 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

[01]

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?

Cargando editor...
[02]

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?

Cargando editor...
[03]

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.

Cargando editor...
[04]

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
Cargando editor...

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...