lección 9
Proyecto: diseñar el warehouse completo de un e-commerce
Aplica todo lo aprendido: diseña el modelo dimensional, ETL, métricas y SCD para "TechMart", una tienda online ficticia.
⏱ 60 min
Ha llegado el momento de poner todo junto. En esta lección no hay teoría nueva — es pura aplicación. Vas a diseñar el Data Warehouse COMPLETO de TechMart, una tienda online de electrónica con 50.000 pedidos/mes, 3 canales de venta (web, app móvil, marketplace) y un equipo de analistas que necesita responder preguntas de negocio diariamente sin pedir ayuda al equipo de datos.
El brief del CTO es claro: "Quiero que cualquier analista pueda abrir Metabase y responder preguntas como: ¿cuáles son nuestros productos estrella? ¿Qué segmentos crecen más? ¿Dónde está cayendo el margen? ¿Cuánto tarda el fulfillment? Y lo quiero sin que tenga que pedirle a un ingeniero que escriba una query custom cada vez."
### Paso 1: Entender el sistema fuente
TechMart tiene un PostgreSQL operacional con el siguiente esquema normalizado. Es el típico esquema de app con 3NF estricto: muchas tablas, muchas FKs, cero redundancia. Perfecto para la app, terrible para análisis.
1-- ═══ SISTEMA FUENTE: PostgreSQL operacional de TechMart ═══23-- Clientes4CREATE TABLE app.customers (5 id SERIAL PRIMARY KEY,6 email VARCHAR(200) UNIQUE,7 first_name VARCHAR(100),8 last_name VARCHAR(100),9 phone VARCHAR(20),10 created_at TIMESTAMP,11 updated_at TIMESTAMP12);1314CREATE TABLE app.addresses (15 id SERIAL PRIMARY KEY,16 customer_id INTEGER REFERENCES app.customers(id),17 street VARCHAR(300),18 city VARCHAR(100),19 state VARCHAR(50),20 country VARCHAR(50),21 postal_code VARCHAR(10),22 is_default BOOLEAN23);2425-- Productos26CREATE TABLE app.products (27 id SERIAL PRIMARY KEY,28 sku VARCHAR(20) UNIQUE,29 name VARCHAR(300),30 description TEXT,31 category_id INTEGER REFERENCES app.categories(id),32 brand_id INTEGER REFERENCES app.brands(id),33 price DECIMAL(10,2),34 cost DECIMAL(10,2),35 stock INTEGER,36 is_active BOOLEAN,37 created_at TIMESTAMP,38 updated_at TIMESTAMP39);4041CREATE TABLE app.categories (42 id SERIAL PRIMARY KEY,43 name VARCHAR(50),44 parent_id INTEGER REFERENCES app.categories(id)45);4647CREATE TABLE app.brands (48 id SERIAL PRIMARY KEY,49 name VARCHAR(100),50 country VARCHAR(50)51);5253-- Pedidos54CREATE TABLE app.orders (55 id SERIAL PRIMARY KEY,56 customer_id INTEGER REFERENCES app.customers(id),57 status VARCHAR(20), -- pending, confirmed, shipped, delivered, cancelled58 channel VARCHAR(20), -- web, app, marketplace59 total_amount DECIMAL(12,2),60 discount_code VARCHAR(30),61 shipping_cost DECIMAL(8,2),62 created_at TIMESTAMP,63 confirmed_at TIMESTAMP,64 shipped_at TIMESTAMP,65 delivered_at TIMESTAMP66);6768CREATE TABLE app.order_items (69 id SERIAL PRIMARY KEY,70 order_id INTEGER REFERENCES app.orders(id),71 product_id INTEGER REFERENCES app.products(id),72 quantity INTEGER,73 unit_price DECIMAL(10,2),74 discount_pct DECIMAL(5,2)75);
Esquema normalizado típico de una app. 8 tablas con FKs cruzadas. No apto para análisis directo.
### Paso 2: Identificar los procesos de negocio
Antes de dibujar tablas, preguntamos: ¿qué PROCESOS quiere analizar el negocio? Cada proceso será una fact table. Para TechMart identificamos 3 procesos clave:
- 01.Ventas (transaccional): cada línea de pedido confirmado → fact_sales
- 02.Fulfillment (accumulating): ciclo de vida del pedido → fact_order_fulfillment
- 03.Inventario (snapshot): stock diario por producto → fact_daily_inventory
### Paso 3: Declarar la granularidad
- fact_sales: "Una fila por cada línea de artículo en cada pedido confirmado"
- fact_order_fulfillment: "Una fila por cada pedido, rastreando su ciclo desde creación hasta entrega"
- fact_daily_inventory: "Una fila por cada producto al cierre de cada día"
### Paso 4: Diseñar las dimensiones conformed
1-- ═══ DIMENSIONES CONFORMED DE TECHMART ═══23CREATE TABLE dim_date (4 date_key INTEGER PRIMARY KEY, -- YYYYMMDD5 full_date DATE NOT NULL,6 day_name VARCHAR(10),7 month_name VARCHAR(10),8 month_number SMALLINT,9 quarter SMALLINT,10 year SMALLINT,11 is_weekend BOOLEAN,12 is_holiday BOOLEAN,13 fiscal_quarter SMALLINT,14 week_of_year SMALLINT15);1617CREATE TABLE dim_customer (18 customer_key BIGINT PRIMARY KEY,19 customer_id INTEGER NOT NULL, -- natural key de app.customers20 full_name VARCHAR(200),21 email VARCHAR(200),22 country VARCHAR(50),23 city VARCHAR(100),24 -- Segmentación (calculada por el warehouse, no por la app)25 customer_segment VARCHAR(30), -- 'VIP', 'Regular', 'Nuevo', 'Inactivo'26 acquisition_month VARCHAR(7), -- '2023-04'27 lifetime_orders INTEGER,28 -- SCD Tipo 2 para: country, city, customer_segment29 is_current BOOLEAN DEFAULT TRUE,30 valid_from DATE,31 valid_to DATE DEFAULT '9999-12-31'32);3334CREATE TABLE dim_product (35 product_key BIGINT PRIMARY KEY,36 product_id INTEGER NOT NULL,37 sku VARCHAR(20),38 product_name VARCHAR(300),39 category VARCHAR(50), -- desnormalizado (no FK a otra tabla)40 parent_category VARCHAR(50), -- jerarquía plana41 brand VARCHAR(100),42 brand_country VARCHAR(50),43 current_price DECIMAL(10,2),44 current_cost DECIMAL(10,2),45 -- SCD Tipo 2 para: category, current_price46 is_current BOOLEAN DEFAULT TRUE,47 valid_from DATE,48 valid_to DATE DEFAULT '9999-12-31'49);5051CREATE TABLE dim_channel (52 channel_key BIGINT PRIMARY KEY,53 channel_name VARCHAR(30), -- 'Web', 'App Móvil', 'Marketplace'54 channel_type VARCHAR(20), -- 'owned', 'third_party'55 platform VARCHAR(50) -- 'techmart.com', 'iOS App', 'Amazon'56);
Dimensiones conformed compartidas entre las 3 fact tables. Nota el SCD Tipo 2 en customer y product.
### Paso 5: Diseñar las fact tables
1-- ═══ FACT TABLES DE TECHMART ═══23-- 1. TRANSACCIONAL: cada línea de venta4CREATE TABLE fact_sales (5 sales_key BIGINT PRIMARY KEY,6 -- FKs a dimensiones7 date_key INTEGER NOT NULL REFERENCES dim_date(date_key),8 customer_key INTEGER NOT NULL REFERENCES dim_customer(customer_key),9 product_key INTEGER NOT NULL REFERENCES dim_product(product_key),10 channel_key INTEGER NOT NULL REFERENCES dim_channel(channel_key),11 -- Dimensión degenerada12 order_number VARCHAR(20),13 -- Métricas aditivas14 quantity SMALLINT NOT NULL,15 unit_price DECIMAL(10,2) NOT NULL,16 unit_cost DECIMAL(10,2) NOT NULL,17 discount_amount DECIMAL(10,2) DEFAULT 0,18 net_revenue DECIMAL(12,2) NOT NULL, -- qty * price * (1-discount)19 gross_profit DECIMAL(12,2) NOT NULL, -- net_revenue - (qty * cost)20 shipping_allocated DECIMAL(8,2) -- shipping prorrateado por línea21);2223-- 2. ACCUMULATING SNAPSHOT: ciclo de vida del pedido24CREATE TABLE fact_order_fulfillment (25 order_key BIGINT PRIMARY KEY,26 order_number VARCHAR(20),27 customer_key INTEGER REFERENCES dim_customer(customer_key),28 channel_key INTEGER REFERENCES dim_channel(channel_key),29 -- Date keys por hito (se llenan progresivamente)30 created_date_key INTEGER REFERENCES dim_date(date_key),31 confirmed_date_key INTEGER,32 shipped_date_key INTEGER,33 delivered_date_key INTEGER,34 -- Métricas35 order_amount DECIMAL(12,2),36 num_items SMALLINT,37 -- Duración entre hitos38 hours_to_confirm INTEGER,39 hours_to_ship INTEGER,40 hours_to_deliver INTEGER,41 total_hours INTEGER,42 -- Estado actual43 current_status VARCHAR(20)44);4546-- 3. SNAPSHOT PERIÓDICO: inventario diario47CREATE TABLE fact_daily_inventory (48 date_key INTEGER NOT NULL REFERENCES dim_date(date_key),49 product_key INTEGER NOT NULL REFERENCES dim_product(product_key),50 -- Métricas semi-aditivas51 quantity_on_hand INTEGER,52 quantity_reserved INTEGER,53 days_of_supply DECIMAL(5,1),54 is_out_of_stock BOOLEAN,55 PRIMARY KEY (date_key, product_key)56);
3 fact tables para 3 procesos: ventas (transaccional), fulfillment (accumulating) e inventario (snapshot).
### Paso 6: Diseñar el flujo ETL
Ahora definimos CÓMO fluyen los datos desde el PostgreSQL operacional hasta nuestro warehouse. El patrón: extraer de la app cada noche, aterrizar en staging, transformar dimensiones (SCD), luego cargar hechos.
1-- ═══ ETL NOCTURNO DE TECHMART ═══2-- Se ejecuta cada día a las 02:0034-- PASO 1: Extraer a staging (carga incremental por updated_at)5-- Python extrae de app.customers WHERE updated_at > last_watermark6-- Python extrae de app.orders WHERE updated_at > last_watermark7-- Python extrae de app.order_items via JOIN con orders nuevas8-- Python extrae de app.products WHERE updated_at > last_watermark910-- PASO 2: Cargar dim_customer (SCD Tipo 2 para segment/city)11-- Detectar cambios:12WITH customer_changes AS (13 SELECT14 s.id AS customer_id,15 s.first_name || ' ' || s.last_name AS full_name,16 a.country, a.city,17 CASE18 WHEN o.total_orders >= 10 AND o.total_spent >= 1000 THEN 'VIP'19 WHEN o.total_orders >= 3 THEN 'Regular'20 WHEN o.first_order > CURRENT_DATE - 90 THEN 'Nuevo'21 ELSE 'Inactivo'22 END AS customer_segment23 FROM staging.stg_customers s24 LEFT JOIN staging.stg_addresses a ON s.id = a.customer_id AND a.is_default25 LEFT JOIN (26 SELECT customer_id, COUNT(*) total_orders, SUM(total_amount) total_spent,27 MIN(created_at) first_order28 FROM staging.stg_orders WHERE status != 'cancelled'29 GROUP BY customer_id30 ) o ON s.id = o.customer_id31)32-- Cerrar versiones que cambiaron + insertar nuevas (SCD Tipo 2)33-- (ver patrón de lección 5)3435-- PASO 3: Cargar dim_product (SCD Tipo 2 para category/price)36-- Similar: detectar cambios en categoría o precio → nueva versión3738-- PASO 4: Cargar fact_sales (idempotente: DELETE + INSERT del día)39DELETE FROM fact_sales40WHERE date_key = CAST(strftime(CURRENT_DATE - INTERVAL '1 day', '%Y%m%d') AS INTEGER);4142INSERT INTO fact_sales (date_key, customer_key, product_key, channel_key,43 order_number, quantity, unit_price, unit_cost,44 discount_amount, net_revenue, gross_profit)45SELECT46 CAST(strftime(o.confirmed_at::DATE, '%Y%m%d') AS INTEGER),47 dc.customer_key,48 dp.product_key,49 dch.channel_key,50 'ORD-' || o.id,51 oi.quantity,52 oi.unit_price,53 p.cost,54 oi.quantity * oi.unit_price * COALESCE(oi.discount_pct, 0) / 100,55 oi.quantity * oi.unit_price * (1 - COALESCE(oi.discount_pct, 0) / 100),56 oi.quantity * oi.unit_price * (1 - COALESCE(oi.discount_pct, 0) / 100)57 - oi.quantity * p.cost58FROM staging.stg_orders o59JOIN staging.stg_order_items oi ON o.id = oi.order_id60JOIN staging.stg_products p ON oi.product_id = p.id61LEFT JOIN dim_customer dc ON o.customer_id = dc.customer_id AND dc.is_current62LEFT JOIN dim_product dp ON oi.product_id = dp.product_id AND dp.is_current63LEFT JOIN dim_channel dch ON o.channel = dch.channel_name64WHERE o.status = 'confirmed'65 AND o.confirmed_at::DATE = CURRENT_DATE - 1;
ETL completo: staging → dims (SCD) → facts (idempotente). Este es el pipeline real de producción.
### Paso 7: Queries de negocio que el warehouse debe responder
El test final de tu diseño: ¿puede un analista responder las preguntas del CTO con SQL simple? Si necesita subqueries anidadas de 50 líneas, el diseño no está bien. Veamos las queries reales que los analistas de TechMart ejecutarán:
1-- ═══ QUERIES DE NEGOCIO SOBRE EL WAREHOUSE ═══23-- 1. Revenue mensual por canal (dashboard principal)4SELECT d.year, d.month_name, ch.channel_name,5 SUM(f.net_revenue) AS revenue,6 COUNT(DISTINCT f.order_number) AS orders7FROM fact_sales f8JOIN dim_date d ON f.date_key = d.date_key9JOIN dim_channel ch ON f.channel_key = ch.channel_key10WHERE d.year = 202411GROUP BY d.year, d.month_name, d.month_number, ch.channel_name12ORDER BY d.month_number;1314-- 2. Top 10 productos por margen (para compras)15SELECT p.product_name, p.brand, p.category,16 SUM(f.gross_profit) AS total_profit,17 SUM(f.gross_profit) / NULLIF(SUM(f.net_revenue), 0) * 100 AS margin_pct18FROM fact_sales f19JOIN dim_product p ON f.product_key = p.product_key20WHERE f.date_key >= 2024010121GROUP BY p.product_name, p.brand, p.category22ORDER BY total_profit DESC LIMIT 10;2324-- 3. Tiempo medio de fulfillment por canal (SLA check)25SELECT ch.channel_name,26 AVG(f.total_hours) AS avg_hours_total,27 AVG(f.hours_to_ship) AS avg_hours_to_ship,28 PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY f.total_hours) AS p95_hours29FROM fact_order_fulfillment f30JOIN dim_channel ch ON f.channel_key = ch.channel_key31WHERE f.delivered_date_key IS NOT NULL32 AND f.created_date_key >= 2024010133GROUP BY ch.channel_name;3435-- 4. Productos en riesgo de rotura de stock36SELECT p.product_name, p.category,37 inv.quantity_on_hand, inv.days_of_supply38FROM fact_daily_inventory inv39JOIN dim_product p ON inv.product_key = p.product_key40JOIN dim_date d ON inv.date_key = d.date_key41WHERE d.full_date = CURRENT_DATE - 142 AND inv.days_of_supply < 743 AND p.is_current = TRUE44ORDER BY inv.days_of_supply ASC;
Queries simples gracias al buen diseño. Un analista junior puede escribir cualquiera de estas.
El test definitivo de un buen warehouse: dale acceso a un analista que NO participó en el diseño. Si puede responder preguntas de negocio sin pedirte ayuda, el diseño es correcto. Si necesita preguntarte "¿qué tabla uso para saber X?" constantemente, necesitas mejor naming, documentación o rediseño. Los mejores warehouses son autodocumentados por sus nombres.
### Paso 8: Documentación y data dictionary
Un warehouse sin documentación es un warehouse que solo entiende quien lo construyó. Y cuando esa persona se va (o se olvida), se convierte en un misterio. El data dictionary es la documentación viva de cada tabla y columna: qué significa, de dónde viene, cómo se calcula, con qué frecuencia se actualiza.
1-- Data dictionary como comentarios SQL (funciona en PostgreSQL/Redshift)2COMMENT ON TABLE fact_sales IS3 'Tabla de hechos transaccional. Grain: una fila por línea de pedido confirmado.4 Actualización: diaria a las 02:00 (ETL nocturno). Retención: 5 años.';56COMMENT ON COLUMN fact_sales.net_revenue IS7 'Revenue neto = quantity * unit_price * (1 - discount_pct/100).8 Aditiva en todas las dimensiones. Fuente: app.order_items.';910COMMENT ON COLUMN fact_sales.gross_profit IS11 'Beneficio bruto = net_revenue - (quantity * unit_cost).12 Aditiva. Útil para análisis de margen por producto/categoría.';1314COMMENT ON TABLE dim_customer IS15 'Dimensión de cliente. SCD Tipo 2 en: country, city, customer_segment.16 SCD Tipo 1 en: email, phone. customer_segment se recalcula cada noche.';
COMMENT ON es infrautilizado. Es documentación que vive DENTRO de la base de datos, siempre actualizada.
El proyecto no termina cuando las tablas están creadas. Termina cuando el primer analista ejecuta su primera query y obtiene la respuesta correcta sin ayuda. Si eso no pasa, tu warehouse es un éxito técnico y un fracaso de producto. Siempre valida con usuarios finales ANTES de dar por terminado el diseño.
### Has terminado cuando…
- Puedes escribir la frase de grano de cada fact table, y son distintas entre sí ("una fila por línea de pedido confirmado", "una fila por pedido", "una fila por producto y día").
- Tus tres dimensiones conformed (dim_date, dim_customer, dim_product) son LA MISMA tabla para las tres fact tables. Compruébalo: ¿puedes responder "productos que se venden mucho y se devuelven mucho"?
- dim_customer y dim_product tienen is_current, valid_from y valid_to, y hay exactamente UNA fila vigente por clave natural.
- Tu ETL se puede ejecutar dos veces seguidas y la fact table no cambia. Ejecútalo dos veces y cuenta las filas — si crecen, no es idempotente.
- Ninguna de tus métricas es un porcentaje o una media guardada. Si hay un _rate o un _avg, guarda sus componentes.
- Los cinco checks del ejercicio 4 se ejecutan DESPUÉS de cada carga y ANTES de que nadie abra un dashboard.
- Tienes un diccionario de datos (COMMENT ON) con la definición de negocio de cada métrica. Prueba: dale tu diccionario a alguien y que calcule el revenue de marzo. Si te tiene que preguntar algo, el diccionario está incompleto.
Para tu portfolio, este proyecto es enseñable en una entrevista. Lo que demuestra de ti: (1) el diagrama del esquema estrella con las tres fact tables y las conformed dimensions; (2) la frase de grano de cada una — es lo primero que te van a preguntar; (3) el ADR de la lección anterior justificando por qué Kimball y no Data Vault; (4) los cinco checks de validación — es lo que distingue a alguien que ha puesto un warehouse en producción de alguien que ha hecho un tutorial.
## ejercicios
Completar las dimensiones faltantes de TechMart
TechMart también necesita dim_promotion (para descuentos/cupones) y dim_shipping_method. Diseña ambas dimensiones con todos los atributos relevantes para análisis.
Escribir el ETL del accumulating snapshot
Escribe el SQL que actualiza fact_order_fulfillment cuando un pedido pasa de "shipped" a "delivered". Debe ser idempotente.
Crear vistas de métricas de negocio
Crea 3 vistas SQL que el equipo de analistas usará como "métricas oficiales": monthly_kpis, product_performance y customer_cohorts.
Escribir checks de validación post-ETL
Escribe 5 queries de validación que se ejecutan DESPUÉS del ETL cada noche para detectar problemas antes de que los vea el negocio.
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...