Cargar 112.647 filas en una tabla de hechos debería tardar segundos. A mí me tardaba más de 25 minutos, y acababa cancelando la query. Los datos estaban bien, el SQL estaba bien, las dimensiones se poblaban sin problema. El culpable era otro, y descubrirlo fue la parte más instructiva de todo el proyecto.
Todo esto surgió construyendo un Data Warehouse en estrella sobre datos reales de e-commerce: no una tabla bonita para hacer un SELECT *, sino un modelo dimensional completo, reproducible desde cero, capaz de responder preguntas de negocio de verdad.
El dataset
Trabajé con el Brazilian E-Commerce Public Dataset by Olist: pedidos reales de un marketplace brasileño entre septiembre de 2016 y octubre de 2018. Son 9 CSV relacionados entre sí:
- 99.441 pedidos y 112.650 líneas de venta
- 103.886 pagos y 104.719 reseñas
- 32.951 productos, 3.095 vendedores
- 1.000.163 registros de geolocalización
Y con trampas de datos reales que hay que ver antes de que te muerdan:
- Un pedido puede tener varios pagos y varias reseñas. Si los unes tal cual a la tabla de hechos, duplicas ventas. Es el error clásico y silencioso: los totales salen inflados y nadie se entera.
-
customer_idno es un cliente. Olist crea uno por cada pedido; la persona real escustomer_unique_id. Contar mal aquí te cambia el KPI: hay 99.441 cuentas frente a 96.096 personas. -
El CSV de productos trae una errata en la cabecera (
product_name_lenght, con "lenght"). Si tu esquema la escribe bien y cargas por interfaz gráfica (que empareja por nombre), esas columnas se quedan vacías sin que nadie avise.
El proceso
Monté una arquitectura en capas: CSV → staging → modelo dimensional → vistas → análisis, todo en cuatro scripts ejecutables en orden y idempotentes (el esquema se recrea desde cero, se puede relanzar mil veces).
El modelo es un star schema: una tabla de hechos fact_sales al grano de línea de producto dentro de un pedido, y cinco dimensiones (cliente, producto, vendedor, pago y fecha), con claves sustitutas, PK/FK, CHECK y UNIQUE.
La clave del ETL está en deduplicar las relaciones 1:N antes de unirlas a la fact. Con ROW_NUMBER() me quedo con el pago principal (el de mayor importe) y la reseña más reciente de cada pedido. Así el grano se respeta y las ventas no se multiplican.
Y aquí llegó el INSERT eterno
Las dimensiones cargaban perfectas. La fact se quedaba colgada. Descarté bloqueos (la sesión estaba active, no esperando), descarté fechas corruptas (el rango era correcto). ¿Entonces?
El problema estaba en que todo el ETL corre dentro de una única transacción. Cuando cargo la fact, las dimensiones se acaban de rellenar en esa misma transacción sin confirmar: PostgreSQL no tiene estadísticas actualizadas de ellas y cree que están vacías. Con esa estimación errónea, el planificador elige un Nested Loop con Seq Scan — recorrer la dimensión entera por cada una de las 112.650 filas. Miles de millones de comparaciones.
La solución cabe en cinco líneas, justo antes de cargar la fact:
ANALYZE ecommerce_dw.dim_customer;
ANALYZE ecommerce_dw.dim_product;
ANALYZE ecommerce_dw.dim_seller;
ANALYZE ecommerce_dw.dim_payment;
ANALYZE ecommerce_dw.dim_date;
ANALYZE actualiza las estadísticas (y, a diferencia de VACUUM, sí funciona dentro de una transacción). El planificador pasa a Hash Join y la carga baja a segundos.
Lección: el optimizador no es magia, es un modelo estadístico. Si le mientes sobre el tamaño de tus tablas, te devuelve un plan absurdo. Y no te avisa: simplemente tarda para siempre.
Los resultados
Con el DW cargado (112.647 líneas, 98.665 pedidos, 95.419 clientes reales, 15,8M R$ de facturación), escribí 12 consultas de negocio con CTEs, funciones ventana y funciones propias. Tres hallazgos que me sorprendieron:
1. La logística no influye en la satisfacción: la determina. Cruzando retraso de entrega con valoración media (de pedidos entregados):
| Entrega | Valoración media |
|---|---|
| Sin retraso | 4,21 |
| Retraso leve (1-3 días) | 3,23 |
| Retraso medio (4-7 días) | 2,09 |
| Retraso grave (>7 días) | 1,70 |
No es una correlación suave. Es un desplome.
2. Nadie repite. Solo el 3,05% de los clientes vuelve a comprar, y aportan el 5,71% de los ingresos. El marketplace es excelente captando y pésimo reteniendo. Ese es el mayor agujero de negocio del dataset.
3. Los "power sellers" mandan. El cuartil superior de vendedores (NTILE(4)) genera el 86,58% de la facturación. Más concentrado incluso que un Pareto 80/20.
Lo que aprendí
Lo que más me marcó no fue el SQL avanzado, sino que las decisiones invisibles son las que arruinan un análisis: un JOIN mal planteado que duplica ventas, contar clientes por la columna equivocada, una errata en una cabecera. Nada de eso lanza un error. Simplemente te da un número falso con toda la confianza del mundo.
Lo siguiente que tiene sentido: cargas incrementales en lugar de recrear el DW entero, y una dimensión de cliente con historial (SCD tipo 2) para seguir la evolución del comportamiento de compra.
📁 Repo completo en GitHub: https://github.com/dnarram/olist-ecommerce-datawarehouse
Proyecto desarrollado durante el Máster en Data Science de Evolve.
Top comments (0)