├── .gitignore ├── sql ├── README.md ├── 01-window-functions.md ├── 02-second-highest-salary-per-department.md ├── 03-pivot-months-as-columns.md ├── 04-total-outstanding-loan-liability.md ├── 06-query-optimization-indexes.md ├── 05-cte-common-table-expressions.md ├── 07-self-join-hierarchies.md ├── 08-aggregations-group-by-having.md └── 09-data-quality-null-handling.md ├── data-modeling └── README.md ├── cloud ├── README.md ├── 04-multi-cloud-strategy.md └── 01-aws-data-stack.md ├── streaming ├── README.md ├── 02-stream-processing-windowing.md └── 03-lambda-kappa-architectures.md ├── orchestration └── README.md ├── python-dsa ├── README.md └── 01-strings-text-processing.md ├── pyspark ├── README.md ├── 06-udfs.md ├── 07-spark-sql.md ├── 01-rdd-vs-dataframe.md ├── 02-transformations-actions.md ├── 05-window-functions.md ├── 03-optimization-caching.md └── 04-data-types-schemas.md ├── system-design ├── README.md └── 04-data-quality-monitoring.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__/ 3 | .DS_Store 4 | *.swp 5 | .vscode/ 6 | .idea/ 7 | -------------------------------------------------------------------------------- /sql/README.md: -------------------------------------------------------------------------------- 1 | # SQL en Entrevistas de Data Engineering 2 | 3 | ## ¿Por qué SQL es importante? 4 | 5 | SQL es la **habilidad número 1** en data engineering. 6 | En entrevistas, esperan que domines: 7 | 8 | - Window Functions 9 | - Joins complejos 10 | - CTEs y subqueries 11 | - Query optimization 12 | - Índices y performance 13 | 14 | ## Temas cubiertos 15 | 16 | 1. Window Functions (ROW_NUMBER, RANK, LAG/LEAD, etc.) 17 | 2. Joins & Subqueries 18 | 3. CTEs & Aggregations 19 | 4. Query Optimization 20 | 21 | ## Recomendación de estudio 22 | 23 | Comienza con Window Functions → luego Joins → después Optimization. 24 | -------------------------------------------------------------------------------- /data-modeling/README.md: -------------------------------------------------------------------------------- 1 | # Data Modeling para Data Engineering 2 | 3 | ## ¿Por qué Data Modeling? 4 | 5 | Data Modeling = cómo estructuras datos para que queries sean rápidas + mantenibles. No es "crear tablas random", es diseño deliberado. 6 | 7 | ## Temas Cubiertos 8 | 9 | 1. **Star Schema & Dimensional Modeling** — Fact vs Dimension, conformed dimensions 10 | 2. **Slowly Changing Dimensions (SCD)** — Types 1, 2, 3, 4 (track history) 11 | 3. **Normalization vs Denormalization** — Trade-offs, cuándo cada uno 12 | 4. **Fact Tables Deep Dive** — Grain, additivity, conformed facts 13 | 14 | ## Recomendación de Estudio 15 | 16 | **Para Mid devs:** 17 | 18 | - Entender el concepto Star Schema 19 | - Conoce cuándo desnormalizar 20 | 21 | **For Seniors:** 22 | 23 | - Domina SCD patterns 24 | - Conoce los costos y trade-offs 25 | - Decisiones reales de modelado 26 | -------------------------------------------------------------------------------- /cloud/README.md: -------------------------------------------------------------------------------- 1 | # Cloud Platforms para Data Engineering 2 | 3 | ## ¿Por qué Cloud? 4 | 5 | Cloud no es opcional. Las 3 plataformas principales: 6 | 7 | - **AWS**: Market leader, mayor ecosistema 8 | - **GCP**: Fuerte en analytics, en DWH (BigQuery), ML (Vertex AI) 9 | - **Azure**: Enterprise (Microsoft integration) 10 | 11 | ## Temas Cubiertos 12 | 13 | 1. **AWS Data Stack** — S3, EC2, EMR, Lambda, Redshift, Glue 14 | 2. **GCP Data Stack** — BigQuery, Dataflow, Cloud Storage, Pub/Sub 15 | 3. **Azure Data Stack** — Data Factory, Synapse, Blob Storage, Stream Analytics 16 | 4. **Multi-Cloud Strategy** — When to use each, trade-offs 17 | 18 | ## Recomendación de Estudio 19 | 20 | **Para desarrolladores de nivel intermedio (Mid devs):** 21 | 22 | - Aprende AWS (la más común) 23 | - Entiende los trade-offs vs GCP/Azure 24 | 25 | **Para Seniors:** 26 | 27 | - Domina a fondo una plataforma 28 | - Entiende los trade-offs entre las 3 29 | - Sabe cuándo hacer migrate/switch 30 | -------------------------------------------------------------------------------- /streaming/README.md: -------------------------------------------------------------------------------- 1 | # Streaming & Real-Time Data Processing 2 | 3 | ## ¿Por qué Streaming? 4 | 5 | Batch = "process all data at once, daily". 6 | Streaming = "process data as it arrives, seconds latency". 7 | En sistemas modernos: orders, clicks, payments → se necesitan real-time insights (fraud detection, dashboards, alerts). 8 | 9 | ## Temas cubiertos 10 | 11 | 1. **Kafka Architecture & Patterns** — topics, partitions, consumer groups, offsets 12 | 2. **Stream Processing** — windowing, state management, joins 13 | 3. **Real-Time vs Batch Trade-offs** — Lambda vs Kappa architectures 14 | 4. **Streaming Frameworks** — Kafka Streams, Spark Streaming basics 15 | 16 | ## Recomendación de estudio 17 | 18 | **Para mid devs:** 19 | 20 | - Entender Kafka basics (topics, producers, consumers) 21 | - Saber cuándo usar batch vs streaming 22 | 23 | **For seniors:** 24 | 25 | - Domina consumer groups + offset management 26 | - Optimiza windowing strategies 27 | - Diseña fault-tolerant streaming pipelines 28 | -------------------------------------------------------------------------------- /orchestration/README.md: -------------------------------------------------------------------------------- 1 | # Pipeline Orchestration para Data Engineering 2 | 3 | ## ¿Por qué Orchestration? 4 | 5 | Data pipelines = múltiples tasks (extract, transform, load). 6 | Orchestration = scheduling + dependency management + error handling + monitoring. 7 | 8 | Sin orchestration: manual scheduling (cron) = frágil. 9 | Con orchestration: automated, resilient, observable. 10 | 11 | ## Temas cubiertos 12 | 13 | 1. **Apache Airflow** — DAGs, tasks, scheduling, error handling 14 | 2. **dbt (data build tool)** — SQL transformations, testing, lineage 15 | 3. **Workflow Patterns** — SLAs, backfills, retry strategies 16 | 4. **Monitoring & Alerting** — task failures, SLA violations 17 | 18 | ## Recomendación de estudio 19 | 20 | **Para mid devs:** 21 | 22 | - Entender DAGs + tasks en Airflow 23 | - Saber cuándo usar dbt vs custom SQL 24 | 25 | **Para seniors:** 26 | 27 | - Dominar error handling + retry strategies 28 | - Optimizar task dependency graphs 29 | - Diseñar para scalability + maintainability 30 | -------------------------------------------------------------------------------- /python-dsa/README.md: -------------------------------------------------------------------------------- 1 | # Python & DSA para Data Engineers 2 | 3 | ## ¿Por qué Python/DSA? 4 | 5 | Data engineers escriben código. DSA no es "LeetCode obsession", sino: 6 | 7 | - **Strings**: Parsing de datos, regex 8 | - **Arrays/Lists**: Manipulación de colecciones 9 | - **Generators**: Memory efficiency (crucial en big data) 10 | - **Decorators**: Clean code, logging, caching 11 | - **Context Managers**: Resource management 12 | 13 | ## Temas cubiertos 14 | 15 | 1. **Strings & Text Processing** — parsing, regex, cleaning 16 | 2. **Arrays & Collections** — lists, tuples, sets, dicts 17 | 3. **Generators & Iterators** — lazy evaluation, memory 18 | 4. **Decorators & Context Managers** — code quality 19 | 5. **Lambda & Functional Programming** — conciseness 20 | 21 | ## Recomendación de estudio 22 | 23 | **Para principiantes:** 24 | 25 | - Empieza con strings 26 | - Luego arrays 27 | - Generators es concepto importante 28 | 29 | **Para seniors:** 30 | 31 | - Review para refresh 32 | - Enfócate en decorators/context managers 33 | - Optimization patterns 34 | -------------------------------------------------------------------------------- /pyspark/README.md: -------------------------------------------------------------------------------- 1 | # PySpark en Entrevistas de Data Engineering 2 | 3 | ## ¿Por qué PySpark? 4 | 5 | PySpark es el **framework más usado** en data engineering moderno. Esperan que entiendas: 6 | 7 | - **RDD vs DataFrame**: Cuándo usar cada uno 8 | - **Transformations & Actions**: Lazy evaluation 9 | - **Optimization & Caching**: Performance tuning 10 | - **Partitioning & Shuffling**: Escalabilidad 11 | - **Window Functions & Aggregations**: Análisis complejos 12 | 13 | ## Temas cubiertos 14 | 15 | 1. **RDD vs DataFrame** — Diferencias, ventajas/desventajas 16 | 2. **Transformations & Actions** — map, filter, reduce, collect, etc. 17 | 3. **Optimization & Caching** — persistence, broadcasting, bucketing 18 | 4. **Performance Tuning** — parallelism, partitioning, shuffle optimization 19 | 5. **Window Functions en Spark** — similar a SQL pero en código 20 | 6. **Data Quality en Spark** — NULL handling, duplicates, validation 21 | 22 | ## Recomendación de estudio 23 | 24 | Empieza con **RDD vs DataFrame** → luego **Transformations** → después **Optimization**. 25 | 26 | Después que entiendas estos, **PySpark SQL** te parecerá más fácil. 27 | -------------------------------------------------------------------------------- /system-design/README.md: -------------------------------------------------------------------------------- 1 | # System Design en Data Engineering 2 | 3 | ## ¿Por qué System Design? 4 | 5 | System Design es lo que diferencia Mid de Senior. No es solo "escribir queries", es "diseñar sistemas" que escalen. 6 | 7 | ## Temas cubiertos 8 | 9 | 1. **ETL Pipeline Architecture** — batch vs real-time 10 | 2. **Data Warehouse Design** — schemas, modeling 11 | 3. **Stream Processing** — Kafka, event-driven 12 | 4. **Data Quality at Scale** — monitoring, alerting 13 | 5. **Optimization & Performance** — bottlenecks, trade-offs 14 | 6. **Infrastructure** — cloud, on-prem, hybrid 15 | 16 | ## Estructura de cada problema 17 | 18 | - Problema real (no académico) 19 | - Constraints (datos, latencia, presupuesto) 20 | - Arquitectura propuesta 21 | - Trade-offs (CAP, consistency, availability) 22 | - Alternativas consideradas 23 | - Scaling strategy 24 | 25 | ## Recomendación de estudio 26 | 27 | **Para mid devs:** 28 | 29 | - Empieza con ETL architecture 30 | - Entiende batch vs real-time 31 | - Aprende trade-offs 32 | 33 | **Para seniors:** 34 | 35 | - Domina todas las preguntas 36 | - Piensa en edge cases 37 | - Sabe por qué elegir cada componente 38 | - Puede defender decisiones 39 | -------------------------------------------------------------------------------- /sql/01-window-functions.md: -------------------------------------------------------------------------------- 1 | # Funciones de Ventana en SQL 2 | 3 | **Tags**: #sql #window-functions #intermediate 4 | 5 | --- 6 | 7 | ## TL;DR (Respuesta en 30 segundos) 8 | 9 | Las funciones de ventana (window functions) permiten calcular valores sobre un conjunto de filas relacionadas sin hacer colapsar los resultados en grupos. Usan `OVER()` para definir la ventana. 10 | 11 | --- 12 | 13 | ## Concepto Core 14 | 15 | - **Qué es**: Una función de ventana calcula un valor para cada fila basándose en un conjunto de filas definido por la cláusula `OVER()`. 16 | 17 | - **Por qué importa**: Es fundamental en data warehousing. Permite cálculos complejos (rankings, running totals) sin perder el nivel de detalle de la fila. 18 | 19 | - **Principio clave**: Window functions = agregaciones sin GROUP BY (mantienes las filas originales) 20 | 21 | --- 22 | 23 | ## Memory Trick 24 | 25 | **"OVER es la clave"** — Sin `OVER()`, es una función agregada normal. Con `OVER()`, es una window function. 26 | 27 | Ejemplo mental: 28 | 29 | - `SUM(salary)` → Te da 1 número 30 | - `SUM(salary) OVER()` → Te da el suma total EN CADA FILA 31 | 32 | --- 33 | 34 | ## Cómo explicarlo en entrevista 35 | 36 | **Paso 1: Define la función** 37 | "Una window function me permite calcular valores que se necesitan preservando las filas individuales." 38 | 39 | **Paso 2: Explica OVER()** 40 | "La cláusula OVER() define qué filas participan en el cálculo: 41 | 42 | - `PARTITION BY`: Agrupa lógicamente (como GROUP BY pero sin colapsar) 43 | - `ORDER BY`: Define el orden (importante para RANK, LAG, etc.)" 44 | 45 | **Paso 3: Da un ejemplo real** 46 | "Si necesito el ranking de salarios POR DEPARTAMENTO, uso: 47 | `ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC)`" 48 | 49 | --- 50 | 51 | ## Código/Query ejemplo 52 | 53 | ```sql 54 | -- Pregunta: "Encuentra el salario más alto por departamento 55 | -- pero mantén todas las filas" 56 | 57 | SELECT 58 | employee_id, 59 | name, 60 | department, 61 | salary, 62 | MAX(salary) OVER (PARTITION BY department) AS dept_max_salary, 63 | ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rank_in_dept 64 | FROM employees 65 | ORDER BY department, salary DESC; 66 | ``` 67 | 68 | **Resultado:** 69 | | employee_id | name | department | salary | dept_max | rank | 70 | |---|---|---|---|---|---| 71 | | 1 | Alice | Sales | 50000 | 60000 | 2 | 72 | | 2 | Bob | Sales | 60000 | 60000 | 1 | 73 | | 3 | Charlie| IT | 80000 | 80000 | 1 | 74 | 75 | --- 76 | 77 | ## Errores comunes en entrevista 78 | 79 | - **Error**: Usar `GROUP BY` cuando necesitas mantener filas → **Solución**: Usa window functions con `OVER()` 80 | 81 | - **Error**: Olvidar `PARTITION BY` cuando necesitas segregar datos → **Solución**: `PARTITION BY` es como GROUP BY dentro de window functions 82 | 83 | - **Error**: Confundir `ROW_NUMBER()`, `RANK()` y `DENSE_RANK()` → **Solución**: Memóriza: ROW_NUMBER es secuencial, RANK salta números, DENSE_RANK no salta 84 | 85 | --- 86 | 87 | ## Preguntas de seguimiento típicas 88 | 89 | 1. **"¿Cuál es la diferencia entre `ROW_NUMBER()`, `RANK()` y `DENSE_RANK()`?"** 90 | - ROW_NUMBER: 1, 2, 3, 4 91 | - RANK: 1, 2, 2, 4 (salta después de empate) 92 | - DENSE_RANK: 1, 2, 2, 3 (no salta) 93 | 94 | 2. **"¿Cómo optimizarías un query con múltiples window functions?"** 95 | - Usa una sola ventana cuando sea posible 96 | - Cuidado con la complejidad O(n log n) 97 | 98 | 3. **"¿Cuándo usarías `LAG()` o `LEAD()`?"** 99 | - LAG: Acceder a fila anterior 100 | - LEAD: Acceder a fila siguiente 101 | - Caso de uso: Calcular diferencia mes-a-mes 102 | 103 | --- 104 | 105 | ## Referencias 106 | 107 | - [Window Functions en SQL - Documentación PostgreSQL](https://www.postgresql.org/docs/current/tutorial-window.html) 108 | - [SQL Window Functions - Mode Analytics](https://mode.com/sql-tutorial/sql-window-functions/) 109 | -------------------------------------------------------------------------------- /sql/02-second-highest-salary-per-department.md: -------------------------------------------------------------------------------- 1 | # Segundo Salario Más Alto por Departamento 2 | 3 | **Tags**: #sql #window-functions #ranking #real-interview 4 | 5 | --- 6 | 7 | ## TL;DR 8 | 9 | Usa `DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC)` para rankear salarios por departamento, luego filtra donde rank = 2. Si no hay segundo salario, la fila no aparece. 10 | 11 | --- 12 | 13 | ## Concepto Core 14 | 15 | - **Qué es**: Necesitas encontrar el segundo valor más alto dentro de cada grupo (departamento) 16 | - **Por qué importa**: Es un patrón muy común en entrevistas: "top N por grupo". Demuestra que entiendes window functions y `PARTITION BY` 17 | - **Principio clave**: `DENSE_RANK` no salta números en empates, ideal cuando múltiples personas tienen el mismo salario 18 | 19 | --- 20 | 21 | ## Memory Trick 22 | 23 | **"Ranking con burbujas departamentales"** — Cada departamento es una burbuja. `DENSE_RANK` rankea dentro de cada burbuja sin saltar números incluso con empates. 24 | 25 | --- 26 | 27 | ## Cómo explicarlo en entrevista 28 | 29 | **Paso 1**: "Necesito rankear salarios en orden descendente dentro de cada departamento" 30 | 31 | **Paso 2**: "Uso `DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC)` para crear rangos. Si dos personas en Ventas ganan 50k, ambas son rank 1, la siguiente es rank 2" 32 | 33 | **Paso 3**: "Filtro donde rank = 2 para obtener el segundo salario. Si no hay segundo salario (solo 1 persona), esa fila no aparece" 34 | 35 | --- 36 | 37 | ## Código/Query ejemplo 38 | 39 | ```sql 40 | -- Opción 1: Con DENSE_RANK (recomendado) 41 | WITH ranked_salaries AS ( 42 | SELECT 43 | employee_id, 44 | employee_name, 45 | department, 46 | salary, 47 | DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) as rank 48 | FROM employees 49 | ) 50 | SELECT 51 | employee_id, 52 | employee_name, 53 | department, 54 | salary 55 | FROM ranked_salaries 56 | WHERE rank = 2 57 | ORDER BY department, salary DESC; 58 | ``` 59 | 60 | **Resultado esperado:** 61 | | employee_id | employee_name | department | salary | 62 | |---|---|---|---| 63 | | 2 | Bob | Sales | 45000 | 64 | | 4 | David | IT | 75000 | 65 | 66 | --- 67 | 68 | ## Variante: ¿Qué pasa con empates? 69 | 70 | Si dos personas tienen el mismo segundo salario: 71 | 72 | ```sql 73 | -- DENSE_RANK muestra ambos 74 | department | salary | rank 75 | Sales | 45000 | 2 76 | Sales | 45000 | 2 ← ambos aparecen 77 | 78 | -- ROW_NUMBER solo muestra uno 79 | department | salary | rank 80 | Sales | 45000 | 2 81 | Sales | 40000 | 3 ← salta a 3 82 | ``` 83 | 84 | --- 85 | 86 | ## Errores comunes en entrevista 87 | 88 | - **Error**: Usar `RANK()` en lugar de `DENSE_RANK()` → **Solución**: `RANK` salta números (1, 2, 2, 4), `DENSE_RANK` no (1, 2, 2, 3). Para "segundo salario" usa `DENSE_RANK` 89 | 90 | - **Error**: Olvidar el `WHERE rank = 2` y devolver todos los rangos → **Solución**: Siempre filtra específicamente el rank que necesitas 91 | 92 | - **Error**: Usar `GROUP BY` con `MAX(salary)` dos veces en lugar de window functions → **Solución**: `GROUP BY` colapsaría el resultado. Window functions preservan cada fila 93 | 94 | --- 95 | 96 | ## Preguntas de seguimiento típicas 97 | 98 | 1. **"¿Y si hay un empate en el segundo salario, qué haces?"** 99 | - "Con `DENSE_RANK`, ambos aparecen. Si necesitas solo uno, cambio a `ROW_NUMBER`" 100 | 101 | 2. **"¿Cómo lo optimizarías para 10 millones de filas?"** 102 | - "Aseguro que hay índice en `(department, salary DESC)`. Spark: repartición por departamento antes de la window function" 103 | 104 | 3. **"¿Qué pasa si un departamento solo tiene 1 empleado?"** 105 | - "Esa fila NO aparecerá en el resultado porque no hay rank = 2. Si necesitas mostrarlos, uso `COALESCE` o `LEFT JOIN`" 106 | 107 | 4. **"¿Diferencia entre RANK y DENSE_RANK?"** 108 | - "RANK: 1, 2, 2, 4 (salta). DENSE_RANK: 1, 2, 2, 3 (no salta). Para 'segundo' uso `DENSE_RANK`" 109 | 110 | --- 111 | 112 | ## Código alternativo (Sin CTE, directo) 113 | 114 | ```sql 115 | SELECT 116 | employee_id, 117 | employee_name, 118 | department, 119 | salary 120 | FROM ( 121 | SELECT 122 | employee_id, 123 | employee_name, 124 | department, 125 | salary, 126 | DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) as rank 127 | FROM employees 128 | ) subquery 129 | WHERE rank = 2 130 | ORDER BY department, salary DESC; 131 | ``` 132 | 133 | ⚠️ **Nota**: Algunos motores no permiten `WHERE` directo sobre window functions. Por eso la solución con CTE es más robusta. 134 | 135 | --- 136 | 137 | ## Referencias 138 | 139 | - [Window Functions - PostgreSQL Official Docs](https://www.postgresql.org/docs/current/tutorial-window.html) 140 | -------------------------------------------------------------------------------- /sql/03-pivot-months-as-columns.md: -------------------------------------------------------------------------------- 1 | # Pivotar Datos: Convertir Meses en Columnas 2 | 3 | **Tags**: #sql #pivot #data-transformation #real-interview 4 | 5 | --- 6 | 7 | ## TL;DR 8 | 9 | Usa `CASE WHEN` con `SUM` para pivotar: agrupa por cliente, luego usa CASE para crear columnas por mes. Alternativa: `PIVOT` (si tu BD lo soporta). 10 | 11 | --- 12 | 13 | ## Concepto Core 14 | 15 | - **Qué es**: Transformar filas (meses como valores) en columnas (enero, febrero, etc.) 16 | - **Por qué importa**: Muy común en reportes BI y data warehousing. Demuestra control sobre transformaciones complejas 17 | - **Principio clave**: Combina `GROUP BY` + `CASE WHEN` para simular pivot, o usa `PIVOT` si disponible 18 | 19 | --- 20 | 21 | ## Memory Trick 22 | 23 | **"De vertical a horizontal"** — Los datos están "verticales" (meses como filas). `PIVOT` los gira "horizontales" (meses como columnas). 24 | 25 | --- 26 | 27 | ## Cómo explicarlo en entrevista 28 | 29 | **Paso 1**: "Tengo una tabla con meses como valores en una columna. Necesito convertirlos a columnas" 30 | 31 | **Paso 2**: "Uso `GROUP BY customer_id` y dentro uso `SUM(CASE WHEN month = 'January' THEN amount END) as January`" 32 | 33 | **Paso 3**: "Repito el CASE WHEN para cada mes. El resultado tiene una fila por cliente y una columna por mes" 34 | 35 | --- 36 | 37 | ## Código/Query ejemplo 38 | 39 | **Entrada (datos "verticales"):** 40 | 41 | ``` 42 | customer_id | month | amount 43 | 1 | January | 100 44 | 1 | February | 150 45 | 1 | March | 200 46 | 2 | January | 50 47 | 2 | February | 75 48 | ``` 49 | 50 | **Salida deseada (datos "horizontales"):** 51 | 52 | ``` 53 | customer_id | January | February | March 54 | 1 | 100 | 150 | 200 55 | 2 | 50 | 75 | NULL 56 | ``` 57 | 58 | ### Solución 1: CASE WHEN (funciona en todos los motores) 59 | 60 | ```sql 61 | SELECT 62 | customer_id, 63 | SUM(CASE WHEN month = 'January' THEN amount ELSE 0 END) as January, 64 | SUM(CASE WHEN month = 'February' THEN amount ELSE 0 END) as February, 65 | SUM(CASE WHEN month = 'March' THEN amount ELSE 0 END) as March, 66 | SUM(CASE WHEN month = 'April' THEN amount ELSE 0 END) as April, 67 | SUM(CASE WHEN month = 'May' THEN amount ELSE 0 END) as May, 68 | -- ... continúa para todos los meses 69 | SUM(amount) as Total 70 | FROM sales 71 | GROUP BY customer_id 72 | ORDER BY customer_id; 73 | ``` 74 | 75 | ### Solución 2: PIVOT (SQL Server, Oracle) 76 | 77 | ```sql 78 | SELECT * 79 | FROM sales 80 | PIVOT ( 81 | SUM(amount) 82 | FOR month IN ('January', 'February', 'March', 'April', 'May') 83 | ) 84 | ORDER BY customer_id; 85 | ``` 86 | 87 | ### Solución 3: Dynamic PIVOT (Spark SQL) 88 | 89 | ```sql 90 | SELECT * 91 | FROM sales 92 | PIVOT ( 93 | SUM(amount) 94 | FOR month IN ( 95 | SELECT DISTINCT month FROM sales ORDER BY month 96 | ) 97 | ); 98 | ``` 99 | 100 | --- 101 | 102 | ## Errores comunes en entrevista 103 | 104 | - **Error**: Olvidar el `SUM()` alrededor del CASE WHEN → **Solución**: Sin SUM(), cada fila es una nueva columna. Con SUM(), agrega valores del mismo cliente-mes 105 | 106 | - **Error**: No usar `GROUP BY` → **Solución**: Sin GROUP BY, el resultado sería inutilizable. Siempre agrupa por la dimensión principal (customer_id) 107 | 108 | - **Error**: Usar `IF()` en lugar de `CASE WHEN` → **Solución**: `IF()` es más simple pero CASE WHEN es más estándar y legible 109 | 110 | --- 111 | 112 | ## Preguntas de seguimiento típicas 113 | 114 | 1. **"¿Y si hay valores NULL para algunos clientes-meses?"** 115 | - "Con `CASE WHEN` devuelve NULL. Puedo usar `COALESCE(total, 0)` si prefiero 0" 116 | 117 | 2. **"¿Cómo lo optimizarías para 12 meses y 1 millón de clientes?"** 118 | - "El query es O(n). Con índice en `(customer_id, month)` es rápido. En Spark, partición por cliente antes de pivot" 119 | 120 | 3. **"¿Diferencia entre PIVOT y CASE WHEN?"** 121 | - "PIVOT es sintaxis especial (no todos lo soportan). CASE WHEN es más universal. Resultados iguales" 122 | 123 | 4. **"¿Y si necesito pivotar 24 meses dinámicamente?"** 124 | - "En Spark SQL, puedo usar `PIVOT ... FOR month IN (SELECT DISTINCT month FROM ...)`. En SQL tradicional, necesito hardcodear" 125 | 126 | --- 127 | 128 | ## Variante: Despivotar (Lo Opuesto) 129 | 130 | A veces necesitas ir de horizontal a vertical (UNPIVOT): 131 | 132 | ```sql 133 | -- De esto (pivotado): 134 | SELECT customer_id, January, February, March FROM sales_pivot; 135 | 136 | -- A esto (despivotado): 137 | SELECT customer_id, 'January' as month, January as amount FROM sales_pivot 138 | UNION ALL 139 | SELECT customer_id, 'February' as month, February as amount FROM sales_pivot 140 | UNION ALL 141 | SELECT customer_id, 'March' as month, March as amount FROM sales_pivot; 142 | ``` 143 | 144 | O usa `UNPIVOT` si tu BD lo soporta: 145 | 146 | ```sql 147 | SELECT customer_id, month, amount 148 | FROM sales_pivot 149 | UNPIVOT ( 150 | amount FOR month IN (January, February, March) 151 | ); 152 | ``` 153 | 154 | --- 155 | 156 | ## Referencias 157 | 158 | - [PIVOT en SQL Server - Microsoft Docs](https://learn.microsoft.com/en-us/sql/t-sql/queries/from-using-pivot-and-unpivot) 159 | - [CASE WHEN en SQL - W3Schools](https://www.w3schools.com/sql/sql_case.asp) 160 | -------------------------------------------------------------------------------- /sql/04-total-outstanding-loan-liability.md: -------------------------------------------------------------------------------- 1 | # Calcular Pasivo Total de Préstamos por Cliente 2 | 3 | **Tags**: #sql #joins #aggregation #banking #real-interview 4 | 5 | --- 6 | 7 | ## TL;DR 8 | 9 | Haz `JOIN` entre `Loans` y `Accounts` usando `customer_id`, luego `SUM()` el saldo pendiente (`outstanding_balance`) agrupando por cliente. 10 | 11 | --- 12 | 13 | ## Concepto Core 14 | 15 | - **Qué es**: Necesitas consolidar datos de múltiples tablas (préstamos e información de cuentas) para un cálculo por cliente 16 | - **Por qué importa**: JOINs son fundamentales en data engineering. Demuestra que entiendes relaciones entre entidades 17 | - **Principio clave**: Usa `INNER JOIN` si ambas tablas tienen el customer, `LEFT JOIN` si algunos clientes podrían no tener préstamos 18 | 19 | --- 20 | 21 | ## Memory Trick 22 | 23 | **"Juntando información"** — Loans y Accounts tienen customer_id. Lo juntas con JOIN (como vincular dos archivos por ID) y luego agrupas. 24 | 25 | --- 26 | 27 | ## Cómo explicarlo en entrevista 28 | 29 | **Paso 1**: "Tengo dos tablas: `Loans` (con préstamos y saldos) y `Accounts` (info de cliente). Ambas tienen `customer_id`" 30 | 31 | **Paso 2**: "Las uno con `JOIN` en customer_id. Si necesito solo clientes CON préstamos, INNER JOIN. Si todos los clientes, LEFT JOIN a partir de Accounts" 32 | 33 | **Paso 3**: "Agrupo por `customer_id` y sumo `outstanding_balance`. El resultado: cada cliente con su pasivo total" 34 | 35 | --- 36 | 37 | ## Código/Query ejemplo 38 | 39 | **Tablas disponibles:** 40 | 41 | Loans: 42 | 43 | ``` 44 | loan_id | customer_id | loan_type | outstanding_balance | status 45 | 1 | 101 | Mortgage | 250000 | Active 46 | 2 | 101 | Auto | 25000 | Active 47 | 3 | 102 | Personal | 5000 | Active 48 | 4 | 103 | Mortgage | 300000 | Defaulted 49 | ``` 50 | 51 | Accounts: 52 | 53 | ``` 54 | account_id | customer_id | account_type | balance 55 | 1001 | 101 | Checking | 5000 56 | 1002 | 101 | Savings | 20000 57 | 1003 | 102 | Checking | 2000 58 | 1004 | 104 | Savings | 50000 59 | ``` 60 | 61 | ### Solución 1: INNER JOIN (solo clientes con préstamos) 62 | 63 | ```sql 64 | SELECT 65 | l.customer_id, 66 | a.account_type, 67 | COUNT(l.loan_id) as num_loans, 68 | SUM(l.outstanding_balance) as total_outstanding, 69 | SUM(a.balance) as total_account_balance, 70 | SUM(l.outstanding_balance) - SUM(a.balance) as net_liability 71 | FROM Loans l 72 | INNER JOIN Accounts a ON l.customer_id = a.customer_id 73 | GROUP BY l.customer_id, a.account_type 74 | ORDER BY total_outstanding DESC; 75 | ``` 76 | 77 | **Resultado:** 78 | 79 | ``` 80 | customer_id | account_type | num_loans | total_outstanding | total_account_balance | net_liability 81 | 101 | Checking | 2 | 275000 | 5000 | 270000 82 | 101 | Savings | 2 | 275000 | 20000 | 255000 83 | 102 | Checking | 1 | 5000 | 2000 | 3000 84 | ``` 85 | 86 | ⚠️ **Nota**: Si un cliente tiene múltiples cuentas, aparece en múltiples filas. Para consolidado, ver Solución 2. 87 | 88 | ### Solución 2: Consolidado por Cliente (más limpio) 89 | 90 | ```sql 91 | SELECT 92 | l.customer_id, 93 | COUNT(DISTINCT l.loan_id) as num_loans, 94 | SUM(l.outstanding_balance) as total_outstanding_liability, 95 | COUNT(DISTINCT a.account_id) as num_accounts, 96 | SUM(a.balance) as total_account_balance 97 | FROM Loans l 98 | INNER JOIN Accounts a ON l.customer_id = a.customer_id 99 | GROUP BY l.customer_id 100 | ORDER BY total_outstanding_liability DESC; 101 | ``` 102 | 103 | **Resultado:** 104 | 105 | ``` 106 | customer_id | num_loans | total_outstanding_liability | num_accounts | total_account_balance 107 | 101 | 2 | 275000 | 2 | 25000 108 | 102 | 1 | 5000 | 1 | 2000 109 | ``` 110 | 111 | ### Solución 3: LEFT JOIN (todos los clientes) 112 | 113 | Si quieres incluir clientes SIN préstamos: 114 | 115 | ```sql 116 | SELECT 117 | a.customer_id, 118 | COUNT(DISTINCT l.loan_id) as num_loans, 119 | COALESCE(SUM(l.outstanding_balance), 0) as total_outstanding_liability, 120 | COUNT(DISTINCT a.account_id) as num_accounts, 121 | SUM(a.balance) as total_account_balance 122 | FROM Accounts a 123 | LEFT JOIN Loans l ON a.customer_id = l.customer_id 124 | GROUP BY a.customer_id 125 | ORDER BY total_outstanding_liability DESC; 126 | ``` 127 | 128 | **Resultado incluye cliente 104 (sin préstamos):** 129 | 130 | ``` 131 | customer_id | num_loans | total_outstanding_liability | num_accounts | total_account_balance 132 | 101 | 2 | 275000 | 2 | 25000 133 | 102 | 1 | 5000 | 1 | 2000 134 | 104 | 0 | 0 | 1 | 50000 135 | ``` 136 | 137 | --- 138 | 139 | ## Errores comunes en entrevista 140 | 141 | - **Error**: Hacer CROSS JOIN en lugar de JOIN (sin ON clause) → **Solución**: Siempre especifica `ON l.customer_id = a.customer_id` 142 | 143 | - **Error**: Usar INNER JOIN cuando necesitas LEFT JOIN → **Solución**: Si clientes sin préstamos deben aparecer, usa LEFT JOIN y COALESCE 144 | 145 | - **Error**: Olvidar DISTINCT en COUNT dentro del GROUP BY → **Solución**: Si un cliente aparece múltiples veces (múltiples cuentas), COUNT sin DISTINCT da números inflados 146 | 147 | - **Error**: No usar COALESCE con LEFT JOIN → **Solución**: Con LEFT JOIN, NULL es común. Usa COALESCE(suma, 0) para evitar NULLs 148 | 149 | --- 150 | 151 | ## Preguntas de seguimiento típicas 152 | 153 | 1. **"¿Diferencia entre INNER JOIN, LEFT JOIN y FULL OUTER JOIN?"** 154 | - INNER: Solo filas que existen en ambas tablas 155 | - LEFT: Todas las filas de la izquierda, NULLs en la derecha si no coinciden 156 | - FULL OUTER: Todas las filas de ambas 157 | 158 | 2. **"¿Qué pasa si un cliente tiene 1 préstamo pero 3 cuentas?"** 159 | - Con INNER JOIN: 3 filas (1 préstamo x 3 cuentas) 160 | - Con COUNT DISTINCT: 1 fila, conteo correcto 161 | 162 | 3. **"¿Cómo optimizarías esto para 100M de clientes?"** 163 | - Índice en `Loans(customer_id, outstanding_balance)` 164 | - Índice en `Accounts(customer_id, balance)` 165 | - Spark: Repartición por customer_id antes del JOIN 166 | 167 | 4. **"¿Y si quiero incluir préstamos pagados (status = 'Closed')?"** 168 | - Agrega `WHERE l.status IN ('Active', 'Closed')` o deja la lógica de negocio abierta 169 | 170 | --- 171 | 172 | ## Alternativa: Subqueries (menos óptimo pero válido) 173 | 174 | ```sql 175 | SELECT 176 | customer_id, 177 | (SELECT SUM(outstanding_balance) FROM Loans WHERE Loans.customer_id = Accounts.customer_id) as total_outstanding 178 | FROM Accounts 179 | GROUP BY customer_id; 180 | ``` 181 | 182 | ⚠️ **Nota**: Esto es más lento porque ejecuta subquery por cada fila. Usa JOIN cuando sea posible. 183 | 184 | --- 185 | 186 | ## Contexto Bancario 187 | 188 | En banking, este query es común para: 189 | 190 | - **Risk Assessment**: ¿Cuánto debe cada cliente? 191 | - **Credit Scoring**: Cliente con alto pasivo = riesgo mayor 192 | - **Collections**: Identificar clientes con deuda importante 193 | - **Compliance**: Reportes de exposición 194 | 195 | --- 196 | 197 | ## Referencias 198 | 199 | - [SQL JOINs - W3Schools](https://www.w3schools.com/sql/sql_join.asp) 200 | - [GROUP BY y Agregaciones - PostgreSQL Docs](https://www.postgresql.org/docs/current/queries-table-expressions.html) 201 | -------------------------------------------------------------------------------- /sql/06-query-optimization-indexes.md: -------------------------------------------------------------------------------- 1 | # Optimización de Queries & Índices 2 | 3 | **Tags**: #sql #performance #optimization #indexing #senior #real-interview 4 | 5 | --- 6 | 7 | ## TL;DR 8 | 9 | Para optimizar queries lentos: (1) Analiza el EXPLAIN PLAN, (2) Agrega índices en columnas usadas en WHERE/JOIN, (3) Reescribe la lógica si es necesario, (4) Particiona datos si es posible. Los índices aceleran búsquedas pero ralentizan INSERTs/UPDATEs. 10 | 11 | --- 12 | 13 | ## Concepto Core 14 | 15 | - **Qué es**: La optimización es el arte de hacer que los queries corran más rápido sin cambiar el resultado. Los índices son estructuras que aceleran búsquedas 16 | - **Por qué importa**: En data engineering, la diferencia entre un query que corre en 5s vs 5 minutos. Crítico para producción 17 | - **Principio clave**: Usa EXPLAIN PLAN para ver qué hace la BD. Agrega índices donde se hace Sequential Scan. Trade-off: lecturas rápidas, escrituras lentas 18 | 19 | --- 20 | 21 | ## Memory Trick 22 | 23 | **"Libro vs Índice"** — Sin índice, la BD lee todas las filas (Sequential Scan) como leer un libro línea por línea. Con índice (B-tree), salta directo como un índice de libro. 24 | 25 | --- 26 | 27 | ## Cómo explicarlo en entrevista 28 | 29 | **Paso 1**: "Si un query es lento, primero corro EXPLAIN PLAN para ver qué hace la BD" 30 | 31 | **Paso 2**: "Busco Sequential Scans en columnas grandes. Eso significa que lee TODAS las filas. Ahí agrego un índice" 32 | 33 | **Paso 3**: "Reescribo: Evito `WHERE UPPER(name) = ...` (función desactiva índice). Uso `WHERE name = UPPER(...)` en la data, no en la BD" 34 | 35 | **Paso 4**: "Valido que el query ahora usa el índice (Index Scan en EXPLAIN). Chequeo tiempo antes/después" 36 | 37 | --- 38 | 39 | ## Código/Query ejemplo 40 | 41 | ### Escenario: Query Lento en Tabla de 100M de filas 42 | 43 | ```sql 44 | -- ❌ QUERY LENTO (sin índice o mal escrito) 45 | SELECT 46 | customer_id, 47 | COUNT(*) as order_count, 48 | SUM(amount) as total_spent 49 | FROM orders 50 | WHERE EXTRACT(YEAR FROM order_date) = 2024 51 | AND order_status = 'completed' 52 | AND UPPER(customer_name) LIKE '%JOHN%' 53 | GROUP BY customer_id 54 | HAVING COUNT(*) > 10 55 | ORDER BY total_spent DESC; 56 | ``` 57 | 58 | **EXPLAIN PLAN output (problema):** 59 | 60 | ``` 61 | Seq Scan on orders (cost=0.00..500000.00 rows=1000000) 62 | Filter: (EXTRACT(YEAR, order_date) = 2024) AND (order_status = 'completed') AND (UPPER(customer_name) LIKE '%JOHN%') 63 | ``` 64 | 65 | ⚠️ **Problemas**: 66 | 67 | 1. **Seq Scan**: Lee TODAS las 100M filas 68 | 2. **EXTRACT en WHERE**: Función desactiva índice en `order_date` 69 | 3. **UPPER()**: Función desactiva índice en `customer_name` 70 | 4. **LIKE '%JOHN%'**: Pattern matching lento (full table scan) 71 | 72 | --- 73 | 74 | ### Paso 1: Crear Índices 75 | 76 | ```sql 77 | -- Índice en order_date (sin función) 78 | CREATE INDEX idx_orders_order_date ON orders(order_date); 79 | 80 | -- Índice en order_status 81 | CREATE INDEX idx_orders_status ON orders(order_status); 82 | 83 | -- Índice compuesto (mejor para este query) 84 | CREATE INDEX idx_orders_composite ON orders(order_date, order_status, customer_id); 85 | 86 | -- Nota: UPPER(customer_name) no se puede indexar bien con LIKE '%...' 87 | -- Mejor solución: full-text search o denormalización 88 | ``` 89 | 90 | --- 91 | 92 | ### Paso 2: Reescribir Query (evitar funciones) 93 | 94 | ```sql 95 | -- ✅ QUERY OPTIMIZADO 96 | SELECT 97 | customer_id, 98 | COUNT(*) as order_count, 99 | SUM(amount) as total_spent 100 | FROM orders 101 | WHERE order_date >= '2024-01-01' AND order_date < '2025-01-01' -- Sin EXTRACT() 102 | AND order_status = 'completed' 103 | AND customer_name ILIKE '%john%' -- ILIKE sin UPPER() 104 | GROUP BY customer_id 105 | HAVING COUNT(*) > 10 106 | ORDER BY total_spent DESC; 107 | ``` 108 | 109 | **EXPLAIN PLAN output (mejorado):** 110 | 111 | ``` 112 | Index Scan using idx_orders_composite on orders (cost=100.00..5000.00 rows=50000) 113 | Index Cond: (order_date >= '2024-01-01' AND order_date < '2025-01-01' AND order_status = 'completed') 114 | Filter: (ILIKE '%john%') 115 | Group Aggregate (cost=5000.00..6000.00 rows=100) 116 | ``` 117 | 118 | ✅ **Mejoras**: 119 | 120 | 1. Index Scan en lugar de Seq Scan (100x más rápido) 121 | 2. Lee solo ~50k filas en lugar de 100M 122 | 3. Índice compuesto acelera el filtro 123 | 124 | **Comparación de tiempo**: 125 | 126 | - Antes: 45 segundos (Seq Scan) 127 | - Después: 0.3 segundos (Index Scan) 128 | 129 | --- 130 | 131 | ## Tipos de Índices 132 | 133 | | Tipo | Uso | Ventaja | Desventaja | 134 | | --------------- | ------------------------------- | ---------------------------- | ------------------------- | 135 | | **B-Tree** | Por defecto, rango queries | Rápido para =, <, >, BETWEEN | No help para LIKE '%...' | 136 | | **Hash** | Igualdad (=) | Super rápido | Solo = operator | 137 | | **Full-Text** | Búsqueda de texto | Soporta LIKE, palabras | Lento para datos no-texto | 138 | | **Bitmap** | Data warehouse, low cardinality | Comprimido | No para OLTP | 139 | | **Partitioned** | Tablas enormes | Elimina particiones | Complejo de mantener | 140 | 141 | --- 142 | 143 | ## Errores comunes en entrevista 144 | 145 | - **Error**: Crear índice en cada columna → **Solución**: Los índices ralentizan INSERTs. Crea solo donde se necesita (WHERE, JOIN, ORDER BY) 146 | 147 | - **Error**: No usar EXPLAIN PLAN antes de optimizar → **Solución**: Siempre EXPLAIN primero. A veces el problema no es índices 148 | 149 | - **Error**: Usar función en WHERE (EXTRACT, UPPER, etc.) → **Solución**: Evita funciones en columnas indexadas 150 | 151 | - **Error**: Índice compuesto en orden incorrecto → **Solución**: El orden importa. `(col1, col2)` ≠ `(col2, col1)` para queries 152 | 153 | --- 154 | 155 | ## Preguntas de seguimiento típicas 156 | 157 | 1. **"¿Diferencia entre UNIQUE index y PRIMARY KEY?"** 158 | - PRIMARY KEY: Unique + NOT NULL + tabla solo tiene 1 159 | - UNIQUE index: Solo enforces uniqueness, puede haber NULLs 160 | 161 | 2. **"¿Cuándo NO deberías usar índices?"** 162 | - Columnas con baja selectividad (95% de filas = true) 163 | - Tablas muy pequeñas (<1M filas) 164 | - Columnas actualizadas frecuentemente (índice ralentiza UPDATE) 165 | 166 | 3. **"¿Cómo debuggearías un query lento?"** 167 | - EXPLAIN PLAN 168 | - ANALYZE (actualiza stats) 169 | - Chequea índices existentes 170 | - Mira si hay missing indexes 171 | 172 | 4. **"¿Spark vs PostgreSQL: ¿Cómo optimizas en Spark?"** 173 | - Spark: No hay índices. Usa **partitioning** y **bucketing** 174 | - Particiona por columna usada en WHERE 175 | - Bucketing para join keys 176 | 177 | --- 178 | 179 | ## Real-World: Data Lake Optimization 180 | 181 | En Spark/Data Lake (sin índices): 182 | 183 | ```sql 184 | -- ❌ SLOW: Sin partición, lee TODO 185 | SELECT * FROM sales_data WHERE country = 'US' AND year = 2024; 186 | 187 | -- ✅ FAST: Datos particionados por country/year 188 | CREATE TABLE sales_data ( 189 | order_id INT, 190 | amount DECIMAL, 191 | country STRING, 192 | year INT 193 | ) 194 | PARTITIONED BY (country, year); 195 | 196 | -- Mismo query, pero Spark solo lee partición US/2024 197 | SELECT * FROM sales_data WHERE country = 'US' AND year = 2024; 198 | ``` 199 | 200 | --- 201 | 202 | ## Checklist para Optimizar Query Lento 203 | 204 | 1. ✅ Correr EXPLAIN PLAN 205 | 2. ✅ Identificar Seq Scans 206 | 3. ✅ Chequear si hay índices en esas columnas 207 | 4. ✅ Si no, crear índice compuesto (si es posible) 208 | 5. ✅ Reescribir query para evitar funciones en WHERE 209 | 6. ✅ Validar con EXPLAN nuevamente 210 | 7. ✅ Comparar tiempo antes/después 211 | 212 | --- 213 | 214 | ## Referencias 215 | 216 | - [EXPLAIN PLAN - PostgreSQL Docs](https://www.postgresql.org/docs/current/sql-explain.html) 217 | - [Index Types - Use The Index Luke](https://use-the-index-luke.com/) 218 | - [Spark Partitioning & Bucketing](https://spark.apache.org/docs/latest/sql-data-sources-parquet.html) 219 | -------------------------------------------------------------------------------- /cloud/04-multi-cloud-strategy.md: -------------------------------------------------------------------------------- 1 | # Multi-Cloud Strategy & Trade-offs 2 | 3 | **Tags**: #cloud #strategy #aws-vs-gcp-vs-azure #trade-offs #architecture #real-interview 4 | 5 | --- 6 | 7 | ## TL;DR 8 | 9 | Multi-cloud = utilizar múltiples providers. Razones: evitar vendor lock-in, poder de negociación, cobertura regional, best-of-breed services. Trade-off: complejidad, operational overhead, costos de data movement. Recomendación: elige 1 primary (AWS o GCP o Azure) y agrega un 2nd solo por necesidad específica. Evita 3+ clouds. 10 | 11 | --- 12 | 13 | ## Concepto Core 14 | 15 | - Qué es: Utilizar más de un cloud provider dentro de la misma arquitectura/producto. 16 | - Por qué importa: Es una decisión estratégica que impacta costo, complejidad, seguridad, contratación y operaciones por años. 17 | - Principio clave: El costo de abstracción y operación suele ser mayor que el costo de vendor lock-in para la mayoría de compañías. 18 | 19 | --- 20 | 21 | ## Cuándo Multi-Cloud 22 | 23 | ### ✅ Razones VÁLIDAS 24 | 25 | - Vendor lock-in avoidance 26 | - “Portabilidad” real cuesta tiempo y dinero; define hasta dónde llegarás (APIs, datos, IaC). 27 | - Muy pocas empresas logran true portability sin sacrificar velocity. 28 | 29 | - Best-of-breed services 30 | - Ejemplo: BigQuery (GCP) para analytics vs Redshift (AWS); Vertex AI vs SageMaker según caso. 31 | - Beneficio técnico puede compensar si el diferencial es grande y medible. 32 | 33 | - Regional coverage y compliance 34 | - Un proveedor sin región/regulación requerida; otro sí. 35 | - Data residency/regulatory constraints justifican dos nubes. 36 | 37 | - Negotiating power 38 | - Spend significativo permite mejores descuentos con competencia explícita. 39 | - Tiene sentido con > contratos de alto volumen. 40 | 41 | - Risk mitigation extremo 42 | - Outages críticos; aun así, multi-region/multi-AZ single-cloud suele bastar. 43 | - Multi-cloud DR sólo si el SLA lo exige. 44 | 45 | ### ❌ Razones INVÁLIDAS 46 | 47 | - “Cada equipo usa lo que prefiere” 48 | - Sin estándares = caos, duplicación, costos y riesgo operacional. 49 | 50 | - “Portabilidad por si acaso” 51 | - Coste de abstracción > valor en 99% de casos; optimiza para hoy y migra si negocio lo exige. 52 | 53 | - “Future-proofing” genérico 54 | - El stack cambia rápido; decide con métricas actuales y plan realista de cambio. 55 | 56 | --- 57 | 58 | ## Real-World: Multi-Cloud Companies 59 | 60 | ### Caso A: AWS + GCP (Best-of-breed) 61 | 62 | - Primary (AWS): EC2, S3, DynamoDB, Lambda. 63 | - Secondary (GCP): BigQuery (warehouse), Dataflow (pipelines), Vertex AI (serving). 64 | - Data flow: Bookings (AWS) → ETL (Glue) → S3 → carga a BigQuery (GCP) → ML (Vertex) → predicciones usadas por Lambda (AWS). 65 | - Implicaciones: 66 | - Data transfer inter-cloud duele en volumen. 67 | - Orchestration multi-cloud con Airflow o equivalentes. 68 | - Ahorros en warehouse pueden perderse por egress y overhead de ingeniería. 69 | 70 | ### Caso B: AWS + Azure (Enterprise) 71 | 72 | - Primary (AWS): cargas nuevas analytics/data. 73 | - Secondary (Azure): identidad (Azure AD), SQL Server, Synapse para apps corporativas. 74 | - Motivo: Licencias existentes y ecosistema Microsoft; transición híbrida por etapas. 75 | - Patrón común: No “multi-cloud” por gusto, sino por herencia y contratos. 76 | 77 | --- 78 | 79 | ## Multi-Cloud Architecture Patterns 80 | 81 | ### Pattern 1: Primary + Secondary (best-of-breed) 82 | 83 | - Primary: AWS (todo excepto warehouse). 84 | - Secondary: GCP (BigQuery). 85 | - Flujo: S3 (data lake) → Glue/EMR → S3 processed → carga programada a BigQuery → dashboards (Looker/Power BI). 86 | - Trade-offs: 87 | - - Mejor warehouse/SQL experience. 88 | - − Costos de egress, complejidad de gobernanza/linaje y duplicación de datos. 89 | 90 | ### Pattern 2: Geographic Distribution 91 | 92 | - Proveedor por región en función de cobertura/precio. 93 | - Contras: sincronización entre regiones/proveedores es costosa y compleja. 94 | - Alternativa: una nube, multi-region/multi-AZ, con políticas de residencia. 95 | 96 | ### Pattern 3: Fallback/Disaster Recovery 97 | 98 | - Primario en AWS, failover en GCP. 99 | - Replicación continua y playbooks de conmutación. 100 | - Cost: alto; normalmente innecesario si multi-region single-cloud cubre SLA. 101 | 102 | --- 103 | 104 | ## Cost Comparison: Single vs Multi-Cloud (ejemplo) 105 | 106 | - Single-cloud (AWS Redshift on-demand): pagas compute dedicado y minimizas egress. 107 | - Multi-cloud (AWS + GCP con BigQuery): 108 | - - Pay-per-query y autoscaling en BigQuery. 109 | - − Egress inter-cloud, más personal experto, observabilidad y seguridad duplicadas. 110 | - Conclusión: Sólo compensa si el diferencial técnico/financiero es muy grande y medible. 111 | 112 | --- 113 | 114 | ## When to Choose Each Cloud 115 | 116 | ### Elige AWS si: 117 | 118 | - Ecosistema más amplio, cobertura global, enterprise establecida. 119 | - Necesitas variedad de servicios y opciones de compute. 120 | 121 | ### Elige GCP si: 122 | 123 | - Analytics-first, SQL-first; BigQuery + BQML aceleran mucho. 124 | - ML/AI fuerte con Vertex AI; equipos data science maduros. 125 | 126 | ### Elige Azure si: 127 | 128 | - Microsoft stack, Active Directory, Power BI, SQL Server. 129 | - Compliance/governance con Purview y controles enterprise. 130 | 131 | ### Evita Multi-Cloud salvo que: 132 | 133 | - Gastes mucho y puedas negociar fuerte. 134 | - Haya un gap crítico de servicio. 135 | - Existan requisitos regulatorios/geográficos estrictos. 136 | - SLA de disponibilidad extrema lo exija. 137 | 138 | --- 139 | 140 | ## Multi-Cloud Anti-Patterns 141 | 142 | ### Anti-Pattern 1: “Que cada equipo elija” 143 | 144 | - Resultado: 3 nubes, 3 IaC, 3 políticas, 3 toolchains. 145 | - Solución: Estándares y “paved roads”; 1 primaria, 1 secundaria si aplica. 146 | 147 | ### Anti-Pattern 2: “Abstraer todo” 148 | 149 | - Kubernetes/terraform no abstraen data plane ni servicios administrados. 150 | - Overhead ≥ 20% y pérdida de ventajas nativas. 151 | - Solución: Abstrae lo mínimo y acepta lock-in táctico. 152 | 153 | ### Anti-Pattern 3: “Migramos después” 154 | 155 | - Después de 2–3 años hay 100+ integraciones; migrar es carísimo. 156 | - Solución: Decide bien al inicio; migra solo con drivers de negocio claros. 157 | 158 | --- 159 | 160 | ## Decision Framework 161 | 162 | ``` 163 | ¿Multi-cloud necesario? 164 | ├─ Sí → ¿Existe gap de servicio medible? 165 | │ ├─ Sí → Primaria: la más alineada al core; Secundaria: best-of-breed. 166 | │ │ Calcula costo de egress + ops; define SLOs y KPIs. 167 | │ └─ No → Single-cloud gana. 168 | └─ No → ¿Cuál alinea mejor con el workload? 169 | ├─ Analytics → GCP (BigQuery) 170 | ├─ Enterprise generalista → AWS/Azure 171 | ├─ ML-heavy → GCP/AWS 172 | └─ Microsoft stack → Azure 173 | ``` 174 | 175 | --- 176 | 177 | ## Errores Comunes en Entrevista 178 | 179 | - “Multi-cloud siempre es mejor” → Complejidad y costo suelen superar beneficios. 180 | - “La portabilidad importa más que la velocidad” → Lock-in táctico es aceptable si acelera valor. 181 | - Subestimar data transfer → Inter-cloud egress puede destruir el business case. 182 | - No contar el ops overhead → Se requiere observabilidad, seguridad, FinOps y skillsets duplicados. 183 | 184 | --- 185 | 186 | ## Preguntas de Seguimiento 187 | 188 | 1. ¿AWS vs GCP vs Azure para tu empresa? 189 | 190 | - Depende de workload, stack actual y presupuesto; normalmente 1 primaria, 1 secundaria si hay gap claro. 191 | 192 | 2. ¿Cuándo migrar de cloud? 193 | 194 | - Raro; sólo si cambian drivers de negocio o hay ahorro/beneficio técnico contundente. 195 | 196 | 3. ¿Hybrid vs multi-cloud? 197 | 198 | - Hybrid = on-prem + 1 cloud (más común y manejable). 199 | - Multi-cloud = 2+ clouds (útil en casos específicos). 200 | 201 | 4. ¿Kubernetes ayuda multi-cloud? 202 | 203 | - Compute: sí, parcialmente. 204 | - Data/servicios gestionados: no; sigue habiendo lock-in significativo. 205 | 206 | --- 207 | 208 | ## Referencias 209 | 210 | - [Multi-Cloud Strategy, Architecture, Benefits, Challenges, Solutions](https://www.wildnetedge.com/blogs/multi-cloud-strategy-architecture-benefits-challenges-solutions) 211 | - [Multi-Cloud Strategies Business 2025](https://www.growin.com/blog/multi-cloud-strategies-business-2025/) 212 | - [Multi-Cloud Strategies: The 2025-2026 Primer](https://www.itconvergence.com/blog/multi-cloud-strategies-the-2025-2026-primer/) 213 | - [Multi-Cloud Adoption Strategies 2025](https://arkentechpublishing.com/multi-cloud-adoption-strategies-2025/) 214 | -------------------------------------------------------------------------------- /sql/05-cte-common-table-expressions.md: -------------------------------------------------------------------------------- 1 | # CTEs (Common Table Expressions) - WITH Clause 2 | 3 | **Tags**: #sql #cte #readability #best-practices #real-interview 4 | 5 | --- 6 | 7 | ## TL;DR 8 | 9 | Un **CTE (WITH clause)** es una tabla temporal dentro del query. Te permite escribir código limpio y legible dividiendo queries complejas en partes reutilizables. Sintaxis: `WITH cte_name AS (SELECT ...) SELECT ... FROM cte_name`. 10 | 11 | --- 12 | 13 | ## Concepto Core 14 | 15 | - **Qué es**: Una tabla temporal que existe solo durante la ejecución del query. La defines con `WITH nombre AS (...)` y luego la usas como si fuera una tabla real 16 | - **Por qué importa**: Hace queries complejos legibles. Es mejor que subqueries anidadas. Permite reutilizar lógica. Fundamental en data engineering 17 | - **Principio clave**: CTEs se evalúan una sola vez (si es no-recursive). Son como "variables temporales" en SQL 18 | 19 | --- 20 | 21 | ## Memory Trick 22 | 23 | **"Tabla prestada"** — WITH te deja crear una tabla temporal que existe solo mientras el query corre. Usas como tabla normal, pero luego desaparece. 24 | 25 | --- 26 | 27 | ## Cómo explicarlo en entrevista 28 | 29 | **Paso 1**: "Para queries complejos, en lugar de anidar subqueries (difícil de leer), uso CTEs" 30 | 31 | **Paso 2**: "Defino la CTE con `WITH nombre AS (SELECT ...)`. Dentro va la lógica compleja" 32 | 33 | **Paso 3**: "Luego en el SELECT principal, uso la CTE como si fuera una tabla: `SELECT ... FROM nombre`. Mucho más limpio" 34 | 35 | --- 36 | 37 | ## Código/Query ejemplo 38 | 39 | ### Problema: Query Anidado (Difícil de leer) 40 | 41 | ```sql 42 | SELECT 43 | customer_id, 44 | (SELECT AVG(order_amount) FROM orders o2 WHERE o2.customer_id = o1.customer_id) as avg_order, 45 | (SELECT COUNT(*) FROM orders o3 WHERE o3.customer_id = o1.customer_id AND o3.status = 'completed') as completed_orders 46 | FROM orders o1 47 | WHERE customer_id IN ( 48 | SELECT customer_id FROM orders 49 | GROUP BY customer_id 50 | HAVING SUM(order_amount) > 1000 51 | ) 52 | GROUP BY customer_id; 53 | ``` 54 | 55 | ❌ **Problema**: Anidado, difícil de seguir, subqueries ejecutadas múltiples veces 56 | 57 | ### Solución: Con CTEs (Limpio y legible) 58 | 59 | ```sql 60 | -- Paso 1: Clientes con compras > 1000 61 | WITH high_value_customers AS ( 62 | SELECT 63 | customer_id, 64 | SUM(order_amount) as total_spent 65 | FROM orders 66 | GROUP BY customer_id 67 | HAVING SUM(order_amount) > 1000 68 | ), 69 | 70 | -- Paso 2: Estadísticas por cliente 71 | customer_stats AS ( 72 | SELECT 73 | customer_id, 74 | COUNT(*) as total_orders, 75 | AVG(order_amount) as avg_order_amount, 76 | MAX(order_amount) as max_order, 77 | MIN(order_amount) as min_order 78 | FROM orders 79 | GROUP BY customer_id 80 | ) 81 | 82 | -- Paso 3: Join y resultado final 83 | SELECT 84 | h.customer_id, 85 | h.total_spent, 86 | s.total_orders, 87 | s.avg_order_amount, 88 | s.max_order, 89 | s.min_order, 90 | ROUND(h.total_spent / s.total_orders, 2) as avg_per_order 91 | FROM high_value_customers h 92 | JOIN customer_stats s ON h.customer_id = s.customer_id 93 | ORDER BY h.total_spent DESC; 94 | ``` 95 | 96 | ✅ **Ventajas**: 97 | 98 | - Cada paso es claro 99 | - Fácil de debuggear 100 | - Lógica reutilizable 101 | - SQL se lee como prosa 102 | 103 | --- 104 | 105 | ## Multiple CTEs (Encadenadas) 106 | 107 | ```sql 108 | WITH 109 | -- CTE 1: Base de datos limpia 110 | cleaned_orders AS ( 111 | SELECT 112 | order_id, 113 | customer_id, 114 | order_date, 115 | order_amount, 116 | CASE 117 | WHEN order_amount < 0 THEN 0 118 | ELSE order_amount 119 | END as adjusted_amount 120 | FROM raw_orders 121 | WHERE order_date >= '2024-01-01' 122 | ), 123 | 124 | -- CTE 2: Agregación 125 | monthly_summary AS ( 126 | SELECT 127 | customer_id, 128 | DATE_TRUNC('month', order_date) as month, 129 | COUNT(*) as orders_that_month, 130 | SUM(adjusted_amount) as revenue_that_month 131 | FROM cleaned_orders 132 | GROUP BY customer_id, DATE_TRUNC('month', order_date) 133 | ), 134 | 135 | -- CTE 3: Ranking 136 | ranked_customers AS ( 137 | SELECT 138 | customer_id, 139 | month, 140 | revenue_that_month, 141 | RANK() OVER (PARTITION BY customer_id ORDER BY revenue_that_month DESC) as rank_in_customer_history 142 | FROM monthly_summary 143 | ) 144 | 145 | -- Query principal 146 | SELECT 147 | customer_id, 148 | month, 149 | revenue_that_month 150 | FROM ranked_customers 151 | WHERE rank_in_customer_history <= 3 152 | ORDER BY customer_id, revenue_that_month DESC; 153 | ``` 154 | 155 | --- 156 | 157 | ## Recursive CTEs (Avanzado) 158 | 159 | Para problemas con jerarquías o secuencias: 160 | 161 | ```sql 162 | -- Ejemplo: Generar números del 1 al 10 163 | WITH RECURSIVE numbers AS ( 164 | -- Anchor (caso base) 165 | SELECT 1 as num 166 | 167 | UNION ALL 168 | 169 | -- Recursive (suma 1 hasta llegar a 10) 170 | SELECT num + 1 171 | FROM numbers 172 | WHERE num < 10 173 | ) 174 | SELECT * FROM numbers; 175 | ``` 176 | 177 | ⚠️ **Cuidado**: Recursive CTEs pueden ser lentas. Usa con moderación. 178 | 179 | --- 180 | 181 | ## Errores comunes en entrevista 182 | 183 | - **Error**: Definir la CTE pero no usarla en el SELECT principal → **Solución**: La CTE debe ser usada, sino es código muerto 184 | 185 | - **Error**: Intentar usar una CTE que no está definida → **Solución**: Verifica que la CTE está en el `WITH` y antes de usarla 186 | 187 | - **Error**: No separar múltiples CTEs con comas → **Solución**: Sintaxis correcta: `WITH cte1 AS (...), cte2 AS (...), cte3 AS (...)` 188 | 189 | - **Error**: Recursive CTE sin condición STOP → **Solución**: Siempre incluye `WHERE` para parar la recursión, sino loop infinito 190 | 191 | --- 192 | 193 | ## Preguntas de seguimiento típicas 194 | 195 | 1. **"¿Diferencia entre CTE y subquery?"** 196 | - CTE: Más legible, reutilizable, se define una vez 197 | - Subquery: Anidado dentro, menos legible, puede ejecutarse múltiples veces 198 | - Performance: Similar, pero CTE es mejor para legibilidad 199 | 200 | 2. **"¿Cuando usarías CTE vs Temporary Table?"** 201 | - CTE: Query único, desaparece después 202 | - Temp Table: Persiste sesión, puedes acceder múltiples veces 203 | - CTE es preferido por limpieza 204 | 205 | 3. **"¿Puedes reutilizar una CTE múltiples veces en el mismo query?"** 206 | - Sí, `SELECT * FROM cte UNION ALL SELECT * FROM cte` es válido 207 | - Pero se define UNA sola vez en el WITH 208 | 209 | 4. **"¿Cómo optimizarías un CTE lento?"** 210 | - Agrega índices en las columnas usadas 211 | - Particiona datos si es posible 212 | - Usa MATERIALIZED hint (en algunos motores) 213 | 214 | --- 215 | 216 | ## Comparación: Subquery vs CTE vs Temp Table 217 | 218 | | Aspecto | Subquery | CTE | Temp Table | 219 | | ----------------- | -------------------------------- | ------------------ | ------------------------------ | 220 | | **Sintaxis** | Anidada dentro de SELECT | WITH...AS | CREATE TABLE | 221 | | **Legibilidad** | Difícil (nidación profunda) | Fácil (top-down) | Clara pero verbose | 222 | | **Performance** | Puede ser lento (repite cálculo) | Similar a subquery | Mejor si reutilizada | 223 | | **Reutilización** | Cada uso = evaluación | Define una vez | Persistente | 224 | | **Cuándo usar** | Simple, una vez | Complejo, modular | Datos grandes, varias sesiones | 225 | 226 | --- 227 | 228 | ## Real-World Scenario: Data Warehouse 229 | 230 | En data engineering, CTEs se usan constantemente: 231 | 232 | ```sql 233 | WITH 234 | -- Stage 1: Source data 235 | source_data AS ( 236 | SELECT * FROM raw_events WHERE date >= '2024-01-01' 237 | ), 238 | 239 | -- Stage 2: Transformations 240 | transformed_data AS ( 241 | SELECT 242 | user_id, 243 | event_type, 244 | TIMESTAMP as event_time, 245 | CASE WHEN event_type = 'purchase' THEN amount ELSE 0 END as revenue 246 | FROM source_data 247 | ), 248 | 249 | -- Stage 3: Aggregation 250 | daily_summary AS ( 251 | SELECT 252 | DATE(event_time) as event_date, 253 | COUNT(*) as total_events, 254 | SUM(revenue) as daily_revenue 255 | FROM transformed_data 256 | GROUP BY DATE(event_time) 257 | ) 258 | 259 | -- Final: Load to warehouse 260 | INSERT INTO analytics.daily_events 261 | SELECT * FROM daily_summary; 262 | ``` 263 | 264 | --- 265 | 266 | ## Referencias 267 | 268 | - [CTEs (WITH clause) - PostgreSQL Docs](https://www.postgresql.org/docs/current/queries-with.html) 269 | - [Recursive CTEs - Advanced SQL](https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-RECURSIVE) 270 | -------------------------------------------------------------------------------- /sql/07-self-join-hierarchies.md: -------------------------------------------------------------------------------- 1 | # Self Joins & Datos Jerárquicos 2 | 3 | **Tags**: #sql #self-join #hierarchies #relationships #tricky #real-interview 4 | 5 | --- 6 | 7 | ## TL;DR 8 | 9 | Un **Self Join** es cuando juntas una tabla CONSIGO MISMA. Útil para relaciones jerárquicas (manager-employee, parent-child). Alias la tabla 2 veces con nombres distintos: `FROM employees e1 JOIN employees e2 ON e1.manager_id = e2.id`. 10 | 11 | --- 12 | 13 | ## Concepto Core 14 | 15 | - **Qué es**: Juntar una tabla consigo misma usando aliases distintos. Permite encontrar relaciones dentro de los mismos datos 16 | - **Por qué importa**: Jerarquías, reportes de línea, relaciones recursivas. Muy común en entrevistas. Demuestra comprensión profundo de JOINs 17 | - **Principio clave**: Necesitas 2+ aliases de la misma tabla. Cada alias representa un "rol" diferente (ej: manager vs employee) 18 | 19 | --- 20 | 21 | ## Memory Trick 22 | 23 | **"Espejo de tabla"** — Imagina la tabla como espejo. Un lado es "managers", el otro "employees". Self Join los conecta. 24 | 25 | --- 26 | 27 | ## Cómo explicarlo en entrevista 28 | 29 | **Paso 1**: "Tengo una tabla employees donde cada fila tiene `manager_id` que apunta a otro empleado" 30 | 31 | **Paso 2**: "Para encontrar quién es el manager de cada employee, hago JOIN de employees CONSIGO MISMA: `FROM employees e1 (empleado) JOIN employees e2 (manager) ON e1.manager_id = e2.id`" 32 | 33 | **Paso 3**: "El resultado: cada fila tiene el empleado Y su manager en la misma fila" 34 | 35 | --- 36 | 37 | ## Código/Query ejemplo 38 | 39 | ### Escenario: Estructura Organizacional 40 | 41 | **Tabla: employees** 42 | 43 | ``` 44 | id | name | manager_id | salary | department 45 | 1 | Alice | NULL | 150000 | Engineering (CEO) 46 | 2 | Bob | 1 | 120000 | Engineering 47 | 3 | Charlie | 1 | 100000 | Engineering 48 | 4 | David | 2 | 80000 | Engineering 49 | 5 | Eve | 2 | 85000 | Engineering 50 | 6 | Frank | NULL | 140000 | Sales (Head of Sales) 51 | 7 | Grace | 6 | 90000 | Sales 52 | ``` 53 | 54 | ### Problema 1: ¿Quién es el Manager de cada Empleado? 55 | 56 | ```sql 57 | -- ❌ Intento sin Self Join (no funciona) 58 | SELECT 59 | id, 60 | name, 61 | manager_id, 62 | (SELECT name FROM employees WHERE id = manager_id) as manager_name 63 | FROM employees; 64 | 65 | -- ✅ Self Join (correcto) 66 | SELECT 67 | e1.id as employee_id, 68 | e1.name as employee_name, 69 | e1.department, 70 | e1.salary as employee_salary, 71 | e2.id as manager_id, 72 | e2.name as manager_name, 73 | e2.salary as manager_salary, 74 | e1.salary - e2.salary as salary_diff 75 | FROM employees e1 76 | LEFT JOIN employees e2 ON e1.manager_id = e2.id 77 | ORDER BY e1.department, e1.name; 78 | ``` 79 | 80 | **Resultado:** 81 | 82 | ``` 83 | employee_id | employee_name | department | employee_salary | manager_id | manager_name | manager_salary | salary_diff 84 | 2 | Bob | Engineering | 120000 | 1 | Alice | 150000 | -30000 85 | 3 | Charlie | Engineering | 100000 | 1 | Alice | 150000 | -50000 86 | 4 | David | Engineering | 80000 | 2 | Bob | 120000 | -40000 87 | 5 | Eve | Engineering | 85000 | 2 | Bob | 120000 | -35000 88 | 7 | Grace | Sales | 90000 | 6 | Frank | 140000 | -50000 89 | 1 | Alice | Engineering | 150000 | NULL | NULL | NULL | NULL 90 | 6 | Frank | Sales | 140000 | NULL | NULL | NULL | NULL 91 | ``` 92 | 93 | **Nota**: LEFT JOIN porque algunos (Alice, Frank) no tienen manager 94 | 95 | --- 96 | 97 | ### Problema 2: Cadena Jerárquica (3+ niveles) 98 | 99 | ```sql 100 | -- Manager -> Director -> VP 101 | SELECT 102 | e1.id as employee_id, 103 | e1.name as employee_name, 104 | e1.department, 105 | 106 | e2.name as direct_manager, 107 | 108 | e3.name as director, 109 | 110 | e4.name as vp 111 | FROM employees e1 112 | LEFT JOIN employees e2 ON e1.manager_id = e2.id 113 | LEFT JOIN employees e3 ON e2.manager_id = e3.id 114 | LEFT JOIN employees e4 ON e3.manager_id = e4.id 115 | ORDER BY e1.department, e1.name; 116 | ``` 117 | 118 | **Problema**: Si tienes 10 niveles, necesitas 10 JOINs. Mejor solución: Recursive CTE 119 | 120 | --- 121 | 122 | ### Problema 3: Todos los Reportes Directos de un Manager 123 | 124 | ```sql 125 | -- ¿Quiénes reportan a Alice? 126 | SELECT 127 | manager.id as manager_id, 128 | manager.name as manager_name, 129 | employee.id as report_id, 130 | employee.name as report_name, 131 | employee.salary 132 | FROM employees employee 133 | JOIN employees manager ON employee.manager_id = manager.id 134 | WHERE manager.name = 'Alice' 135 | ORDER BY employee.salary DESC; 136 | ``` 137 | 138 | **Resultado:** 139 | 140 | ``` 141 | manager_id | manager_name | report_id | report_name | salary 142 | 1 | Alice | 2 | Bob | 120000 143 | 1 | Alice | 3 | Charlie | 100000 144 | ``` 145 | 146 | --- 147 | 148 | ### Problema 4: Todos los Subordinados (Recursivo - Cadena Completa) 149 | 150 | Para encontrar TODOS los subordinados de Alice (no solo directos): 151 | 152 | ```sql 153 | -- Recursive CTE (mejor que múltiples Self Joins) 154 | WITH RECURSIVE org_hierarchy AS ( 155 | -- Base: Alice (manager) 156 | SELECT 157 | id, 158 | name, 159 | manager_id, 160 | 1 as level 161 | FROM employees 162 | WHERE id = 1 -- Alice's ID 163 | 164 | UNION ALL 165 | 166 | -- Recursive: Sus subordinados 167 | SELECT 168 | e.id, 169 | e.name, 170 | e.manager_id, 171 | oh.level + 1 172 | FROM employees e 173 | INNER JOIN org_hierarchy oh ON e.manager_id = oh.id 174 | WHERE oh.level < 10 -- Limita profundidad para evitar loops 175 | ) 176 | SELECT * FROM org_hierarchy 177 | ORDER BY level, name; 178 | ``` 179 | 180 | **Resultado (todos bajo Alice):** 181 | 182 | ``` 183 | id | name | manager_id | level 184 | 1 | Alice | NULL | 1 185 | 2 | Bob | 1 | 2 186 | 3 | Charlie | 1 | 2 187 | 4 | David | 2 | 3 188 | 5 | Eve | 2 | 3 189 | ``` 190 | 191 | --- 192 | 193 | ### Problema 5: Encontrar Empleados con Mismo Manager 194 | 195 | ```sql 196 | -- ¿Quiénes trabajan bajo el mismo manager? 197 | SELECT 198 | e1.name as employee_1, 199 | e2.name as employee_2, 200 | e1.department, 201 | e1.manager_id 202 | FROM employees e1 203 | JOIN employees e2 ON e1.manager_id = e2.manager_id AND e1.id < e2.id 204 | WHERE e1.manager_id IS NOT NULL 205 | ORDER BY e1.manager_id, e1.name; 206 | ``` 207 | 208 | **Resultado:** 209 | 210 | ``` 211 | employee_1 | employee_2 | department | manager_id 212 | Bob | Charlie | Engineering | 1 213 | David | Eve | Engineering | 2 214 | ``` 215 | 216 | **Nota**: `e1.id < e2.id` evita duplicados (Bob-Charlie vs Charlie-Bob) 217 | 218 | --- 219 | 220 | ## Errores comunes en entrevista 221 | 222 | - **Error**: Olvidar aliases en Self Join → **Solución**: SIEMPRE usa `FROM table t1 JOIN table t2`. Sin aliases, ambos son "table" 223 | 224 | - **Error**: Usar INNER JOIN cuando debería LEFT JOIN (pierde CEOs sin manager) → **Solución**: Piensa en la lógica: ¿todos deben tener match? 225 | 226 | - **Error**: No evitar duplicados en `e1.id < e2.id` → **Solución**: Sin esto, obtienes cada par 2 veces 227 | 228 | - **Error**: Crear Recursive CTE sin condición STOP → **Solución**: Siempre limita profundidad para evitar loop infinito 229 | 230 | --- 231 | 232 | ## Preguntas de seguimiento típicas 233 | 234 | 1. **"¿Diferencia entre Self Join y Recursive CTE?"** 235 | - Self Join: Niveles fijos (ej: solo employee + manager) 236 | - Recursive CTE: Cadenas profundas y desconocidas 237 | 238 | 2. **"¿Cómo optimizarías un Self Join lento?"** 239 | - Índice en `manager_id` 240 | - Índice en jerarquía si es muy profunda 241 | - Considerar denormalización (guardar toda la cadena en una columna) 242 | 243 | 3. **"¿Cómo manejarías ciclos?"** (ej: A → B → A) 244 | - Recursive CTE con LIMIT en level 245 | - Agregar lógica para detectar: `WHERE id NOT IN (path_so_far)` 246 | 247 | 4. **"¿Real-World Use Cases?"** 248 | - Org charts, jerarquías 249 | - Categorías de productos (parent-child) 250 | - Rutas de tránsito (station → next station) 251 | - Threads de comentarios (reply-to) 252 | 253 | --- 254 | 255 | ## Comparación: Self Join vs Recursive vs Denormalization 256 | 257 | | Escenario | Solución | Ventaja | Desventaja | 258 | | -------------------------------------- | ------------------------------ | -------------- | ------------------- | 259 | | **2 niveles (manager-employee)** | Self Join | Simple, rápido | Solo 2 niveles | 260 | | **N niveles desconocidos** | Recursive CTE | Flexible | Más lento, complejo | 261 | | **Acceso frecuente a cadena completa** | Denormalization (guardar path) | Super rápido | Caro mantener | 262 | | **Queries exploratorios** | Self Join + manual | Flexible | Múltiples queries | 263 | 264 | --- 265 | 266 | ## Real-World: LinkedIn Org Chart 267 | 268 | ```sql 269 | -- Todos bajo VP de Engineering 270 | WITH RECURSIVE reporting_chain AS ( 271 | SELECT id, name, manager_id, 1 as depth 272 | FROM employees 273 | WHERE id = (SELECT id FROM employees WHERE title = 'VP Engineering') 274 | 275 | UNION ALL 276 | 277 | SELECT e.id, e.name, e.manager_id, rc.depth + 1 278 | FROM employees e 279 | INNER JOIN reporting_chain rc ON e.manager_id = rc.id 280 | WHERE rc.depth < 5 281 | ) 282 | SELECT * FROM reporting_chain 283 | ORDER BY depth, name; 284 | ``` 285 | 286 | --- 287 | 288 | ## Referencias 289 | 290 | - [Self Join Examples - W3Schools](https://www.w3schools.com/sql/sql_join_self.asp) 291 | - [Recursive CTEs - PostgreSQL](https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-RECURSIVE) 292 | - [Hierarchical Data in SQL - Use The Index Luke](https://use-the-index-luke.com/) 293 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Data Engineering Interview Bank 🚀 2 | 3 | **+40 preguntas profesionales + recursos para dominar Data Engineering** 4 | 5 | --- 6 | 7 | ## 📌 ¿Qué es esto? 8 | 9 | Repositorio completo de **preguntas de Data Engineering** cubriendo SQL, PySpark, System Design, Cloud, Data Modeling, Orchestration y Streaming. 10 | 11 | **Diseñado para:** 12 | 13 | - ✅ Prepararte para entrevistas (Junior → Mid → Senior level) 14 | - ✅ Refrescar conocimientos (seniors) 15 | - ✅ Aprender patterns reales (producción) 16 | - ✅ GitHub contributions 17 | 18 | **Cobertura (hasta el momento):** 19 | 20 | - SQL (Window Functions, Optimization, Data Quality) 21 | - PySpark (Architecture, Performance, I/O) 22 | - System Design (ETL, Warehouse, Real-Time, CDC, Lineage) 23 | - Python/DSA (Strings, Collections, Generators, Decorators) 24 | - Cloud (AWS, GCP, Azure, Multi-Cloud, Cost, Security) 25 | - Data Modeling (Star Schema, SCD, Normalization) 26 | - Orchestration (Airflow, dbt, Patterns) 27 | - Streaming (Kafka, Windowing, Lambda vs Kappa) 28 | 29 | --- 30 | 31 | ## 🎯 Cómo Usar Este Repositorio 32 | 33 | ### Para Principiantes / Mid-Level 34 | 35 | 1. Comienza con **SQL** (fundamentals) 36 | 2. **Python/DSA** (language skills) 37 | 3. Luego **PySpark** (core tool) 38 | 4. **System Design** (ver el big picture) 39 | 40 | ### Para Seniors 41 | 42 | 1. **System Design** (detalles arquitectónicos) 43 | 2. **Orchestration** (production patterns) 44 | 3. **Streaming** (real-time systems) 45 | 4. **Cloud** (strategic decisions) 46 | 47 | --- 48 | 49 | ## 📖 Cómo Estudiar Cada Pregunta 50 | 51 | Cada pregunta sigue este patrón: 52 | 53 | **TL;DR (30 seg)** 54 | ├─ Qué es 55 | ├─ Por qué importa 56 | └─ Principio clave 57 | 58 | **Concepto (5 min)** 59 | ├─ Definición clara 60 | ├─ Analogy / memory trick 61 | └─ Cuándo / dónde aplica 62 | 63 | **Explicación técnica (10 min)** 64 | ├─ Diagrama 65 | ├─ Código 66 | └─ Real-world example 67 | 68 | **Errores comunes (2 min)** 69 | └─ Qué no hacer en entrevista 70 | 71 | **Preguntas de seguimiento (2 min)** 72 | └─ Questions que podrían hacer 73 | 74 | **Tiempo total:** 15–20 minutos profundos 75 | 76 | **Tip:** Después de leer, explica el concepto en voz alta. Eso es lo que harás en entrevista. 77 | 78 | --- 79 | 80 | ## 🌟 Recursos Recomendados 81 | 82 | ### 📺 Canales YouTube 83 | 84 | - [Omar Valdez](https://www.youtube.com/@soyomarvaldezg) 85 | - [Data Wizards](https://www.youtube.com/@DataWizardClub) 86 | - [Daniel Portugal](https://www.youtube.com/@danielportugalr) 87 | - [CodinEric](https://www.youtube.com/@CodinEric) 88 | - [Dataneo](https://www.youtube.com/@Dateneo) 89 | - [FabriBits](https://www.youtube.com/@FabriBits) 90 | - [Daniel Santos](https://www.youtube.com/@danielsantosdata) 91 | - [Hazlo con Datos](https://www.youtube.com/@HazloConDatos) 92 | - [Nataya Flores](https://www.youtube.com/@natayadev/videos) 93 | - [Keyla Dolores](https://www.youtube.com/@KeylaDolores) 94 | 95 | ### 📇 LinkedIn Data Profiles 96 | 97 | - [Omar Valdez](https://linkedin.com/in/soyomarvaldezg) 98 | - [Fabricio Lennart](https://www.linkedin.com/in/fabricio-lennart/) 99 | - [Bruno Masciarelli](https://www.linkedin.com/in/bruno-masciarelli/) 100 | - [Daniel Portugal](https://www.linkedin.com/in/danielportugalr/) 101 | - [Kremlin Huaman](https://www.linkedin.com/in/khuamans/) 102 | - [Daniel Santos](https://www.linkedin.com/in/danielsantosp/) 103 | - [Data Wizards](https://linkedin.com/company/data-wizard-club) 104 | - [Nataya Flores](https://www.linkedin.com/in/natayadev/) 105 | - [Flavio Cesar Sandoval](https://www.linkedin.com/in/flaviocesarsandoval/) 106 | - [CodinEric](https://www.linkedin.com/in/eric-rishmuller/) 107 | - [Keyla Dolores](https://www.linkedin.com/in/keyladolores/) 108 | - [Elias Velazquez](https://www.linkedin.com/in/eliassvelazquez/) 109 | - [Antony Henao](https://www.linkedin.com/in/ajhenaor/) 110 | - [Ian Saura](https://www.linkedin.com/in/ian-saura/) 111 | 112 | ### 🐦 X / Twitter Profiles 113 | 114 | - [Omar Valdez](https://x.com/soyomarvaldezg) 115 | - [Nataya Dev](https://x.com/natayadev) 116 | - [Fabricio Lennart](https://x.com/fabriciolennart) 117 | - [CodinEric](https://x.com/CodinEric) 118 | - [Flavio Cesar Sandoval](https://x.com/DSandovalFlavio) 119 | - [Elias Velazquez](https://x.com/esvdev) 120 | - [Antony Henao](https://x.com/ajhenaor) 121 | 122 | ### 🧭 Cursos / Roadmaps 123 | 124 | - [Dateneo x Bruno Masciarelli](https://www.dateneo.com/) 125 | - [Data Hackers Academy x Daniel Santos](https://datahackersacademy.com/) 126 | - [Data Engineering Roadmap x Nataya Flores](https://github.com/natayadev/dataengineering-roadmap?tab=readme-ov-file) 127 | - [Bootcamp Fundamentos Data Engineering x Ian Saura](https://iansaura.com/bootcamps) 128 | - [Roadmap Interactivo, Proyectos y Videos x Ian Saura](https://iansaura.com/suscripcion) 129 | - [Bootcamp Ingeniería de datos x Kremlin Huaman](https://www.datacore.com.pe/bootcamp/bootcamp_data_engineering/) 130 | - [Data Engineering Roadmap x roadmap.sh](https://roadmap.sh/data-engineer) 131 | - [Awesome Data Engineering GitHub Repo](https://github.com/igorbarinov/awesome-data-engineering) 132 | - [Data Engineering Roadmap x DataExpert.io](https://blog.dataexpert.io/p/the-2025-breaking-into-data-engineering-roadmap) 133 | - [101 Start Data Engineering](https://de101.startdataengineering.com/) 134 | 135 | ### 💬 Comunidades 136 | 137 | - [Data Wizards Discord Server](https://discord.gg/bzH8Pjajv6) 138 | - [Data Plumbers Whatsapp](https://chat.whatsapp.com/FPPDkGwcYB61yBOfx4Pnm7) 139 | 140 | ### 📷 Instagram 141 | 142 | - [Omar Valdez](https://www.instagram.com/soyomarvaldezg) 143 | - [CodinEric](https://www.instagram.com/codin_eric) 144 | - [Nataya Dev](https://www.instagram.com/natayadev) 145 | - [Antony Henao](https://www.instagram.com/ajhenaor/) 146 | - [Ian Saura](https://www.instagram.com/iansaura/) 147 | 148 | ### TikTok 149 | 150 | - [Omar Valdez](https://www.tiktok.com/@soyomarvaldezg) 151 | - [Ian Saura](https://www.tiktok.com/@iansaura) 152 | - [Daniel Portugal](https://www.tiktok.com/@danielportugalr) 153 | 154 | ### Blogs 155 | 156 | - [Omar Valdez](https://soyomarvaldezg.substack.com/) 157 | - [El Ingeniero Consciente x Elias Velazquez](https://elingenieroconsciente.substack.com/) 158 | - [The Latam Engineer x Antony Henao](https://thelatamengineer.substack.com/) 159 | 160 | --- 161 | 162 | ## 📚 Libros Recomendados 163 | 164 | - ["Desbloquea tu Carrera en Datos" — Omar Valdez](https://payhip.com/b/6zilp) 165 | - "Fundamentals of Data Engineering" — Joe Reis, Matt Housley 166 | - "Data Pipelines Pocket Reference" — James Densmore 167 | - "The Data Warehouse Toolkit" — Ralph Kimball 168 | - "Data Engineering Design Patterns" — Bartosz Konieczny 169 | - "Deciphering Data Architectures" — James Serra 170 | - "97 Things Every Data Engineer Should Know" — Tobias Macey 171 | - "Spark: The Definitive Guide" — Chambers & Zaharia 172 | - "Designing Data-Intensive Applications" — Martin Kleppmann 173 | - "Learning Apache Kafka" — Stephane Maarek 174 | 175 | --- 176 | 177 | ## 🤝 Contribuciones Bienvenidas 178 | 179 | **Este repo es comunitario.** 180 | Deseo que: 181 | 182 | - ✅ Agregues nuevas preguntas 183 | - ✅ Mejores explicaciones existentes 184 | - ✅ Corrijas errores 185 | - ✅ Agregues ejemplos de tu experiencia 186 | 187 | --- 188 | 189 | ### 🛠️ Cómo Contribuir 190 | 191 | #### 1. Fork este repositorio 192 | 193 | ``` 194 | git clone https://github.com/tu-usuario/data-engineering-interview-bank.git 195 | cd data-engineering-interview-bank 196 | git checkout -b mi-contribucion 197 | ``` 198 | 199 | #### 2. Sigue el Formato 200 | 201 | Cada pregunta debe ser un archivo `.md` con esta estructura: 202 | 203 | ``` 204 | **Título Claro** 205 | **Tags:** #tag1 #tag2 #tag3 #real-interview 206 | **Dificultad:** Junior | Mid | Senior 207 | 208 | **TL;DR** 209 | 2–3 líneas ultra-concisas. Qué es + por qué importa + principio clave. 210 | 211 | **Concepto Core** 212 | - Qué es 213 | - Por qué importa 214 | - Principio clave 215 | 216 | **Memory Trick** 217 | Frase memorable para recordar. 218 | 219 | **Cómo explicarlo en entrevista** 220 | 4 pasos claros. Cómo explicarías esto a un entrevistador. 221 | 222 | **Código / Ejemplos** 223 | Ejemplos reales, ejecutables si es posible. 224 | 225 | **Errores Comunes en Entrevista** 226 | Error: X 227 | → Solución: Y 228 | 229 | **Preguntas de Seguimiento** 230 | "Pregunta?" 231 | Respuesta corta. 232 | 233 | **References** 234 | Link 235 | ``` 236 | 237 | #### 3. Checklist Antes de PR 238 | 239 | - [ ] Título descriptivo 240 | - [ ] TL;DR < 100 palabras 241 | - [ ] Mínimo 1 ejemplo de código 242 | - [ ] Real-world case study incluido 243 | - [ ] References activos 244 | - [ ] Explica como si fuera entrevista 245 | - [ ] Proofread (sin typos) 246 | 247 | #### 4. Ejemplos de Contribuciones Bienvenidas 248 | 249 | ✅ BIENVENIDO: 250 | ├─ Nueva pregunta en sección existente (Streaming, System Design) 251 | ├─ Mejorar explicación (más clara, mejor ejemplo) 252 | ├─ Agregar casos reales (tu experiencia) 253 | ├─ Corregir errores (typos, code bugs) 254 | └─ Traducir a español (si está en inglés) 255 | 256 | ❌ NO SE ACEPTA: 257 | ├─ Cambios estructurales sin discusión 258 | ├─ Spam o auto-promoción 259 | ├─ Contenido sin fuentes 260 | └─ Preguntas out-of-scope (no data engineering) 261 | 262 | #### 5. Proceso PR 263 | 264 | 1. **Fork** → **Rama** → **Commit** → **Push** 265 | 2. **Pull Request** con descripción clara 266 | 3. Se revisa (24–72h) 267 | 4. Feedback / Merge 268 | 5. ¡Crédito en README! 269 | 270 | --- 271 | 272 | ## 📝 License 273 | 274 | MIT License — Puedes usar, modificar y compartir (con atribución). 275 | 276 | --- 277 | 278 | ## ⭐ Si te gustó 279 | 280 | Dale ⭐ al repo + compártelo con tu red + contribuye. 281 | 282 | --- 283 | 284 | **Built with ❤️ para la comunidad Data Engineering hispanohablante** 285 | -------------------------------------------------------------------------------- /sql/08-aggregations-group-by-having.md: -------------------------------------------------------------------------------- 1 | # Agregaciones Complejas: GROUP BY & HAVING 2 | 3 | **Tags**: #sql #aggregations #group-by #having #data-analysis #real-interview 4 | 5 | --- 6 | 7 | ## TL;DR 8 | 9 | `GROUP BY` agrupa filas. `HAVING` filtra grupos (como WHERE pero para agregados). Usa `GROUP_CONCAT` (MySQL) / `STRING_AGG` (PostgreSQL) / `LISTAGG` (Oracle) para concatenar valores dentro de grupos. Siempre: SELECT solo columnas que están en GROUP BY o funciones agregadas. 10 | 11 | --- 12 | 13 | ## Concepto Core 14 | 15 | - **Qué es**: GROUP BY divide datos en grupos. HAVING filtra esos grupos basado en agregados. Funciones como SUM, COUNT, STRING_AGG combinan valores dentro de cada grupo 16 | - **Por qué importa**: Fundamental en reporting y análisis. Demuestra comprensión de agregaciones y cómo pensar en "grupos" vs "filas" 17 | - **Principio clave**: WHERE filtra filas ANTES de GROUP BY. HAVING filtra grupos DESPUÉS de GROUP BY 18 | 19 | --- 20 | 21 | ## Memory Trick 22 | 23 | **"Dividir, agregar, filtrar"** — GROUP BY divide, SUM/COUNT agregan, HAVING filtra el resultado agregado. 24 | 25 | --- 26 | 27 | ## Cómo explicarlo en entrevista 28 | 29 | **Paso 1**: "GROUP BY divide filas en grupos basado en una columna (ej: por categoría)" 30 | 31 | **Paso 2**: "Dentro de cada grupo, aplico funciones agregadas: COUNT(), SUM(), AVG(), etc." 32 | 33 | **Paso 3**: "HAVING filtra esos grupos. Si necesito solo grupos con suma > 1000, uso `HAVING SUM(amount) > 1000`" 34 | 35 | **Paso 4**: "Diferencia: WHERE filtra filas individuales. HAVING filtra grupos agregados" 36 | 37 | --- 38 | 39 | ## Código/Query ejemplo 40 | 41 | ### Tabla: sales 42 | 43 | ``` 44 | order_id | customer_id | product | category | amount | order_date 45 | 1 | 101 | Laptop | Electronics| 1000 | 2024-01-15 46 | 2 | 101 | Mouse | Electronics| 25 | 2024-01-20 47 | 3 | 102 | Desk | Furniture | 300 | 2024-02-01 48 | 4 | 102 | Chair | Furniture | 150 | 2024-02-10 49 | 5 | 103 | Monitor | Electronics| 400 | 2024-01-05 50 | 6 | 101 | Keyboard | Electronics| 80 | 2024-03-01 51 | 7 | 104 | Lamp | Furniture | 50 | 2024-02-15 52 | ``` 53 | 54 | --- 55 | 56 | ### Problema 1: Total de Ventas por Categoría (Básico) 57 | 58 | ```sql 59 | SELECT 60 | category, 61 | COUNT(*) as total_orders, 62 | SUM(amount) as total_revenue, 63 | AVG(amount) as avg_order, 64 | MIN(amount) as min_order, 65 | MAX(amount) as max_order 66 | FROM sales 67 | GROUP BY category 68 | ORDER BY total_revenue DESC; 69 | ``` 70 | 71 | **Resultado:** 72 | 73 | ``` 74 | category | total_orders | total_revenue | avg_order | min_order | max_order 75 | Electronics | 4 | 1505 | 376.25 | 25 | 1000 76 | Furniture | 3 | 500 | 166.67 | 50 | 300 77 | ``` 78 | 79 | ✅ **Nota**: SELECT contiene SOLO lo que está en GROUP BY (category) o agregados (COUNT, SUM) 80 | 81 | --- 82 | 83 | ### Problema 2: HAVING - Filtrar Grupos 84 | 85 | ```sql 86 | -- ¿Categorías con ingresos > 500? 87 | SELECT 88 | category, 89 | COUNT(*) as total_orders, 90 | SUM(amount) as total_revenue 91 | FROM sales 92 | GROUP BY category 93 | HAVING SUM(amount) > 500 -- Filtra GRUPOS, no filas 94 | ORDER BY total_revenue DESC; 95 | ``` 96 | 97 | **Resultado:** 98 | 99 | ``` 100 | category | total_orders | total_revenue 101 | Electronics | 4 | 1505 102 | ``` 103 | 104 | ⚠️ **¿Dónde va cada filtro?** 105 | WHERE → Filtra filas ANTES de GROUP BY 106 | HAVING → Filtra grupos DESPUÉS de GROUP BY 107 | 108 | ```sql 109 | SELECT category, SUM(amount) as rev 110 | FROM sales 111 | WHERE amount > 100 -- Filtra filas: solo ordenes > 100 112 | GROUP BY category 113 | HAVING SUM(amount) > 500 -- Filtra grupos: solo categorías con suma > 500 114 | ``` 115 | 116 | --- 117 | 118 | ### Problema 3: Concatenar Valores en un Grupo 119 | 120 | ```sql 121 | -- Todos los productos vendidos en cada categoría, en una lista 122 | SELECT 123 | category, 124 | COUNT(*) as total_products, 125 | STRING_AGG(product, ', ') as product_list, -- PostgreSQL 126 | -- GROUP_CONCAT(product) as product_list, -- MySQL 127 | -- LISTAGG(product, ', ') WITHIN GROUP (ORDER BY product) -- Oracle 128 | SUM(amount) as total_revenue 129 | FROM sales 130 | GROUP BY category 131 | ORDER BY total_revenue DESC; 132 | ``` 133 | 134 | **Resultado:** 135 | 136 | ``` 137 | category | total_products | product_list | total_revenue 138 | Electronics | 4 | Laptop, Mouse, Monitor, Keyboard | 1505 139 | Furniture | 3 | Chair, Desk, Lamp | 500 140 | ``` 141 | 142 | --- 143 | 144 | ### Problema 4: GROUP BY con Múltiples Columnas 145 | 146 | ```sql 147 | -- Ventas por categoría Y mes 148 | SELECT 149 | category, 150 | DATE_TRUNC('month', order_date) as month, 151 | COUNT(*) as orders_that_month, 152 | SUM(amount) as revenue_that_month 153 | FROM sales 154 | GROUP BY category, DATE_TRUNC('month', order_date) 155 | ORDER BY category, month; 156 | ``` 157 | 158 | **Resultado:** 159 | 160 | ``` 161 | category | month | orders_that_month | revenue_that_month 162 | Electronics | 2024-01-01 | 3 | 1425 163 | Electronics | 2024-03-01 | 1 | 80 164 | Furniture | 2024-02-01 | 3 | 500 165 | ``` 166 | 167 | --- 168 | 169 | ### Problema 5: Clientes con > 2 Órdenes (HAVING con COUNT) 170 | 171 | ```sql 172 | SELECT 173 | customer_id, 174 | COUNT(*) as order_count, 175 | SUM(amount) as total_spent, 176 | STRING_AGG(product, ', ' ORDER BY product) as products_purchased 177 | FROM sales 178 | GROUP BY customer_id 179 | HAVING COUNT(*) > 1 -- Solo clientes con más de 1 orden 180 | ORDER BY order_count DESC; 181 | ``` 182 | 183 | **Resultado:** 184 | 185 | ``` 186 | customer_id | order_count | total_spent | products_purchased 187 | 101 | 3 | 1105 | Keyboard, Laptop, Mouse 188 | 102 | 2 | 450 | Chair, Desk 189 | ``` 190 | 191 | --- 192 | 193 | ### Problema 6: WHERE + GROUP BY + HAVING (Combinados) 194 | 195 | ```sql 196 | -- Categorías que tienen > 1 orden DE ELECTRONICS, con total > 300 197 | SELECT 198 | category, 199 | COUNT(*) as order_count, 200 | SUM(amount) as total_revenue 201 | FROM sales 202 | WHERE amount > 50 -- Filtra filas: ordenes > 50 203 | GROUP BY category 204 | HAVING SUM(amount) > 300 AND COUNT(*) > 1 -- Filtra grupos 205 | ORDER BY total_revenue DESC; 206 | ``` 207 | 208 | **Resultado:** 209 | 210 | ``` 211 | category | order_count | total_revenue 212 | Electronics | 4 | 1505 213 | ``` 214 | 215 | **Orden de ejecución:** 216 | 217 | 1. WHERE filtra (amount > 50) 218 | 2. GROUP BY agrupa 219 | 3. Agregados se calculan 220 | 4. HAVING filtra grupos 221 | 222 | --- 223 | 224 | ## Diferencias: WHERE vs HAVING vs GROUP BY 225 | 226 | | Concepto | Cuándo | Ejemplo | Filtra | 227 | | ------------ | ------------------ | --------------------- | ------------------ | 228 | | **WHERE** | Antes de agrupar | `WHERE amount > 100` | Filas individuales | 229 | | **GROUP BY** | Agrupa filas | `GROUP BY category` | N/A (agrupa) | 230 | | **HAVING** | Después de agrupar | `HAVING COUNT(*) > 5` | Grupos | 231 | | **ORDER BY** | Después de todo | `ORDER BY total DESC` | Orden resultado | 232 | 233 | --- 234 | 235 | ## STRING_AGG vs GROUP_CONCAT vs LISTAGG 236 | 237 | | BD | Función | Sintaxis | 238 | | ---------- | ------------ | --------------------------------------------------- | 239 | | PostgreSQL | STRING_AGG | `STRING_AGG(col, ', ' ORDER BY col)` | 240 | | MySQL | GROUP_CONCAT | `GROUP_CONCAT(col ORDER BY col SEPARATOR ', ')` | 241 | | Oracle | LISTAGG | `LISTAGG(col, ', ') WITHIN GROUP (ORDER BY col)` | 242 | | SQL Server | STRING_AGG | `STRING_AGG(col, ', ') WITHIN GROUP (ORDER BY col)` | 243 | 244 | --- 245 | 246 | ## Errores comunes en entrevista 247 | 248 | - **Error**: Poner columna en SELECT sin estar en GROUP BY → **Solución**: SELECT solo: columnas de GROUP BY + agregados 249 | 250 | - **Error**: Usar WHERE cuando necesitas HAVING → **Solución**: WHERE es pre-aggregation, HAVING es post-aggregation 251 | 252 | - **Error**: ORDER BY posición incorrecta → **Solución**: ORDER BY va DESPUÉS de HAVING 253 | 254 | - **Error**: Olvidar que agregados necesitan ALL datos del grupo → **Solución**: SUM() necesita acceso a todas las filas del grupo 255 | 256 | --- 257 | 258 | ## Preguntas de seguimiento típicas 259 | 260 | 1. **"¿Puedo usar HAVING sin GROUP BY?"** 261 | - Técnicamente sí, pero es raro. Sin GROUP BY, toda tabla = 1 grupo 262 | 263 | 2. **"¿Diferencia entre ORDER BY col vs ORDER BY 1?"** 264 | - `ORDER BY col` es explícito (mejor) 265 | - `ORDER BY 1` es posición de columna (less clear, evita) 266 | 267 | 3. **"¿Cómo hago un GROUP BY en 2+ columnas?"** 268 | - `GROUP BY col1, col2` — Crea grupos únicos por combinación 269 | 270 | 4. **"¿Puede GROUP BY ser en columna sin estar en SELECT?"** 271 | - Sí, válido: `SELECT category FROM sales GROUP BY category, customer_id` — Agrupa pero no muestra customer_id 272 | 273 | --- 274 | 275 | ## Real-World: E-Commerce Analytics 276 | 277 | ```sql 278 | -- Top productos por categoría (últimos 30 días) con más de 10 órdenes 279 | SELECT 280 | category, 281 | product, 282 | DATE_TRUNC('month', order_date)::date as month, 283 | COUNT(*) as order_count, 284 | SUM(amount) as revenue, 285 | AVG(amount) as avg_order_value, 286 | STRING_AGG(DISTINCT customer_id::text, ', ') as unique_customers 287 | FROM orders 288 | WHERE order_date >= NOW() - INTERVAL '30 days' 289 | GROUP BY category, product, DATE_TRUNC('month', order_date) 290 | HAVING COUNT(*) > 10 291 | ORDER BY revenue DESC 292 | LIMIT 20; 293 | ``` 294 | 295 | --- 296 | 297 | ## Referencias 298 | 299 | - [GROUP BY - PostgreSQL Docs](https://www.postgresql.org/docs/current/sql-select.html#SQL-GROUPBY) 300 | - [STRING_AGG - PostgreSQL](https://www.postgresql.org/docs/current/functions-aggregate.html) 301 | - [GROUP_CONCAT - MySQL](https://dev.mysql.com/doc/refman/8.0/en/aggregate-functions.html#function_group-concat) 302 | -------------------------------------------------------------------------------- /pyspark/06-udfs.md: -------------------------------------------------------------------------------- 1 | # UDFs: User Defined Functions en PySpark 2 | 3 | **Tags**: #pyspark #udfs #custom-functions #performance #real-interview 4 | 5 | --- 6 | 7 | ## TL;DR 8 | 9 | **UDF** = función Python personalizada. Dos tipos: Python UDF (lento, flexible) y Pandas UDF (rápido, recomendado). Python UDF deserializa cada fila (overhead). Pandas UDF usa Apache Arrow (batch processing). Regla: usa Pandas UDF siempre. Python UDF solo si realmente necesitas flexibilidad. 10 | 11 | --- 12 | 13 | ## Concepto Core 14 | 15 | - **Qué es**: UDF permite definir lógica custom que Spark no tiene built-in. Ejecuta en workers 16 | - **Por qué importa**: A veces necesitas transformación que SQL functions no cubren. Pero UDFs pueden ser lentas (serialización overhead) 17 | - **Principio clave**: Prefiere built-in functions > Pandas UDF > Python UDF. Cada "salto" es más lento 18 | 19 | --- 20 | 21 | ## Memory Trick 22 | 23 | **"Envío postal"** — Python UDF = envía cada fila por separado (lento, overhead). Pandas UDF = envía batch de filas (rápido, eficiente). 24 | 25 | --- 26 | 27 | ## Cómo explicarlo en entrevista 28 | 29 | **Paso 1**: "UDF es función Python custom que corre en Spark. Necesaria cuando built-in functions no bastan" 30 | 31 | **Paso 2**: "Hay 2 tipos: Python UDF (flexible pero lento) y Pandas UDF (rápido, recomendado)" 32 | 33 | **Paso 3**: "Python UDF serializa CADA fila (Python ↔ JVM). Overhead gigante. Pandas UDF usa Arrow, batch processing" 34 | 35 | **Paso 4**: "Regla: usa built-in si posible, luego Pandas UDF, último recurso Python UDF" 36 | 37 | --- 38 | 39 | ## Código/Query ejemplo 40 | 41 | ### Escenario: Validar email 42 | 43 | Spark no tiene función "validar email". Necesitas UDF. 44 | 45 | --- 46 | 47 | ### ❌ Opción 1: Python UDF (Lento) 48 | 49 | ```python 50 | from pyspark.sql import SparkSession 51 | from pyspark.sql.functions import udf 52 | from pyspark.sql.types import BooleanType 53 | import re 54 | 55 | spark = SparkSession.builder.appName("UDF Example").getOrCreate() 56 | 57 | # Define función Python 58 | def validate_email(email): 59 | """Validar si email es válido""" 60 | pattern = r'^[a-zA-Z0-9.*%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' 61 | return bool(re.match(pattern, email)) 62 | 63 | # Registra como UDF (Python UDF) 64 | validate_email_udf = udf(validate_email, BooleanType()) 65 | 66 | customers = spark.read.parquet("customers.parquet") 67 | 68 | # Aplica UDF 69 | result = customers.withColumn("email_valid", validate_email_udf(col("email"))) 70 | 71 | result.show() 72 | ``` 73 | 74 | ❌ PROBLEMA: 75 | 76 | - Cada fila: serializa a Python, valida, deserializa resultado 77 | - 1M filas = 1M serializations (lento, overhead CPU+network) 78 | 79 | **Performance:** ~2 minutos para 1M filas 80 | 81 | --- 82 | 83 | ### ✅ Opción 2: Pandas UDF (Rápido - Recomendado) 84 | 85 | ```python 86 | from pyspark.sql.functions import pandas_udf 87 | from pyspark.sql.types import BooleanType 88 | import re 89 | import pandas as pd 90 | 91 | # Define función Pandas (recibe/retorna Series, no fila individual) 92 | @pandas_udf(BooleanType()) 93 | def validate_email_pandas(emails: pd.Series) -> pd.Series: 94 | """Vectorizada: recibe Series de emails, retorna Series de booleans""" 95 | pattern = r'^[a-zA-Z0-9.*%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' 96 | return emails.str.match(pattern) 97 | 98 | customers = spark.read.parquet("customers.parquet") 99 | 100 | # Aplica Pandas UDF 101 | result = customers.withColumn("email_valid", validate_email_pandas(col("email"))) 102 | 103 | result.show() 104 | ``` 105 | 106 | ✅ VENTAJA: 107 | 108 | - Batch processing: 10k filas por batch (no 1 por 1) 109 | - Arrow serialization (eficiente, columnar) 110 | - Pandas optimizado (vectorized operations) 111 | 112 | **Performance:** ~20 segundos para 1M filas (6x más rápido) 113 | 114 | --- 115 | 116 | ### Comparación: Python UDF vs Pandas UDF 117 | 118 | | Aspecto | Python UDF | Pandas UDF | 119 | | ----------------- | --------------------- | -------------------- | 120 | | **Velocidad** | Lento (2 min / 1M) | Rápido (20 seg / 1M) | 121 | | **Batch Size** | 1 fila | ~10k filas | 122 | | **Serialización** | Python + JVM | Arrow (columnar) | 123 | | **Sintaxis** | `udf(func, type)` | `@pandas_udf(type)` | 124 | | **Datos Entrada** | Valor escalar | pandas.Series | 125 | | **Datos Salida** | Valor escalar | pandas.Series | 126 | | **Flexibilidad** | Máxima | Buena | 127 | | **Cuándo Usar** | Rare (último recurso) | Casi siempre | 128 | 129 | --- 130 | 131 | ## UDF por Tipo de Retorno 132 | 133 | ### Retorna Escalar (Series) 134 | 135 | ```python 136 | # Pandas UDF que retorna Series (una columna) 137 | @pandas_udf(StringType()) 138 | def uppercase_col(texts: pd.Series) -> pd.Series: 139 | return texts.str.upper() 140 | 141 | df.withColumn("text_upper", uppercase_col(col("text"))) 142 | ``` 143 | 144 | --- 145 | 146 | ### Retorna Struct (Varias columnas) 147 | 148 | ```python 149 | from pyspark.sql.types import StructType, StructField, StringType, DoubleType 150 | 151 | @pandas_udf( 152 | StructType([ 153 | StructField("name_upper", StringType()), 154 | StructField("length", DoubleType()) 155 | ]) 156 | ) 157 | def process_name(names: pd.Series) -> pd.DataFrame: 158 | return pd.DataFrame({ 159 | "name_upper": names.str.upper(), 160 | "length": names.str.len().astype(float) 161 | }) 162 | 163 | df.withColumn("processed", process_name(col("name"))) 164 | ``` 165 | 166 | --- 167 | 168 | ### Retorna Array 169 | 170 | ```python 171 | @pandas_udf(ArrayType(StringType())) 172 | def split_text(texts: pd.Series) -> pd.Series: 173 | return texts.str.split(" ") 174 | 175 | df.withColumn("words", split_text(col("text"))) 176 | ``` 177 | 178 | --- 179 | 180 | ## UDF Registrados (SQL-accessible) 181 | 182 | ```python 183 | # Registra UDF para usar en SQL 184 | spark.udf.register("validate_email_sql", validate_email_pandas, BooleanType()) 185 | 186 | # Ahora puedes usarlo en SQL 187 | spark.sql(""" 188 | SELECT name, email, validate_email_sql(email) as email_valid 189 | FROM customers 190 | WHERE validate_email_sql(email) = false 191 | """).show() 192 | ``` 193 | 194 | --- 195 | 196 | ## Errores comunes en entrevista 197 | 198 | - **Error**: Usar Python UDF para todo → **Solución**: Pandas UDF siempre que posible. Performance es 5-10x mejor 199 | 200 | - **Error**: No vectorizar en Pandas UDF → **Solución**: `.str`, `.apply()` en pandas, no Python loops 201 | 202 | - **Error**: No especificar tipo de retorno → **Solución**: Siempre declara tipo explícitamente (BooleanType(), StringType(), etc.) 203 | 204 | - **Error**: Usar UDF cuando built-in function existe → **Solución**: Spark funciones built-in son optimizadas (Catalyst). UDF bypassa optimizer 205 | 206 | --- 207 | 208 | ## Built-in vs UDF: Cuándo Cada Uno 209 | 210 | ```python 211 | from pyspark.sql.functions import col, regexp_replace, upper, length 212 | 213 | # ✅ Usa BUILT-IN (rápido, optimizado) 214 | df.withColumn("email_normalized", upper(col("email"))) 215 | df.withColumn("text_clean", regexp_replace(col("text"), r'\s+', ' ')) 216 | df.withColumn("name_length", length(col("name"))) 217 | 218 | # ❌ UDF solo si no existe built-in 219 | # Ej: reglas de negocio custom, lógica compleja 220 | ``` 221 | 222 | --- 223 | 224 | ## Performance Tips 225 | 226 | ✅ BIEN: Pandas UDF vectorizado 227 | 228 | ```python 229 | @pandas_udf(BooleanType()) 230 | def is_valid(emails: pd.Series) -> pd.Series: 231 | return emails.str.match(PATTERN) # Vectorizado 232 | ``` 233 | 234 | ❌ MAL: Python loop (derrota propósito de Pandas UDF) 235 | 236 | ```python 237 | @pandas_udf(BooleanType()) 238 | def is_valid_slow(emails: pd.Series) -> pd.Series: 239 | return pd.Series([bool(re.match(PATTERN, e)) for e in emails]) # ← Loop 240 | ``` 241 | 242 | Primer es 100x más rápido 243 | 244 | --- 245 | 246 | ## Preguntas de seguimiento típicas 247 | 248 | 1. **"¿Por qué Pandas UDF es más rápido?"** 249 | - Batch processing (10k filas a la vez) 250 | - Arrow serialization (columnar, comprimido) 251 | - Menos context switching Python ↔ JVM 252 | 253 | 2. **"¿Cuándo NO usarías UDF?"** 254 | - Si existe built-in function 255 | - Si lógica es muy simple (usa SQL expressions) 256 | - Si performance es crítico (última opción) 257 | 258 | 3. **"¿Cómo debuggeas UDF?"** 259 | - `.show()` directamente 260 | - Agrega `print()` en función (ojo: salida va a worker logs, no local) 261 | - Usa `try/except` para capturar errores 262 | 263 | 4. **"¿UDF puede tener state?"** 264 | - No, cada batch es independiente 265 | - Si necesitas state, usa `groupBy().applyInPandas()` (más complejo) 266 | 267 | --- 268 | 269 | ## Real-World: Data Validation Pipeline 270 | 271 | ```python 272 | from pyspark.sql.functions import pandas_udf, col 273 | from pyspark.sql.types import StructType, StructField, StringType, BooleanType 274 | import pandas as pd 275 | 276 | # Multi-validation UDF 277 | @pandas_udf( 278 | StructType([ 279 | StructField("is_valid", BooleanType()), 280 | StructField("error", StringType()) 281 | ]) 282 | ) 283 | def validate_customer(names: pd.Series, emails: pd.Series) -> pd.DataFrame: 284 | results = pd.DataFrame({ 285 | "is_valid": True, 286 | "error": "" 287 | }, index=names.index) 288 | 289 | # Validar nombre 290 | invalid_names = names.isna() | (names.str.len() < 2) 291 | results.loc[invalid_names, "is_valid"] = False 292 | results.loc[invalid_names, "error"] = "Invalid name" 293 | 294 | # Validar email 295 | invalid_emails = ~emails.str.match(r'^[\w\.-]+@[\w\.-]+\.\w+$') 296 | results.loc[invalid_emails, "is_valid"] = False 297 | results.loc[invalid_emails, "error"] = "Invalid email" 298 | 299 | return results 300 | 301 | # Aplica 302 | customers = spark.read.parquet("customers/") 303 | validated = customers.withColumn( 304 | "validation", 305 | validate_customer(col("name"), col("email")) 306 | ) 307 | 308 | # Explota struct 309 | validated.select("*", "validation.*").show() 310 | ``` 311 | 312 | --- 313 | 314 | ## References 315 | 316 | - [UDFs - PySpark Docs](https://spark.apache.org/docs/latest/api/python/reference/pyspark.sql/functions.html#udf) 317 | - [Performance Guide - Spark](https://spark.apache.org/docs/latest/sql-performance-tuning.html) 318 | -------------------------------------------------------------------------------- /streaming/02-stream-processing-windowing.md: -------------------------------------------------------------------------------- 1 | # Procesamiento de Streams: Windowing y Gestión de Estado 2 | 3 | **Tags**: #streaming #windowing #state #aggregation #spark-streaming #real-interview 4 | 5 | --- 6 | 7 | ## TL;DR 8 | 9 | **Stream processing** = procesar flujos infinitos de eventos en tiempo real. **Windowing** = agrupar eventos por tiempo (ventana de 5 minutos, 1 hora). **Aggregation** = contar, sumar dentro de la ventana. **State** = mantener contexto (sesión de usuario, total del pedido). **Desafío**: Manejar eventos tardíos, datos fuera de orden, límites de ventana. 10 | 11 | --- 12 | 13 | ## Concepto Core 14 | 15 | - **Qué es**: Windowing = "dividir el tiempo en trozos", agregar dentro de cada trozo 16 | - **Por qué importa**: Las analíticas en tiempo real necesitan ventanas (ventas de 5 minutos, errores por hora) 17 | - **Principio clave**: El tiempo es complicado en streaming (tiempo de evento vs tiempo de procesamiento) 18 | 19 | --- 20 | 21 | ## Tipos de Ventanas 22 | 23 | Tipo 1: TUMBLING (Fija, no superpuesta) 24 | ├─ Tamaño de ventana: 5 minutos 25 | ├─ Uso: Ventas por hora, conteos diarios de usuarios 26 | 27 | Tipo 2: SLIDING (Fija, superpuesta) 28 | ├─ Tamaño de ventana: 5 minutos 29 | ├─ Deslizamiento: 1 minuto (cada minuto, nueva ventana) 30 | ├─ Uso: Promedio móvil, métricas en tiempo real 31 | 32 | Tipo 3: SESSION (Dinámica, basada en usuario) 33 | ├─ Brecha de inactividad: 10 minutos 34 | ├─ [Usuario A clic 00:00] → Iniciar sesión 35 | ├─ [Usuario A clic 00:02] → Extender sesión 36 | ├─ [Usuario A sin actividad > 10 min] → Finalizar sesión 37 | ├─ [Usuario A clic 00:15] → NUEVA sesión 38 | └─ Longitud de sesión variable 39 | 40 | Tipo 4: DELAY (Después del plazo, actualización) 41 | ├─ Ventana se cierra en tiempo T 42 | ├─ Pero permite eventos tardíos hasta tiempo T + 10 min 43 | ├─ Actualiza resultados a medida que llegan datos tardíos 44 | └─ Resultado final después del período de gracia 45 | 46 | --- 47 | 48 | ## Ejemplos de Windowing en SQL 49 | 50 | ```sql 51 | -- Ventana de Tumbling: agregación de ventas de 5 minutos 52 | SELECT 53 | TUMBLE_START(event_time, INTERVAL '5' MINUTE) as window_start, 54 | TUMBLE_END(event_time, INTERVAL '5' MINUTE) as window_end, 55 | product_id, 56 | COUNT(*) as num_sales, 57 | SUM(amount) as revenue 58 | FROM orders 59 | GROUP BY TUMBLE(event_time, INTERVAL '5' MINUTE), product_id; 60 | 61 | -- Resultado: 62 | -- window_start window_end product_id num_sales revenue 63 | -- 2024-01-15 10:00:00 2024-01-15 10:05:00 1 100 1000 64 | -- 2024-01-15 10:05:00 2024-01-15 10:10:00 1 110 1100 65 | ``` 66 | 67 | ```sql 68 | -- Ventana de Sliding: promedio móvil de 1 hora (calculado cada 10 min) 69 | SELECT 70 | HOP_START(event_time, INTERVAL '10' MINUTE, INTERVAL '1' HOUR) as window_start, 71 | HOP_END(event_time, INTERVAL '10' MINUTE, INTERVAL '1' HOUR) as window_end, 72 | AVG(latency_ms) as avg_latency 73 | FROM requests 74 | GROUP BY HOP(event_time, INTERVAL '10' MINUTE, INTERVAL '1' HOUR); 75 | 76 | -- Resultado: Actualizado cada 10 min con promedio de 1 hora 77 | ``` 78 | 79 | ```sql 80 | -- Ventana de Sesión: comportamiento del usuario 81 | SELECT 82 | SESSION_START(event_time, INTERVAL '10' MINUTE) as session_start, 83 | SESSION_END(event_time, INTERVAL '10' MINUTE) as session_end, 84 | user_id, 85 | COUNT(*) as num_events, 86 | ARRAY_AGG(event_type) as event_sequence 87 | FROM user_events 88 | GROUP BY SESSION(event_time, INTERVAL '10' MINUTE), user_id; 89 | 90 | -- Resultado: 91 | -- session_start session_end user_id num_events event_sequence 92 | -- 2024-01-15 10:00:00 2024-01-15 10:12:00 1 5 ['click','add_cart','checkout','pay','confirm'] 93 | -- 2024-01-15 10:30:00 2024-01-15 10:35:00 1 2 ['login','view_profile'] 94 | ``` 95 | 96 | --- 97 | 98 | ## Semántica del Tiempo 99 | 100 | Concepto clave: 3 tiempos diferentes en streaming 101 | 102 | TIEMPO DE EVENTO 103 | └─ Cuándo ocurrió el evento 104 | └─ Ejemplo: Procesamiento del pago a las 10:05:32 105 | 106 | TIEMPO DE PROCESAMIENTO 107 | └─ Cuándo el evento llega a Kafka 108 | └─ Ejemplo: Llega a las 10:05:35 (3 segundos después) 109 | 110 | WATERMARK 111 | └─ "Hemos visto todos los eventos hasta el tiempo X" 112 | └─ Ejemplo: Marca de agua a las 10:05:00 = no hay más eventos antes de 10:05:00 113 | └─ Señales: Ventana completa, tiempo de emitir resultados 114 | 115 | Cronología: 116 | Evento ocurre: 10:05:32 (tiempo de evento) 117 | Evento enviado: 10:05:33 118 | Evento llega a Kafka: 10:05:35 (tiempo de procesamiento) 119 | Latencia: 3 segundos 120 | 121 | La ventana debe usar TIEMPO DE EVENTO (no tiempo de procesamiento) 122 | De lo contrario, los resultados dependen de los retrasos de la red, no de los eventos reales 123 | 124 | --- 125 | 126 | ## Gestión de Estado 127 | 128 | Desafío: Algunas operaciones necesitan CONTEXTO 129 | Simple (sin estado): Contar eventos 130 | Resultado: 5 eventos en la ventana 131 | Con estado: Sesión de usuario 132 | Necesidad: ¿Qué eventos pertenecen a la misma sesión? 133 | 134 | ```python 135 | from pyspark.sql.functions import col, window, session_window 136 | from pyspark.sql import SparkSession 137 | 138 | spark = SparkSession.builder.appName("session_tracking").getOrCreate() 139 | 140 | # Stream: user_events 141 | df = spark.readStream 142 | .format("kafka") 143 | .option("kafka.bootstrap.servers", "localhost:9092") 144 | .option("subscribe", "user_events") 145 | .load() 146 | 147 | # Parsear eventos 148 | events = df.select( 149 | col("value").cast("string"), 150 | col("timestamp").cast("timestamp").alias("event_time") 151 | ) 152 | 153 | # Ventana de sesión: 10 min de brecha de inactividad 154 | sessions = events 155 | .groupBy( 156 | session_window(col("event_time"), "10 minutes"), 157 | col("user_id") 158 | ) 159 | .agg( 160 | count("*").alias("num_events"), 161 | collect_list("event_type").alias("event_sequence") 162 | ) 163 | 164 | # Resultado: Por cada usuario, por sesión: 165 | # ├─ Inicio/fin de sesión 166 | # ├─ Número de eventos 167 | # └─ Secuencia de eventos 168 | ``` 169 | 170 | Implicaciones de estado: 171 | ├─ Debe mantener el estado del usuario a través de ventanas 172 | ├─ Memoria: O(# de sesiones activas) 173 | └─ Tiempo de espera: Limpiar después de 10+ minutos de inactividad 174 | 175 | --- 176 | 177 | ## Manejo de Datos Tardíos 178 | 179 | ```python 180 | from pyspark.sql.functions import col, window 181 | 182 | Escenario: Evento retrasado en tránsito 183 | Evento 1: Pedido realizado a las 10:05:32, llega a las 10:05:35 184 | Evento 2: Pedido realizado a las 10:06:15, llega a las 10:06:18 185 | Evento 3: Pedido realizado a las 10:05:58, llega a las 10:07:00 (¡TARDE!) 186 | 187 | Ventana: 188 | ├─ Eventos esperados: Evento 1, Evento 2 189 | ├─ Evento 3 llega DESPUÉS de que la ventana se cerró 190 | ├─ Decisión: ¿Ignorar o actualizar? 191 | Enfoque de Spark: Permitir actualización si está dentro del período de gracia 192 | query = df 193 | .groupBy( 194 | window(col("event_time"), "5 minutes", "1 minute"), # Ventana deslizante 195 | col("product_id") 196 | ) 197 | .agg(count("*").alias("num_orders")) 198 | .writeStream 199 | .outputMode("update") # Emitir actualizaciones (no solo añadir) 200 | .option("watermarkDelayThreshold", "10 minutes") # Período de gracia 201 | .start() 202 | 203 | Cronología: 204 | 10:05:35 - Evento 1 llega → 205 | 10:06:18 - Evento 2 llega → 206 | 10:07:00 - Evento 3 (TARDE) llega → 207 | 10:15:00 - Marca de agua pasa 10:15 → 208 | Modos de salida: 209 | ├─ append: Solo nuevas filas (ventanas asentadas) 210 | ├─ update: Filas modificadas (datos tardíos) 211 | └─ complete: Todas las filas (recomputado) 212 | ``` 213 | 214 | --- 215 | 216 | ## Caso Real: Dashboard en Tiempo Real 217 | 218 | ```sql 219 | -- Stream: user_events (clics, compras, etc.) 220 | -- Tarea: Dashboard de 5 minutos (eventos, ingresos, productos principales) 221 | 222 | CREATE TABLE dashboard_5min AS 223 | SELECT 224 | TUMBLE_START(event_time, INTERVAL '5' MINUTE) as window_time, 225 | COUNT(*) as total_events, 226 | COUNT(CASE WHEN event_type = 'purchase' THEN 1 END) as num_purchases, 227 | SUM(CASE WHEN event_type = 'purchase' THEN amount ELSE 0 END) as revenue, 228 | AVG(session_duration_sec) as avg_session_duration, 229 | ARRAY_AGG(DISTINCT product_id ORDER BY COUNT(*) DESC LIMIT 5) as top_5_products 230 | FROM user_events 231 | WHERE event_time > CURRENT_TIMESTAMP - INTERVAL '1' HOUR 232 | GROUP BY TUMBLE(event_time, INTERVAL '5' MINUTE); 233 | 234 | -- Resultado se actualiza cada 5 minutos: 235 | -- window_time total_events num_purchases revenue avg_session_duration top_5_products 236 | -- 2024-01-15 10:00:00 50000 5000 50000 420 237 | -- 2024-01-15 10:05:00 52000 5200 52000 425 238 | -- 2024-01-15 10:10:00 51000 5100 51000 422 239 | ``` 240 | 241 | --- 242 | 243 | ## Errores Comunes en Entrevista 244 | 245 | - **Error**: Windowing por TIEMPO DE PROCESAMIENTO en lugar de TIEMPO DE EVENTO → **Solución**: Siempre usar tiempo de evento (los retrasos de red sesgan los resultados) 246 | 247 | - **Error**: Ignorar datos tardíos → **Solución**: Los sistemas reales tienen eventos retrasados, deben manejarse con gracia 248 | 249 | - **Error**: No limpiar estado (fuga de memoria) → **Solución**: Establecer tiempo de espera = TTL para el estado de la sesión 250 | 251 | - **Error**: Ventanas TUMBLING superpuestas → **Solución**: Tumbling = no superpuestas por definición. Si necesitas superposición, usa Sliding 252 | 253 | --- 254 | 255 | ## Preguntas de Seguimiento Típicas 256 | 257 | 1. **"¿Diferencia entre Tumbling y Sliding?"** 258 | - Tumbling: No superpuestas (eficiente) 259 | - Sliding: Superpuestas (más suave, más computación) 260 | 261 | 2. **"¿Cómo manejas eventos fuera de orden?"** 262 | - Windowing por tiempo de evento (no tiempo de procesamiento) 263 | - Período de gracia permite llegadas tardías 264 | - Marca de agua señala el cierre de la ventana 265 | 266 | 3. **"¿Implementación de ventana de sesión?"** 267 | - Rastrear: último tiempo de evento por usuario 268 | - Nuevo evento: Si hay brecha > umbral de inactividad = nueva sesión 269 | - De lo contrario: Extender sesión existente 270 | 271 | 4. **"¿Explosión de estado?"** 272 | - Monitorear: Número de sesiones activas 273 | - Limpieza: Eliminar sesiones inactivas > tiempo de espera 274 | - Escalabilidad: Mover a tienda de estado distribuida (Redis, RocksDB) 275 | 276 | --- 277 | 278 | ## Referencias 279 | 280 | - [Watermarks & Grace Period - Beam](https://beam.apache.org/documentation/programming-guide/#watermarks-and-late-data) 281 | -------------------------------------------------------------------------------- /pyspark/07-spark-sql.md: -------------------------------------------------------------------------------- 1 | # Spark SQL: Queries SQL en PySpark 2 | 3 | **Tags**: #pyspark #spark-sql #sql #real-interview 4 | 5 | --- 6 | 7 | ## TL;DR 8 | 9 | **Spark SQL** = escribir SQL directamente en Spark. Registra DataFrame como tabla temporal con `.createOrReplaceTempView()`, luego usa `spark.sql("SELECT ...")`. Spark SQL y DataFrame API usan mismo optimizer (Catalyst). Elige SQL si SQL devs, elige API si Python devs. Performance igual. 10 | 11 | --- 12 | 13 | ## Concepto Core 14 | 15 | - **Qué es**: Spark SQL permite escribir SQL en lugar de DataFrame API. Ambos se compilan al mismo plan de ejecución (Catalyst) 16 | - **Por qué importa**: SQL devs pueden contribuir sin aprender Spark API. Algunos queries son más fáciles en SQL (joins complejos, window functions) 17 | - **Principio clave**: SQL y API son equivalentes. Elige por preferencia/legibilidad, no performance 18 | 19 | --- 20 | 21 | ## Memory Trick 22 | 23 | **"Dos idiomas, un motor"** — SQL y DataFrame API hablan diferente pero usan mismo motor (Catalyst). El result es idéntico. 24 | 25 | --- 26 | 27 | ## Cómo explicarlo en entrevista 28 | 29 | **Paso 1**: "Spark SQL permite escribir SQL en lugar de DataFrame transformations" 30 | 31 | **Paso 2**: "Registra DataFrame como tabla temporal: `.createOrReplaceTempView()`" 32 | 33 | **Paso 3**: "Luego `spark.sql('SELECT ...')` como si fuera base de datos SQL" 34 | 35 | **Paso 4**: "Performance es igual a DataFrame API porque Catalyst optimiza ambos" 36 | 37 | --- 38 | 39 | ## Código/Query ejemplo 40 | 41 | ### DataFrame API vs Spark SQL 42 | 43 | **Ambos hacen lo mismo:** 44 | 45 | ```python 46 | from pyspark.sql import SparkSession 47 | from pyspark.sql.functions import col, sum, avg 48 | 49 | spark = SparkSession.builder.appName("Spark SQL Example").getOrCreate() 50 | 51 | orders = spark.read.parquet("orders.parquet") 52 | customers = spark.read.parquet("customers.parquet") 53 | 54 | # ===== OPCIÓN 1: DataFrame API ===== 55 | result_api = ( 56 | orders 57 | .join(customers, "customer_id") 58 | .filter(col("order_amount") > 100) 59 | .groupBy("customer_id", "name") 60 | .agg( 61 | sum("order_amount").alias("total_spent"), 62 | avg("order_amount").alias("avg_order") 63 | ) 64 | .orderBy(col("total_spent").desc()) 65 | ) 66 | 67 | result_api.show() 68 | 69 | # ===== OPCIÓN 2: Spark SQL ===== 70 | # Primero: registra DataFrames como tablas temporales 71 | orders.createOrReplaceTempView("orders_temp") 72 | customers.createOrReplaceTempView("customers_temp") 73 | 74 | # Luego: SQL query 75 | result_sql = spark.sql(""" 76 | SELECT 77 | o.customer_id, 78 | c.name, 79 | SUM(o.order_amount) as total_spent, 80 | AVG(o.order_amount) as avg_order 81 | FROM orders_temp o 82 | JOIN customers_temp c ON o.customer_id = c.customer_id 83 | WHERE o.order_amount > 100 84 | GROUP BY o.customer_id, c.name 85 | ORDER BY total_spent DESC 86 | """) 87 | 88 | result_sql.show() 89 | 90 | # Performance: IDÉNTICO (mismo optimizer) 91 | # Elige por legibilidad/preferencia 92 | ``` 93 | 94 | --- 95 | 96 | ## Registrar Tablas Temporales 97 | 98 | ### Temporary Views (sesión actual) 99 | 100 | ```python 101 | # Scope: Solo esta sesión 102 | df.createOrReplaceTempView("my_table") 103 | 104 | # SQL access 105 | result = spark.sql("SELECT * FROM my_table") 106 | ``` 107 | 108 | --- 109 | 110 | ### Global Temporary Views (todos drivers) 111 | 112 | ```python 113 | # Scope: Todos drivers conectados a cluster 114 | df.createGlobalTempView("my_global_table") 115 | 116 | # SQL access (prefix global_temp) 117 | result = spark.sql("SELECT * FROM global_temp.my_global_table") 118 | ``` 119 | 120 | --- 121 | 122 | ### Permanent Tables (persisten) 123 | 124 | ```python 125 | # Scope: Persistente en metastore (incluso después de Spark stop) 126 | df.write.mode("overwrite").saveAsTable("my_permanent_table") 127 | 128 | # SQL access 129 | result = spark.sql("SELECT * FROM my_permanent_table") 130 | 131 | # Permanece después de: 132 | spark.stop() 133 | 134 | # ... restart Spark 135 | spark.sql("SELECT * FROM my_permanent_table") # Aún existe! 136 | ``` 137 | 138 | --- 139 | 140 | ## Spark SQL vs DataFrame: Cuándo Cada Uno 141 | 142 | ### ✅ Usa Spark SQL si: 143 | 144 | - SQL devs en tu team 145 | - Query es complejo (múltiples joins, CTEs) 146 | - Ya tienes SQL escrita (legacy) 147 | - Te sientes más cómodo con SQL 148 | 149 | ```python 150 | # SQL: CTEs, window functions, complejas joins 151 | spark.sql(""" 152 | WITH ranked AS ( 153 | SELECT *, ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC) as rank 154 | FROM products 155 | ) 156 | SELECT * FROM ranked WHERE rank <= 3 157 | """) 158 | ``` 159 | 160 | --- 161 | 162 | ### ✅ Usa DataFrame API si: 163 | 164 | - Python devs puro 165 | - Lógica con UDFs/custom functions 166 | - Necesitas debuggear paso a paso 167 | - Data engineering workflow (transformaciones complejas) 168 | 169 | ```python 170 | # API: UDFs, custom logic, debuggeable 171 | from pyspark.sql.functions import row_number, col, window 172 | 173 | products = spark.read.parquet("products/") 174 | window_spec = window.partitionBy("category").orderBy(col("sales").desc()) 175 | 176 | result = ( 177 | products 178 | .withColumn("rank", row_number().over(window_spec)) 179 | .filter(col("rank") <= 3) 180 | ) 181 | ``` 182 | 183 | --- 184 | 185 | ## Complex Examples: SQL vs API 186 | 187 | ### Ejemplo 1: Window Functions + CTEs 188 | 189 | **SQL (más legible):** 190 | 191 | ```sql 192 | WITH ranked_sales AS ( 193 | SELECT 194 | product_id, 195 | category, 196 | sales, 197 | ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC) as rank 198 | FROM products 199 | ) 200 | SELECT * FROM ranked_sales WHERE rank <= 3 201 | ``` 202 | 203 | **API (más verboso):** 204 | 205 | ```python 206 | from pyspark.sql.functions import row_number, col, window 207 | 208 | window_spec = window.partitionBy("category").orderBy(col("sales").desc()) 209 | result = ( 210 | products 211 | .withColumn("rank", row_number().over(window_spec)) 212 | .filter(col("rank") <= 3) 213 | .select("product_id", "category", "sales", "rank") 214 | ) 215 | ``` 216 | 217 | --- 218 | 219 | ### Ejemplo 2: Multiple Joins 220 | 221 | **SQL (más claro):** 222 | 223 | ```sql 224 | SELECT 225 | o.order_id, 226 | c.customer_name, 227 | p.product_name, 228 | s.supplier_name, 229 | o.amount 230 | FROM orders o 231 | JOIN customers c ON o.customer_id = c.customer_id 232 | JOIN products p ON o.product_id = p.product_id 233 | JOIN suppliers s ON p.supplier_id = s.supplier_id 234 | WHERE o.date >= '2024-01-01' 235 | ``` 236 | 237 | **API (más pasos):** 238 | 239 | ```python 240 | from pyspark.sql.functions import col 241 | 242 | result = ( 243 | orders 244 | .join(customers, "customer_id") 245 | .join(products, "product_id") 246 | .join(suppliers, "supplier_id") 247 | .filter(col("date") >= "2024-01-01") 248 | .select("order_id", "customer_name", "product_name", "supplier_name", "amount") 249 | ) 250 | ``` 251 | 252 | --- 253 | 254 | ## Explain Plans: Validar Optimización 255 | 256 | ```python 257 | # Spark SQL 258 | spark.sql(""" 259 | SELECT * FROM orders WHERE amount > 100 260 | """).explain(mode="extended") 261 | 262 | # DataFrame API (same result) 263 | orders.filter(col("amount") > 100).explain(mode="extended") 264 | 265 | # Output (identical): 266 | # == Optimized Logical Plan == 267 | # Filter (amount#1 > 100) 268 | # +- Relation[id#0, amount#1, ...] 269 | # == Physical Plan == 270 | # Filter (amount#1 > 100) 271 | # +- FileScan parquet [id#0, amount#1, ...] 272 | ``` 273 | 274 | Catalyst optimiza ambos idénticamente. 275 | 276 | --- 277 | 278 | ## Interoperabilidad: SQL ↔ DataFrame 279 | 280 | ```python 281 | # Partir de SQL, retornar DataFrame 282 | sql_result = spark.sql("SELECT * FROM orders WHERE amount > 100") 283 | 284 | # Aplicar API transformations 285 | final = sql_result.withColumn("tax", col("amount") * 0.1) 286 | 287 | # Back to SQL 288 | final.createOrReplaceTempView("orders_with_tax") 289 | spark.sql("SELECT * FROM orders_with_tax") 290 | 291 | # Mezclar libremente 292 | ``` 293 | 294 | --- 295 | 296 | ## Errores comunes en entrevista 297 | 298 | - **Error**: Pensar que SQL es más lento que API → **Solución**: Performance es idéntico (Catalyst optimiza ambos) 299 | 300 | - **Error**: Usar `createTempView()` sin verificar existencia → **Solución**: Usa `createOrReplaceTempView()` para evitar errors 301 | 302 | - **Error**: No limpiar tablas temporales → **Solución**: Se limpian automáticamente al cerrar sesión, pero buena práctica: `spark.sql("DROP TABLE IF EXISTS my_table")` 303 | 304 | - **Error**: Mezclar temporary + permanent views sin entender scope → **Solución**: Temporary = sesión actual. Permanent = persistente. Elige según necesidad 305 | 306 | --- 307 | 308 | ## Preguntas de seguimiento típicas 309 | 310 | 1. **"¿Cuándo usarías Spark SQL vs DataFrame API?"** 311 | - SQL: SQL devs, queries complejas, legibilidad 312 | - API: Python devs, custom logic, debuggeable 313 | 314 | 2. **"¿Diferencia entre createTempView y createOrReplaceTempView?"** 315 | - `createTempView`: Error si tabla existe 316 | - `createOrReplaceTempView`: Reemplaza silenciosamente (safe) 317 | 318 | 3. **"¿Performance de Spark SQL vs DataFrame API?"** 319 | - Idéntico. Catalyst optimiza ambos 320 | 321 | 4. **"¿Cómo pasas resultados entre SQL y API?"** 322 | - SQL → DataFrame: `spark.sql(...)` 323 | - DataFrame → SQL: `.createOrReplaceTempView()` 324 | - Mezcla libremente 325 | 326 | --- 327 | 328 | ## Real-World: ETL Pipeline Mixta 329 | 330 | ```python 331 | # Leer datos 332 | raw_events = spark.read.parquet("raw_events/") 333 | 334 | # Limpieza: API (más control) 335 | cleaned = raw_events.filter(col("timestamp").isNotNull()) 336 | 337 | # Agregación: SQL (más legible) 338 | cleaned.createOrReplaceTempView("cleaned_events") 339 | 340 | result = spark.sql(""" 341 | SELECT 342 | DATE(timestamp) as event_date, 343 | event_type, 344 | COUNT(*) as count, 345 | COUNT(DISTINCT user_id) as unique_users 346 | FROM cleaned_events 347 | WHERE event_type IN ('purchase', 'view') 348 | GROUP BY DATE(timestamp), event_type 349 | ORDER BY event_date DESC, count DESC 350 | """) 351 | 352 | # Agregación adicional: API 353 | final = result.withColumn("daily_total", 354 | sum("count").over(Window.partitionBy("event_date"))) 355 | 356 | # Guardar 357 | final.write.parquet("output/daily_summary") 358 | ``` 359 | 360 | --- 361 | 362 | ## References 363 | 364 | - [Spark SQL - PySpark Docs](https://spark.apache.org/docs/latest/api/python/reference/pyspark.sql/index.html) 365 | - [Temporary Views - Spark Docs](https://spark.apache.org/docs/latest/sql-data-sources.html#temporary-views) 366 | - [Catalyst Optimizer - Spark Architecture](https://spark.apache.org/docs/latest/sql-performance-tuning.html) 367 | -------------------------------------------------------------------------------- /pyspark/01-rdd-vs-dataframe.md: -------------------------------------------------------------------------------- 1 | # RDD vs DataFrame: Cuándo Usar Cada Uno 2 | 3 | **Tags**: #pyspark #rdd #dataframe #architecture #fundamental #real-interview 4 | 5 | --- 6 | 7 | ## TL;DR 8 | 9 | **RDD** (Resilient Distributed Dataset) = bajo nivel, unstructured, flexible. **DataFrame** = alto nivel, structured (SQL), optimizado. En 99% de casos: **usa DataFrame**. RDD solo si datos son realmente unstructured o necesitas transformaciones muy específicas. 10 | 11 | --- 12 | 13 | ## Concepto Core 14 | 15 | - **Qué es**: RDD es la abstracción más baja en Spark (colecciones distribuidas). DataFrame es RDD con schema (estructura conocida) 16 | - **Por qué importa**: Elegir mal entre RDD y DataFrame = performance disaster. Demuestra comprensión arquitectónico 17 | - **Principio clave**: DataFrame > RDD siempre (a menos que tengas buena razón). Spark Catalyst optimizer trabaja con DataFrames 18 | 19 | --- 20 | 21 | ## Memory Trick 22 | 23 | **"Escalera de abstracción"**: 24 | 25 | - RDD = Lego blocks sueltos (máxima flexibilidad, máximo trabajo manual) 26 | - DataFrame = Casas pre-construidas (menos flexible, súper rápido) 27 | - Spark SQL = Ciudades enteras (SQL puro, máxima optimización) 28 | 29 | --- 30 | 31 | ## Cómo explicarlo en entrevista 32 | 33 | **Paso 1**: "RDD es la abstracción más baja. Cada elemento es un objeto Python/Java, zero schema" 34 | 35 | **Paso 2**: "DataFrame es RDD estructurado. Tiene columnas nombradas, tipos conocidos, schema" 36 | 37 | **Paso 3**: "DataFrame usa Catalyst optimizer (Spark entiende qué haces y optimiza). RDD no" 38 | 39 | **Paso 4**: "Conclusión: Usa DataFrame 99% del tiempo. RDD solo si datos son realmente unstructured" 40 | 41 | --- 42 | 43 | ## Código/Query ejemplo 44 | 45 | ### Escenario: Leer y procesar logs 46 | 47 | **Logs sin estructura:** 48 | 2024-01-15 10:23:45 ERROR User 123 failed login attempt 49 | 2024-01-15 10:24:12 INFO User 456 logged in successfully 50 | 2024-01-15 10:25:03 WARNING High memory usage detected 51 | 52 | --- 53 | 54 | ### ❌ Opción 1: RDD (Flexible pero lento) 55 | 56 | ```python 57 | from pyspark import SparkContext 58 | 59 | sc = SparkContext("local", "RDD Example") 60 | 61 | # Leer como RDD (cada línea es un string) 62 | logs_rdd = sc.textFile("logs.txt") 63 | 64 | # Transformación 1: Parse log line 65 | def parse_log(line): 66 | parts = line.split() 67 | return { 68 | 'timestamp': f"{parts[0]} {parts[1]}", 69 | 'level': parts[2], 70 | 'message': ' '.join(parts[3:]) 71 | } 72 | 73 | parsed_rdd = logs_rdd.map(parse_log) 74 | 75 | # Transformación 2: Filter errores 76 | errors_rdd = parsed_rdd.filter(lambda log: log['level'] == 'ERROR') 77 | 78 | # Acción: Collect 79 | errors_list = errors_rdd.collect() 80 | print(errors_list) 81 | ``` 82 | 83 | **Problemas**: 84 | 85 | - Cada `map()` = conversión Python → objeto genérico → Python (overhead) 86 | - Spark NO SABE que hay columnas "level", "message" 87 | - NO hay optimización automática 88 | - `collect()` retorna a driver (puede ser huge) 89 | 90 | --- 91 | 92 | ### ✅ Opción 2: DataFrame (Recomendado) 93 | 94 | ```python 95 | from pyspark.sql import SparkSession 96 | from pyspark.sql.functions import col, split, concat_ws 97 | from pyspark.sql.types import StructType, StructField, StringType 98 | 99 | spark = SparkSession.builder.appName("DataFrame Example").getOrCreate() 100 | 101 | # Define schema explícito 102 | schema = StructType([ 103 | StructField("timestamp", StringType(), True), 104 | StructField("level", StringType(), True), 105 | StructField("message", StringType(), True) 106 | ]) 107 | 108 | # Leer y parsear con schema 109 | logs_df = ( 110 | spark.read.text("logs.txt") 111 | .select( 112 | split(col("value"), r"\s+").getItem(0).alias("date"), 113 | split(col("value"), r"\s+").getItem(1).alias("time"), 114 | split(col("value"), r"\s+").getItem(2).alias("level"), 115 | concat_ws(" ", split(col("value"), r"\s+").getItem(3), 116 | split(col("value"), r"\s+").getItem(4), 117 | split(col("value"), r"\s+").getItem(5), 118 | split(col("value"), r"\s+").getItem(6)).alias("message") 119 | ) 120 | .withColumn("timestamp", concat_ws(" ", col("date"), col("time"))) 121 | .select("timestamp", "level", "message") 122 | ) 123 | 124 | # Filter: Spark optimiza automáticamente 125 | errors_df = logs_df.filter(col("level") == "ERROR") 126 | 127 | # Show (no colapsas a driver como collect) 128 | errors_df.show() 129 | ``` 130 | 131 | **Ventajas**: 132 | 133 | - Spark SABE qué columnas tienes 134 | - Catalyst optimizer puede predicate pushdown 135 | - `.show()` es lazy (no trae TODO a driver) 136 | - Interoperable con Spark SQL 137 | 138 | --- 139 | 140 | ### Comparación: RDD vs DataFrame (Mismo resultado, diferente performance) 141 | 142 | ❌ RDD: 1000 segundos (sin optimización) 143 | 144 | ```python 145 | errors_count_rdd = parsed_rdd.filter( 146 | lambda log: log['level'] == 'ERROR' 147 | ).count() 148 | ``` 149 | 150 | ✅ DataFrame: 50 segundos (con optimizer) 151 | 152 | ```python 153 | errors_count_df = logs_df.filter( 154 | col("level") == "ERROR" 155 | ).count() 156 | ``` 157 | 158 | Catalyst optimizer hizo 20x más rápido 159 | 160 | --- 161 | 162 | ## Diferencias Clave: RDD vs DataFrame 163 | 164 | | Aspecto | RDD | DataFrame | Ganador | 165 | | --------------------- | ------------ | ------------------ | --------- | 166 | | **Schema** | No | Sí | DataFrame | 167 | | **Performance** | Lento | Rápido (optimizer) | DataFrame | 168 | | **SQL** | No | Sí | DataFrame | 169 | | **Type Safety** | Débil | Fuerte | DataFrame | 170 | | **Flexibilidad** | Máxima | Media | RDD | 171 | | **Casos de Uso** | Unstructured | Structured | Depende | 172 | | **Curva aprendizaje** | Difícil | Fácil | DataFrame | 173 | 174 | --- 175 | 176 | ## ¿Cuándo Usar RDD? 177 | 178 | **Usa RDD solo si:** 179 | 180 | 1. **Datos realmente unstructured** (binary, irregular) 181 | Ejemplo: Imágenes, audio, binarios 182 | 183 | ```python 184 | images_rdd = sc.binaryFiles("images/").map(lambda x: x) 185 | ``` 186 | 187 | 2. **Necesitas transformaciones muy específicas** 188 | Ejemplo: Custom encoding/decoding 189 | 190 | ```python 191 | encoded_rdd = data_rdd.map(custom_encryption_function) 192 | ``` 193 | 194 | 3. **Optimización extrema de memoria** (rare) 195 | Ejemplo: Comprimir antes de serializar 196 | ```python 197 | compressed_rdd = data_rdd.map(lambda x: zlib.compress(str(x))) 198 | ``` 199 | 200 | **En 99% de casos: Esto NO aplica.** Usa DataFrame. 201 | 202 | --- 203 | 204 | ## ¿Cuándo Usar DataFrame? 205 | 206 | **Usa DataFrame SIEMPRE que:** 207 | 208 | - ✅ Datos tienen estructura (CSV, Parquet, JSON, SQL) 209 | - ✅ Necesitas filtrado/agregación 210 | - ✅ Quieres usar Spark SQL 211 | - ✅ Performance importa 212 | - ✅ Team necesita mantenibilidad 213 | 214 | **Resumen: Casi siempre usa DataFrame.** 215 | 216 | --- 217 | 218 | ## Performance: Catalyst Optimizer 219 | 220 | Cuando usas DataFrame, Spark ejecuta estas optimizaciones automáticamente: 221 | 222 | Tu código: 223 | 224 | ```python 225 | df.filter(col("age") > 25).select("name", "salary").groupBy("department").avg("salary") 226 | ``` 227 | 228 | Lo que Spark hace internamente (Catalyst): 229 | 230 | - Predicate Pushdown: Filtra ANTES de select (menos datos) 231 | - Column Pruning: Solo lee columnas necesarias 232 | - Constant Folding: Pre-calcula constantes 233 | - Join Reordering: Ordena joins para minimizar shuffle 234 | - Result en Parquet: Serializa eficientemente 235 | 236 | **RDD hace NINGUNA de estas optimizaciones.** 237 | 238 | --- 239 | 240 | ## Hybrid: Conversiones RDD ↔ DataFrame 241 | 242 | A veces tienes RDD y necesitas DataFrame (o viceversa): 243 | 244 | ```python 245 | # RDD → DataFrame 246 | rdd = sc.parallelize([("Alice", 28), ("Bob", 35)]) 247 | df = rdd.toDF(["name", "age"]) 248 | 249 | # DataFrame → RDD (perdes schema, metadata) 250 | df_back_rdd = df.rdd # df.rdd es RDD[Row] 251 | 252 | # Útil para: operaciones específicas que necesitan RDD flexibility 253 | processed_rdd = df.rdd.map(lambda row: (row.name.upper(), row.age * 2)) 254 | result_df = processed_rdd.toDF(["name_upper", "age_doubled"]) 255 | ``` 256 | 257 | --- 258 | 259 | ## Errores comunes en entrevista 260 | 261 | - **Error**: "RDD es siempre mejor porque es más flexible" → **Solución**: Flexibilidad ≠ velocidad. DataFrame gana en performance 262 | 263 | - **Error**: "Usa RDD porque entiendo mejor la lógica" → **Solución**: DataFrame es más intuitivo. Si RDD te parece fácil, no entiendes Spark 264 | 265 | - **Error**: No pensar en schema → **Solución**: Schema explícito (no inferencia) es production-ready 266 | 267 | - **Error**: Usar `collect()` en RDD grande → **Solución**: Causa out-of-memory. Usa `.take()` o `.first()` 268 | 269 | --- 270 | 271 | ## Preguntas de seguimiento típicas 272 | 273 | 1. **"¿Qué es Catalyst Optimizer?"** 274 | - Componente de Spark que optimiza queries DataFrame automáticamente 275 | - Analiza lógica, reordena operaciones, predicate pushdown 276 | - RDD bypassa completamente esto 277 | 278 | 2. **"¿Cómo eliges schema: explícito o inferencia?"** 279 | - Explícito: Mejor para production, conoces tipos 280 | - Inferencia: Rápido para exploración, más lento (2 scans) 281 | 282 | 3. **"¿Spark SQL vs DataFrame API?"** 283 | - SQL: Más legible para SQL devs 284 | - API: Más flexible, se integra con código Python 285 | - Mismo motor abajo (Catalyst optimiza ambos) 286 | 287 | 4. **"¿Cuándo RDD es más rápido que DataFrame?"** 288 | - Casi nunca. DataFrame siempre gana 289 | - Excepción: si datos son tiny (< 1GB), overhead es negligible 290 | 291 | --- 292 | 293 | ## Real-World: Decisión en Proyecto Actual 294 | 295 | **Escenario: Pipeline de E-Commerce** 296 | 297 | Datos: clicks de usuario, compras, reviews 298 | Estructura: CSV con columnas conocidas 299 | ✅ CORRECTO: DataFrame 300 | 301 | ```python 302 | events_df = spark.read.schema(my_schema).csv("events.csv") 303 | purchase_df = events_df.filter(col("event_type") == "purchase") 304 | by_user = purchase_df.groupBy("user_id").agg(sum("amount")) 305 | by_user.write.parquet("output/") 306 | ``` 307 | 308 | ❌ INCORRECTO: RDD 309 | 310 | ```python 311 | events_rdd = sc.textFile("events.csv").map(parse_csv) 312 | purchase_rdd = events_rdd.filter(lambda x: x['event_type'] == 'purchase') 313 | by_user_rdd = purchase_rdd.groupByKey().mapValues(lambda v: sum([x['amount'] for x in v])) 314 | ``` 315 | 316 | Lento, difícil de mantener, zero optimización 317 | 318 | **La regla**: Si tienes datos estructurados, siempre DataFrame. 319 | 320 | --- 321 | 322 | ## Referencias 323 | 324 | - [Catalyst Optimizer - Apache Spark](https://spark.apache.org/docs/latest/sql-performance-tuning.html) 325 | - [DataFrame API - PySpark Docs](https://spark.apache.org/docs/latest/api/python/reference/pyspark.sql/dataframe.html) 326 | -------------------------------------------------------------------------------- /sql/09-data-quality-null-handling.md: -------------------------------------------------------------------------------- 1 | # Calidad de Datos: Manejo de NULLs y Detección de Duplicados 2 | 3 | **Tags**: #sql #data-quality #null-handling #duplicates #data-engineering #real-interview 4 | 5 | --- 6 | 7 | ## TL;DR 8 | 9 | Maneja NULLs con `IS NULL`, `COALESCE()`, `NULLIF()`. Detecta duplicados con `ROW_NUMBER()` o `GROUP BY ... HAVING COUNT(*) > 1`. Valida datos con `CASE WHEN` checks. En data engineering, 80% del trabajo es limpiar datos, 20% es análisis. 10 | 11 | --- 12 | 13 | ## Concepto Core 14 | 15 | - **Qué es**: La calidad de datos asegura que los datos sean correctos, completos y sin duplicados. Los NULLs son valores faltantes que causan problemas 16 | - **Por qué importa**: Datos sucios = análisis sucios = decisiones malas. Los ingenieros de datos pasan 80% del tiempo limpiando. Habilidad crítica 17 | - **Principio clave**: NULL ≠ 0 ≠ cadena vacía. Cada uno se maneja distinto. Nunca ignores los NULLs 18 | 19 | --- 20 | 21 | ## Memory Trick 22 | 23 | **"Basura dentro, basura fuera"** (Garbage in, garbage out) — Si los datos están sucios (NULLs, duplicados, valores inválidos), el resultado será basura. 24 | 25 | --- 26 | 27 | ## Cómo explicarlo en entrevista 28 | 29 | **Paso 1**: "Los datos reales siempre tienen problemas: valores faltantes (NULLs), duplicados, valores inválidos" 30 | 31 | **Paso 2**: "Para manejar NULLs: `COALESCE()` para valores por defecto, `IS NULL` para filtrar, `NULLIF()` para casos especiales" 32 | 33 | **Paso 3**: "Para duplicados: `ROW_NUMBER()` y filtro donde rank = 1, o `GROUP BY` con `HAVING COUNT(*) > 1`" 34 | 35 | **Paso 4**: "Valida datos con `CASE WHEN` checks y alertas" 36 | 37 | --- 38 | 39 | ## Código/Query ejemplo 40 | 41 | ### Tabla: customers (con datos sucios) 42 | 43 | ``` 44 | customer_id | name | email | phone | age | registration_date 45 | 1 | Alice | alice@example.com | NULL | 28 | 2024-01-15 46 | 2 | Bob | bob@example.com | 555-1234 | NULL| 2024-01-20 47 | 3 | Charlie | NULL | 555-5678 | 35 | 2024-02-01 48 | 4 | Alice | alice@example.com | NULL | 28 | 2024-01-15 (DUPLICATE!) 49 | 5 | David | david@example.com | 555-9999 | -5 | 2024-03-01 (INVALID!) 50 | 6 | Eve | eve@example.com | NULL | 45 | NULL (INCOMPLETE!) 51 | 7 | Frank | frank@example.com | 555-1234 | 32 | 2024-02-15 52 | ``` 53 | 54 | --- 55 | 56 | ### Problema 1: Manejar NULLs con COALESCE 57 | 58 | ```sql 59 | -- ❌ Problema: NULL values en output 60 | SELECT 61 | customer_id, 62 | name, 63 | email, 64 | phone, 65 | age 66 | FROM customers; 67 | 68 | -- ✅ Solución: COALESCE para default values 69 | SELECT 70 | customer_id, 71 | name, 72 | COALESCE(email, 'no-email@unknown.com') as email, 73 | COALESCE(phone, 'Not provided') as phone, 74 | COALESCE(age, 0) as age, 75 | CASE 76 | WHEN email IS NULL THEN 'Missing Email' 77 | WHEN phone IS NULL THEN 'Missing Phone' 78 | WHEN age IS NULL THEN 'Missing Age' 79 | ELSE 'Complete' 80 | END as data_quality_flag 81 | FROM customers 82 | ORDER BY customer_id; 83 | ``` 84 | 85 | **Resultado:** 86 | 87 | ``` 88 | customer_id | name | email | phone | age | data_quality_flag 89 | 1 | Alice | alice@example.com | Not provided | 28 | Missing Phone 90 | 2 | Bob | bob@example.com | 555-1234 | 0 | Missing Age 91 | 3 | Charlie | no-email@unknown.com | 555-5678 | 35 | Missing Email 92 | ... 93 | ``` 94 | 95 | --- 96 | 97 | ### Problema 2: Detectar Duplicados 98 | 99 | ```sql 100 | -- ¿Cuáles customers están duplicados? 101 | SELECT 102 | customer_id, 103 | name, 104 | email, 105 | COUNT(*) as occurrences 106 | FROM customers 107 | GROUP BY name, email 108 | HAVING COUNT(*) > 1 109 | ORDER BY occurrences DESC; 110 | ``` 111 | 112 | **Resultado:** 113 | 114 | ``` 115 | customer_id | name | email | occurrences 116 | 1 / 4 | Alice | alice@example.com | 2 117 | ``` 118 | 119 | --- 120 | 121 | ### Problema 3: Eliminar Duplicados (Deduplication) 122 | 123 | ```sql 124 | -- ✅ Opción 1: ROW_NUMBER (más flexible) 125 | WITH deduped AS ( 126 | SELECT 127 | *, 128 | ROW_NUMBER() OVER (PARTITION BY name, email ORDER BY customer_id) as rn 129 | FROM customers 130 | ) 131 | SELECT * 132 | FROM deduped 133 | WHERE rn = 1; -- Mantén solo el primero de cada duplicado 134 | 135 | -- ✅ Opción 2: DISTINCT (solo si todos los campos son idénticos) 136 | SELECT DISTINCT * 137 | FROM customers; 138 | 139 | -- ✅ Opción 3: GROUP BY (útil si necesitas agregación) 140 | SELECT 141 | MIN(customer_id) as customer_id, -- Toma el ID más bajo 142 | name, 143 | email, 144 | MIN(phone) as phone, 145 | MAX(age) as age, 146 | MIN(registration_date) as registration_date 147 | FROM customers 148 | GROUP BY name, email; 149 | ``` 150 | 151 | --- 152 | 153 | ### Problema 4: Validar Datos (Business Rules) 154 | 155 | ```sql 156 | -- ¿Qué datos violan las reglas de negocio? 157 | SELECT 158 | customer_id, 159 | name, 160 | age, 161 | registration_date, 162 | CASE 163 | -- Validaciones 164 | WHEN age < 0 OR age > 150 THEN 'Invalid: age out of range' 165 | WHEN age < 18 THEN 'Warning: underage' 166 | WHEN registration_date IS NULL THEN 'Error: missing registration date' 167 | WHEN registration_date > CURRENT_DATE THEN 'Error: future date' 168 | WHEN email IS NULL AND phone IS NULL THEN 'Error: no contact info' 169 | WHEN email ~ '^[A-Za-z0-9.*%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$' THEN 'Valid email' 170 | ELSE 'OK' 171 | END as data_quality_check 172 | FROM customers 173 | ORDER BY data_quality_check; 174 | ``` 175 | 176 | **Resultado:** 177 | 178 | ``` 179 | customer_id | name | age | registration_date | data_quality_check 180 | 5 | David | -5 | 2024-03-01 | Invalid: age out of range 181 | 6 | Eve | 45 | NULL | Error: missing registration date 182 | 2 | Bob | NULL| 2024-01-20 | Error: underage (if NULL means < 18) 183 | ... 184 | ``` 185 | 186 | --- 187 | 188 | ### Problema 5: NULLIF (Convertir valores a NULL) 189 | 190 | ```sql 191 | -- Convertir valores específicos a NULL para mejor análisis 192 | SELECT 193 | customer_id, 194 | name, 195 | NULLIF(age, 0) as age, -- Convertir 0 a NULL (porque 0 es placeholder) 196 | NULLIF(phone, '') as phone, -- Convertir string vacío a NULL 197 | COALESCE(NULLIF(age, 0), 25) as age_with_default -- Si age = 0, usa 25 198 | FROM customers; 199 | ``` 200 | 201 | --- 202 | 203 | ### Problema 6: Reporte de Calidad de Datos 204 | 205 | ```sql 206 | -- Reporte de calidad de datos 207 | SELECT 208 | 'Customers' as table_name, 209 | COUNT(*) as total_rows, 210 | COUNT(*) FILTER (WHERE customer_id IS NULL) as null_customer_id, 211 | COUNT(*) FILTER (WHERE name IS NULL) as null_name, 212 | COUNT(*) FILTER (WHERE email IS NULL) as null_email, 213 | COUNT(*) FILTER (WHERE phone IS NULL) as null_phone, 214 | COUNT(*) FILTER (WHERE age IS NULL) as null_age, 215 | COUNT(*) FILTER (WHERE age < 0 OR age > 150) as invalid_age, 216 | COUNT(DISTINCT name, email) as unique_customers, 217 | COUNT(*) - COUNT(DISTINCT name, email) as potential_duplicates 218 | FROM customers; 219 | ``` 220 | 221 | **Resultado:** 222 | 223 | ``` 224 | table_name | total_rows | null_customer_id | null_name | null_email | null_phone | null_age | invalid_age | unique_customers | potential_duplicates 225 | Customers | 7 | 0 | 0 | 1 | 2 | 1 | 1 | 6 | 1 226 | ``` 227 | 228 | --- 229 | 230 | ## Comportamiento de NULL en SQL 231 | 232 | | Operación | Resultado | Razón | 233 | | -------------------- | ---------------- | ------------------------------------------------------------------- | 234 | | `NULL = NULL` | NULL (not true!) | En SQL, NULL = desconocido, desconocido = desconocido = desconocido | 235 | | `NULL IS NULL` | TRUE | Forma correcta de comparar NULL | 236 | | `NULL + 5` | NULL | Operación con NULL = NULL | 237 | | `SUM(col)` con NULLs | Ignora NULLs | COUNT, SUM, AVG ignoran NULLs automáticamente | 238 | | `COUNT(*)` con NULLs | Incluye | `COUNT(*)` cuenta filas, `COUNT(col)` ignora NULLs | 239 | 240 | --- 241 | 242 | ## Errores comunes en entrevista 243 | 244 | - **Error**: Usar `WHERE col = NULL` → **Solución**: `WHERE col IS NULL` 245 | 246 | - **Error**: Olvidar que NULLs afectan JOINs → **Solución**: `WHERE col1 = col2` no matchea si alguno es NULL. Usa `COALESCE` si necesitas 247 | 248 | - **Error**: No validar datos antes de análisis → **Solución**: Siempre haz data quality checks primero 249 | 250 | - **Error**: Asumir que DISTINCT elimina duplicados cuando hay NULLs → **Solución**: DISTINCT con NULLs es tricky. Usa ROW_NUMBER para control fino 251 | 252 | --- 253 | 254 | ## Preguntas de seguimiento típicas 255 | 256 | 1. **"¿Diferencia entre COALESCE y IFNULL?"** 257 | - COALESCE: retorna primer valor no-NULL (soporta múltiples) 258 | - IFNULL: solo dos argumentos (más limitado) 259 | - COALESCE es más estándar 260 | 261 | 2. **"¿Cómo determinas si duplicados son reales o errores?"** 262 | - Analiza timestamps: si son idénticos = error 263 | - Analiza IDs: si son diferentes = posible entidad duplicada 264 | - Habla con data owner para reglas 265 | 266 | 3. **"¿Qué haces con duplicados: eliminas o archivas?"** 267 | - Nunca elimines sin documentar 268 | - Archiva en tabla histórica 269 | - Marca como "deduped_source_id" para trazabilidad 270 | 271 | 4. **"¿Cómo manejas NULLs en agregaciones?"** 272 | - SUM/AVG/COUNT ignoran automáticamente 273 | - Si necesitas contar NULLs: `COUNT(*) - COUNT(col)` 274 | 275 | --- 276 | 277 | ## Real-World: Ingestión en Data Warehouse 278 | 279 | ```sql 280 | -- ETL: Limpiar datos antes de cargar a warehouse 281 | WITH raw_data AS ( 282 | SELECT * FROM staging.raw_customers 283 | ), 284 | 285 | cleaned_data AS ( 286 | SELECT 287 | customer_id, 288 | TRIM(name) as name, -- Remove spaces 289 | LOWER(email) as email, -- Standardize 290 | COALESCE(phone, 'Unknown') as phone, 291 | CASE 292 | WHEN age < 0 OR age > 150 THEN NULL -- Invalid becomes NULL 293 | ELSE age 294 | END as age, 295 | registration_date, 296 | CURRENT_TIMESTAMP as loaded_at 297 | FROM raw_data 298 | WHERE customer_id IS NOT NULL -- Must have ID 299 | ), 300 | 301 | deduplicated AS ( 302 | SELECT * 303 | FROM ( 304 | SELECT 305 | *, 306 | ROW_NUMBER() OVER (PARTITION BY email ORDER BY registration_date) as rn 307 | FROM cleaned_data 308 | ) t 309 | WHERE rn = 1 310 | ) 311 | 312 | INSERT INTO analytics.dim_customers 313 | SELECT * FROM deduplicated; 314 | ``` 315 | 316 | --- 317 | 318 | ## Checklist de Calidad de Datos 319 | 320 | - ✅ Valores NULL identificados y manejados 321 | - ✅ Duplicados detectados 322 | - ✅ Valores inválidos validados (edad, fecha, etc.) 323 | - ✅ Valores faltantes documentados 324 | - ✅ Tipos de datos correctos 325 | - ✅ Relaciones referenciales válidas (FK checks) 326 | - ✅ Datos históricos preservados (audit trail) 327 | 328 | --- 329 | 330 | ## Referencias 331 | 332 | - [NULL Handling - PostgreSQL Docs](https://www.postgresql.org/docs/current/functions-comparison.html) 333 | - [Data Quality in SQL - Mode Analytics](https://mode.com/sql-tutorial/) 334 | -------------------------------------------------------------------------------- /streaming/03-lambda-kappa-architectures.md: -------------------------------------------------------------------------------- 1 | # Tiempo Real vs Batch: Arquitecturas Lambda y Kappa 2 | 3 | **Tags**: #architecture #lambda #kappa #real-time #batch #trade-offs #real-interview 4 | 5 | --- 6 | 7 | ## TL;DR 8 | 9 | **Lambda** = batch + capa de velocidad (pipelines duales). **Kappa** = solo streaming (pipeline único, capaz de replay). **Compromiso**: Lambda = corrección garantizada pero complejo; Kappa = más simple pero requiere streaming perfecto. **Realidad**: La mayoría de las empresas usan Kappa ahora (Kafka replay lo hace funcionar). 10 | 11 | --- 12 | 13 | ## Concepto Core 14 | 15 | - **Qué es**: Dos arquitecturas competidoras para analítica en tiempo real 16 | - **Por qué importa**: Afecta la infraestructura, complejidad, corrección 17 | - **Principio clave**: Streaming ≠ perfecto. Necesita estrategia para correcciones 18 | 19 | --- 20 | 21 | ## Arquitectura Lambda 22 | 23 | ``` 24 | Fuente de Datos 25 | │ 26 | ├─────────────────────────┬──────────────────────────┐ 27 | │ │ 28 | ▼ ▼ ▼ 29 | CAPA BATCH LENTA CAPA DE VELOCIDAD RÁPIDA 30 | (Lento, Correcto) (Rápido, Aproximado) (Resultados fusionados) 31 | ``` 32 | 33 | Batch: 34 | ├─ Datos históricos 35 | ├─ Se ejecuta nocturnamente 36 | ├─ Spark/EMR (recalcula todo) 37 | ├─ Tarda 2-4 horas 38 | ├─ Absolutamente correcto 39 | └─ Resultados → Base de datos 40 | 41 | Velocidad: 42 | ├─ Eventos en tiempo real 43 | ├─ Consumidores de Kafka 44 | ├─ Spark Streaming (incremental) 45 | ├─ 10-30 segundos de latencia 46 | ├─ Aproximado (puede tener errores) 47 | └─ Resultados → Caché/Base de datos 48 | 49 | Servicio: 50 | ├─ Consulta de usuario 51 | │ 52 | ├─ Verificar capa de velocidad (últimos ~2 horas) 53 | ├─ Verificar capa batch (completo) 54 | ├─ Fusionar resultados 55 | ├─ Si hay discrepancia: confiar en batch 56 | └─ Devolver al usuario 57 | 58 | Arquitectura: 59 | Consulta de usuario 60 | │ 61 | ├─ Verificar capa de velocidad (últimos ~2 horas) 62 | ├─ Verificar capa batch (completo) 63 | ├─ Fusionar resultados 64 | ├─ Si hay discrepancia: confiar en batch 65 | └─ Devolver al usuario 66 | 67 | Cronología: 68 | 2AM - La capa batch comienza (recalcula todo) 69 | 6AM - La capa batch termina (resultados correctos) 70 | 6-24h - La capa de velocidad se ejecuta (resultados aproximados) 71 | Siguiente 2AM - La capa de velocidad se descarta, reemplazada por batch 72 | 73 | ``` 74 | 75 | ### Pros y Contras de Lambda 76 | 77 | **Pros:** 78 | ✓ Corrección garantizada (capa batch = fuente de verdad) 79 | ✓ Fácil corregir errores (volver a ejecutar batch) 80 | ✓ Lógica simple (herramientas batch conocidas) 81 | 82 | **Contras:** 83 | ✗ Mantenimiento dual (código batch + código streaming) 84 | ✗ Complejidad (fusionar dos sistemas) 85 | ✗ Inconsistencia de datos (velocidad ≠ batch por 12-22 horas) 86 | ✗ Costo más alto (calcular + almacenar dos veces) 87 | 88 | --- 89 | 90 | ## Arquitectura Kappa 91 | 92 | ``` 93 | 94 | Fuente de Datos 95 | │ 96 | ▼ 97 | PIPELINE DE STREAMING (único sistema, siempre correcto) 98 | ├─ Kafka (log inmutable) 99 | ├─ Procesador de stream (Spark, Flink) 100 | ├─ Tienda de estado (Redis, RocksDB) 101 | └─ Resultados → Base de datos 102 | 103 | ``` 104 | 105 | Visión clave: Kafka es replay 106 | ├─ Contiene TODOS los eventos (inmutable) 107 | ├─ Nuevo error descubierto 108 | ├─ Rebobinar Kafka desde el principio 109 | ├─ Todos los resultados regenerados (correctos) 110 | └─ Sin "ventana de inconsistencia" 111 | 112 | Arquitectura: 113 | Tema de Kafka "eventos" 114 | ├─ Contiene TODOS los eventos (inmutable) 115 | ├─ Consumidor: offset actual (ej: evento 5M) 116 | ├─ Error descubierto 117 | ├─ Código corregido 118 | ├─ Reiniciar consumidor: offset = 0 119 | ├─ Reprocesar todos los eventos 120 | └─ Base de datos actualizada (correcta) 121 | 122 | Cronología: 123 | 2AM - Error descubierto 124 | 2:05AM - Código corregido 125 | 2:10AM - Reiniciar consumidor (offset = 0) 126 | 3AM - Todos los eventos reprocesados 127 | 3:05AM - Base de datos correcta (sin ventana de 22 horas) 128 | 129 | ``` 130 | 131 | ### Pros y Contras de Kappa 132 | 133 | **Pros:** 134 | ✓ Sistema único (más simple) 135 | ✓ Siempre correcto (o fácilmente corregible) 136 | ✓ Sin ventana de inconsistencia 137 | ✓ Recuperación más rápida (reprocesar desde Kafka) 138 | 139 | **Contras:** 140 | ✗ Requiere Kafka (log inmutable) 141 | ✗ Replay puede ser lento (reprocesar todo el historial) 142 | ✗ Gestión de estado compleja (para sesiones, agregaciones) 143 | ✗ No adecuado para lógica batch compleja (ML entrenamiento, etc.) 144 | 145 | --- 146 | 147 | ## Comparación 148 | 149 | | Aspecto | Lambda | Kappa | 150 | | ----------------- | ------------------------------------- | ------------------------------- | 151 | | **Complejidad** | Alta (2 sistemas) | Baja (1 sistema) | 152 | | **Corrección** | Eventual (después de batch) | Garantizada (replay) | 153 | | **Latencia** | Mixta (velocidad rápida, batch lento) | Consistente (siempre streaming) | 154 | | **Recuperación** | Volver a ejecutar batch | Reprocesar desde Kafka | 155 | | **Mantenimiento** | Dual (código batch + streaming) | Único (solo streaming) | 156 | | **Costo** | Alto (calcular + almacenar dos veces) | Medio (solo streaming) | 157 | | **Adecuado para** | Analítica compleja | Sistemas en tiempo real | 158 | 159 | --- 160 | 161 | ## Caso Real: Pedidos de E-commerce 162 | 163 | ### Enfoque Lambda 164 | 165 | Capa Batch (nocturna): 166 | ├─ Leer: pedidos crudos (S3, todo el historial) 167 | ├─ Calcular: ingresos, productos principales, segmentos de clientes 168 | ├─ Escribir: tablas agregadas (Redshift) 169 | └─ Latencia: 2-4 horas 170 | 171 | Capa de Velocidad (tiempo real): 172 | ├─ Leer: Kafka pedidos (últimos 30 minutos) 173 | ├─ Calcular: agregados incrementales 174 | ├─ Escribir: caché (Redis, expira en 30 minutos) 175 | └─ Latencia: 30 segundos 176 | 177 | Servicio: 178 | Consulta "ingresos hoy" 179 | ├─ Verificar caché (últimos ~30 minutos, ~2 horas) 180 | ├─ Verificar Redshift (completo, hasta ayer) 181 | ├─ Fusionar resultados 182 | ├─ Si hay discrepancia: confiar en Redshift 183 | └─ Devolver: $150k + nota "últimas 2 horas aproximadas" 184 | 185 | Problema: 186 | ├─ Usuario ve $150k a las 10 AM 187 | ├─ A las 2 AM, batch termina: $145k 188 | ├─ Usuario ve $145k a las 10:05 AM (¡bajó!) 189 | ├─ Confusión: "¿dónde fueron mis $5k?" 190 | └─ Explicación: "Ajuste de batch, los datos de ayer se corrigieron" 191 | 192 | ### Enfoque Kappa 193 | 194 | Pipeline de Streaming (siempre): 195 | ├─ Leer: Kafka pedidos (todos los eventos) 196 | ├─ Calcular: agregados incrementales 197 | ├─ Escribir: base de datos (actualizada continuamente) 198 | └─ Latencia: 30 segundos 199 | 200 | Consulta "ingresos hoy": 201 | ├─ Verificar base de datos (siempre correcta) 202 | ├─ Devolver: $150k 203 | └─ Sin notas, sin confusión 204 | 205 | Error descubierto: 206 | ├─ Error en cálculo de ingresos (impuesto faltante) 207 | ├─ Código corregido 208 | ├─ Reiniciar consumidor Kafka (offset = 0) 209 | ├─ Reprocesar todos los eventos (1 hora) 210 | ├─ Base de datos corregida 211 | └─ Sin ventana de inconsistencia 212 | 213 | --- 214 | 215 | ## Decisión: Lambda vs Kappa 216 | 217 | Elegir Lambda si: 218 | ├─ Lógica batch compleja (ML, informes semanales/mensuales) 219 | ├─ La corrección puede esperar 12-24 horas 220 | ├─ El costo es una preocupación importante 221 | ├─ Ya tienes sistemas batch optimizados 222 | 223 | Elegir Kappa si: 224 | ├─ Tiempo real es crítico (<1 minuto de latencia) 225 | ├─ La lógica de streaming es manejable 226 | ├─ La corrección inmediata es necesaria 227 | ├─ Tienes Kafka (o log inmutable equivalente) 228 | 229 | Tendencia actual: 230 | ├─ Empresas moviéndose a Kappa 231 | ├─ Razón: Kafka maduro, frameworks de streaming mejorados 232 | ├─ Compromiso: Simplicidad > ahorro de costos (para empresas con muchos ingenieros) 233 | 234 | --- 235 | 236 | ## Híbrido: Lambda con Beneficios de Kappa 237 | 238 | Idea: Usar estilo Kappa replay en arquitectura Lambda 239 | 240 | Arquitectura: 241 | ├─ Capa batch: nocturna, como siempre 242 | ├─ Capa de velocidad: Kafka + capaz de replay 243 | ├─ Error encontrado: no solo reiniciar capa de velocidad 244 | ├─ Solución: rebobinar Kafka, reprocesar capa de velocidad + ingestar a batch 245 | └─ Resultado: corrección rápida (30 minutos) sin esperar a batch nocturno 246 | 247 | Ventajas: 248 | ✓ Seguridad de batch (resultados finales correctos) 249 | ✓ Corrección rápida de Kappa (reprocesamiento inmediato) 250 | ✓ Mejor de ambos mundos 251 | 252 | --- 253 | 254 | ## Trucos del Streaming 255 | 256 | Truco 1: Eventos fuera de orden 257 | ├─ Evento A timestamp 10:00, llega 10:05 258 | ├─ Evento B timestamp 10:02, llega 10:01 259 | ├─ Si se procesa secuencial: orden incorrecto 260 | ├─ Solución: Windowing por tiempo de evento + watermarks 261 | 262 | Truco 2: Estado con fugas de memoria 263 | ├─ "¿Cuántos pedidos del cliente X en las últimas 24 horas?" 264 | ├─ Necesitas estado: cliente → lista de pedidos 265 | ├─ Si el consumidor falla: estado perdido 266 | ├─ Solución: Tienda de estado externa (Redis, RocksDB) 267 | 268 | Truco 3: Entrega exactamente una vez 269 | ├─ "Procesar cada pago exactamente una vez" 270 | ├─ Kafka puede duplicar mensajes 271 | ├─ Solución: Procesamiento idempotente + clave única en base de datos 272 | 273 | Truco 4: Presión de datos (backpressure) 274 | ├─ Productor demasiado rápido para el consumidor 275 | ├─ Consumidor no puede mantenerse al día 276 | ├─ Solución: Buffer en Kafka, escalar consumidores 277 | 278 | --- 279 | 280 | ## Errores Comunes en Entrevista 281 | 282 | - **Error**: "Kappa reemplaza completamente a Lambda" → **Solución**: Depende del caso de uso. Lambda todavía es mejor para analytics complejos 283 | 284 | - **Error**: "Streaming siempre es mejor" → **Solución**: Más complejo, mayor carga operativa 285 | 286 | - **Error**: "Kafka almacena eventos para siempre" → **Solución**: Retención predeterminada (7 días por defecto) 287 | 288 | - **Error**: "Los consumidores compiten por mensajes" → **Solución**: Depende del grupo de consumidores (mismo grupo = dividen; diferentes grupos = todos reciben) 289 | 290 | --- 291 | 292 | ## Preguntas de Seguimiento Típicas 293 | 294 | 1. **"¿Diferencia entre Lambda y Kappa?"** 295 | - Lambda: Batch + velocidad 296 | - Kappa: Solo streaming 297 | - Lambda: Corrección eventual; Kappa: Corrección inmediata (con replay) 298 | 299 | 2. **"¿Kappa replay performance?"** 300 | - Depende de la retención de Kafka + velocidad del procesador 301 | - Si 1 año de retención: replay puede tomar horas 302 | - Trade-off: Almacenamiento vs flexibilidad 303 | 304 | 3. **"¿Manejo de estado en Kappa?"** 305 | - Tienda de estado externa (Redis, RocksDB) 306 | - Para sesiones, agregaciones complejas 307 | - Clave: particionar por clave de estado 308 | 309 | 4. **"¿Netflix/Uber architecture?"** 310 | - Principalmente Kappa ahora 311 | - Batch para ML entrenamiento, informes pesadas 312 | - Híbrido: Streaming principal con batch secundario 313 | 314 | --- 315 | 316 | ## Referencias 317 | 318 | - [Real-time Big Data - O'Reilly](https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/) 319 | -------------------------------------------------------------------------------- /pyspark/02-transformations-actions.md: -------------------------------------------------------------------------------- 1 | # Transformations & Actions: Lazy Evaluation 2 | 3 | **Tags**: #pyspark #transformations #actions #lazy-evaluation #fundamental #real-interview 4 | 5 | --- 6 | 7 | ## TL;DR 8 | 9 | **Transformations** = operaciones lazy (no se ejecutan hasta acción). Devuelven nuevo DataFrame/RDD. **Actions** = operaciones eager (ejecutan TODO). Devuelven resultado a driver o escriben. Lazy evaluation es lo que hace Spark eficiente: puede optimizar todo el plan antes de ejecutar. 10 | 11 | --- 12 | 13 | ## Concepto Core 14 | 15 | - **Qué es**: Lazy evaluation significa Spark no ejecuta transformations hasta que le pidas un resultado (action) 16 | - **Por qué importa**: Spark puede optimizar el plan completo antes de ejecutar. Sin lazy evaluation, sería 100x más lento 17 | - **Principio clave**: Transf. = "qué hacer". Actions = "hazlo ahora". Transformations se stackean, actions las ejecutan todas juntas 18 | 19 | --- 20 | 21 | ## Memory Trick 22 | 23 | **"Director y actores"**: 24 | 25 | - Transformations = Director dice "éstas van a ser las escenas" 26 | - Actions = "¡Rodenlo AHORA!" (se ejecuta todo de verdad) 27 | - Sin actions, no hay película (transformations nunca corren) 28 | 29 | --- 30 | 31 | ## Cómo explicarlo en entrevista 32 | 33 | **Paso 1**: "Spark es lazy: no ejecuta nada hasta que pides un resultado" 34 | 35 | **Paso 2**: "Transformations (map, filter, join) = 'planes'. Actions (collect, write, count) = 'ejecuta'" 36 | 37 | **Paso 3**: "Ventaja: Spark ve el plan completo, optimiza, luego ejecuta. Sin lazy, ejecutaría cada transformación por separado" 38 | 39 | **Paso 4**: "Eso es por qué Spark es rápido: optimización global, no paso a paso" 40 | 41 | --- 42 | 43 | ## Código/Query ejemplo 44 | 45 | ### Conceptual: Lazy vs Eager 46 | 47 | ```python 48 | from pyspark.sql import SparkSession 49 | from pyspark.sql.functions import col 50 | 51 | spark = SparkSession.builder.appName("Lazy Evaluation").getOrCreate() 52 | 53 | # Leer datos 54 | df = spark.read.parquet("data.parquet") 55 | 56 | # ===== TRANSFORMATIONS (Lazy) ===== 57 | # Ninguna de estas ejecuta. Spark solo PLANEA qué hacer 58 | # Transf 1: Filter 59 | filtered = df.filter(col("age") > 25) 60 | 61 | # Transf 2: Select 62 | selected = filtered.select("name", "salary") 63 | 64 | # Transf 3: GroupBy 65 | grouped = selected.groupBy("department").avg("salary") 66 | 67 | # En este punto: NADA se ha ejecutado. Spark tiene el plan pero no corre 68 | print("Plan lógico (no ejecutado):") 69 | grouped.explain(mode="simple") 70 | 71 | # ===== ACTION: AHORA se ejecuta TODO ===== 72 | result = grouped.collect() # ← AQUÍ Spark ejecuta transformations 1-3 73 | 74 | print("Resultado:", result) 75 | ``` 76 | 77 | **Timeline:** 78 | Línea 1-12: Spark construye plan (instantáneo) 79 | Línea 15: explain() muestra plan (no ejecuta) 80 | Línea 18: collect() ejecuta TODO (toma tiempo) 81 | 82 | --- 83 | 84 | ## Transformations Comunes (Lazy) 85 | 86 | ===== Transformations que DEVUELVEN DataFrame/RDD ===== 87 | 88 | 1. FILTER: Filas que cumplen condición 89 | 90 | ```python 91 | filtered = df.filter(col("salary") > 50000) 92 | ``` 93 | 94 | 2. SELECT: Columnas específicas 95 | 96 | ```python 97 | selected = df.select("name", "salary") 98 | ``` 99 | 100 | 3. WITHCOLUMN: Agregar/modificar columna 101 | 102 | ```python 103 | with_bonus = df.withColumn("bonus", col("salary") * 0.1) 104 | ``` 105 | 106 | 4. MAP: Transformación custom (RDD) 107 | 108 | ```python 109 | squared_rdd = numbers_rdd.map(lambda x: x ** 2) 110 | ``` 111 | 112 | 5. FLATMAP: Map + flatten 113 | 114 | ```python 115 | words = text_rdd.flatMap(lambda line: line.split()) 116 | ``` 117 | 118 | 6. JOIN: Combina DataFrames 119 | 120 | ```python 121 | joined = df1.join(df2, "id") 122 | ``` 123 | 124 | 7. UNION: Combina verticalmente 125 | 126 | ```python 127 | combined = df1.union(df2) 128 | ``` 129 | 130 | 8. GROUPBY: Agrupa (devuelve GroupedData, aún lazy) 131 | 132 | ```python 133 | grouped = df.groupBy("department") 134 | ``` 135 | 136 | 9. SORT: Ordena 137 | 138 | ```python 139 | sorted_df = df.sort(col("salary").desc()) 140 | ``` 141 | 142 | 10. DISTINCT: Únicos 143 | 144 | ```python 145 | unique = df.select("department").distinct() 146 | ``` 147 | 148 | 11. DROP: Elimina columna 149 | ```python 150 | dropped = df.drop("unnecessary_column") 151 | ``` 152 | 153 | NINGUNA DE ESTAS EJECUTÓ NADA. Son planes. 154 | 155 | --- 156 | 157 | ## Actions Comunes (Eager) 158 | 159 | ===== ACTIONS: Estos sí ejecutan TODO ===== 160 | 161 | 1. COLLECT: Trae TODO a driver (⚠️ cuidado con datasets grandes) 162 | 163 | ```python 164 | all_data = df.collect() 165 | ``` 166 | 167 | 2. TAKE: Primeras N filas 168 | 169 | ```python 170 | first_10 = df.take(10) 171 | ``` 172 | 173 | 3. FIRST: Primera fila 174 | 175 | ```python 176 | first_row = df.first() 177 | ``` 178 | 179 | 4. COUNT: Número de filas 180 | 181 | ```python 182 | row_count = df.count() 183 | ``` 184 | 185 | 5. SHOW: Muestra primeras 20 filas en consola 186 | 187 | ```python 188 | df.show() 189 | ``` 190 | 191 | 6. WRITE: Guarda a storage (S3, HDFS, Parquet) 192 | 193 | ```python 194 | df.write.parquet("output/result.parquet") 195 | ``` 196 | 197 | 7. FOREACH: Aplica función a cada fila 198 | 199 | ```python 200 | def process_row(row): 201 | print(row) 202 | 203 | df.foreach(process_row) 204 | ``` 205 | 206 | 8. REDUCE: Reduce RDD a 1 valor (RDD only) 207 | 208 | ```python 209 | sum_rdd = numbers_rdd.reduce(lambda a, b: a + b) 210 | ``` 211 | 212 | 9. SAVEASTEXTFILE: Guarda como texto (RDD) 213 | ```python 214 | rdd.saveAsTextFile("output/") 215 | ``` 216 | 217 | TODAS ESTAS EJECUTAN. Buscan el plan, lo optimizan, lo corren. 218 | 219 | --- 220 | 221 | ## Lazy Evaluation en Acción 222 | 223 | ### Escenario: Pipeline de Ventas 224 | 225 | ```python 226 | # Datos: millones de transacciones 227 | sales = spark.read.parquet("sales.parquet") 228 | 229 | # Plan (no ejecuta): 230 | monthly_report = ( 231 | sales 232 | .filter(col("status") == "completed") # Transf 1 233 | .filter(col("amount") > 0) # Transf 2 234 | .withColumn("month", to_date(col("date"))) # Transf 3 235 | .groupBy("month", "product") # Transf 4 236 | .agg(sum("amount").alias("revenue")) # Transf 5 237 | .sort(col("revenue").desc()) # Transf 6 238 | ) 239 | 240 | print("Plan construido (instantáneo)") 241 | print(monthly_report.explain(mode="simple")) 242 | 243 | # ===== PRIMER ACTION ===== 244 | print("COLLECT (action 1): Trae TODO a driver") 245 | result = monthly_report.collect() 246 | 247 | # Aquí: Spark ejecuta transf 1-6 JUNTAS (no uno por uno) 248 | # Catalyst optimizó: predicate pushdown, column pruning, etc. 249 | 250 | # ===== SEGUNDO ACTION (diferente) ===== 251 | print("WRITE (action 2): Guarda a Parquet") 252 | monthly_report.write.parquet("output/sales_report") 253 | 254 | # Aquí: Spark ejecuta transf 1-6 de nuevo (cada action es ejecución nueva) 255 | # PERO el plan se re-optimiza 256 | ``` 257 | 258 | ⚠️ **Importante**: Cada action re-ejecuta transformations anteriores. Si usas resultado múltiples veces, **cachea**: 259 | 260 | ```python 261 | # Cachear antes de múltiples actions 262 | monthly_report.cache() 263 | 264 | result1 = monthly_report.collect() # Ejecuta, cachea 265 | result2 = monthly_report.count() # Usa cache (rápido) 266 | result3 = monthly_report.write.parquet(...) # Usa cache (rápido) 267 | 268 | # Sin cache, ejecutaría 3 veces la pipeline 269 | ``` 270 | 271 | --- 272 | 273 | ## Cadena de Transformations (DAG) 274 | 275 | Spark crea un DAG (Directed Acyclic Graph): 276 | 277 | Tu código: 278 | 279 | ```python 280 | result = ( 281 | df.filter(col("age") > 25) 282 | .select("name", "salary") 283 | .groupBy("department") 284 | .avg("salary") 285 | ) 286 | ``` 287 | 288 | Spark internamente: 289 | 290 | ``` 291 | Read Parquet 292 | ↓ 293 | Filter (age > 25) ← Predicate pushdown aquí (Catalyst) 294 | ↓ 295 | Select (name, salary) ← Column pruning aquí 296 | ↓ 297 | GroupBy + Agg 298 | ↓ 299 | Output 300 | ``` 301 | 302 | Plan COMPLETO se optimiza antes de ejecutar 303 | 304 | **Sin lazy evaluation (❌ ineficiente):** 305 | Read → (ejecuta) → Filter → (ejecuta) → Select → (ejecuta) → GroupBy → (ejecuta) 306 | 307 | **Con lazy evaluation (✅ eficiente):** 308 | Read + Filter + Select + GroupBy → (optimiza TODO junto) → (ejecuta una sola vez) 309 | 310 | --- 311 | 312 | ## Errores comunes en entrevista 313 | 314 | - **Error**: Pensar que `.filter()` ejecuta inmediatamente → **Solución**: Es lazy. Espera action 315 | 316 | - **Error**: Usar `collect()` en DataFrame gigante → **Solución**: Causa out-of-memory. Usa `.write()` directo 317 | 318 | - **Error**: No cachear cuando usas resultado múltiples veces → **Solución**: `.cache()` después del transformation costoso 319 | 320 | - **Error**: No entender por qué 2 actions son lentos (repiten trabajo) → **Solución**: Cada action = nueva ejecución. Si reutilizas, cachea 321 | 322 | --- 323 | 324 | ## Preguntas de seguimiento típicas 325 | 326 | 1. **"¿Qué es un DAG?"** 327 | - Directed Acyclic Graph: representación del plan de ejecución 328 | - Spark lo usa para optimizar (Catalyst) 329 | - Puedes verlo con `.explain()` 330 | 331 | 2. **"¿Por qué `collect()` es peligroso?"** 332 | - Trae TODO a driver memory (un servidor) 333 | - Si DataFrame = 100GB, crash 334 | - Mejor: `.write()` directo a storage 335 | 336 | 3. **"¿Diferencia entre `cache()` y `persist()`?"** 337 | - `cache()` = guardar en memoria (por defecto) 338 | - `persist()` = guardar en memoria/disk/hybrid (configurable) 339 | - `persist(StorageLevel.MEMORY_AND_DISK)` es safe para datos grandes 340 | 341 | 4. **"¿Cómo debuggear plan lento?"** 342 | - `.explain(mode="extended")` muestra optimizaciones 343 | - Busca "Shuffle" (costoso) 344 | - Busca "Cartesian Product" (muy costoso) 345 | - Optimiza: partitioning, predicate pushdown, joins 346 | 347 | --- 348 | 349 | ## Patrón Common: Pipeline Real 350 | 351 | ```python 352 | # Lectura 353 | orders = spark.read.parquet("orders.parquet") 354 | customers = spark.read.parquet("customers.parquet") 355 | 356 | # Transformations (planea, no ejecuta) 357 | pipeline = ( 358 | orders 359 | .filter(col("year") == 2024) # Predicate pushdown 360 | .join(customers, "customer_id") # Broadcast si customers es pequeño 361 | .select("name", "order_amount", "category") 362 | .groupBy("category") 363 | .agg(sum("order_amount").alias("total")) 364 | ) 365 | 366 | # Cachea si lo usarás múltiples veces 367 | pipeline.cache() 368 | 369 | # Action 1 370 | pipeline.show() 371 | 372 | # Action 2 373 | pipeline.write.parquet("output/summary") 374 | 375 | # Action 3 376 | count = pipeline.count() 377 | 378 | # Sin cache, ejecutaría 3 veces. Con cache, 1 ejecución + 2 lecturas de cache. 379 | ``` 380 | 381 | --- 382 | 383 | ## Referencias 384 | 385 | - [Lazy Evaluation - Spark Docs](https://spark.apache.org/docs/latest/rdd-programming-guide.html#lazy-evaluation) 386 | - [Transformations & Actions - PySpark](https://spark.apache.org/docs/latest/api/python/reference/pyspark.sql/dataframe.html) 387 | -------------------------------------------------------------------------------- /pyspark/05-window-functions.md: -------------------------------------------------------------------------------- 1 | # Window Functions en PySpark 2 | 3 | **Tags**: #pyspark #window-functions #ranking #real-interview 4 | 5 | --- 6 | 7 | ## TL;DR 8 | 9 | Window functions en Spark = window functions en SQL pero código Python. Usa `Window.partitionBy().orderBy()` para definir ventana, luego aplica `row_number()`, `rank()`, `dense_rank()`, `lag()`, `lead()`, etc. Similar a SQL pero más flexible. Muy usado en transformaciones de datos. 10 | 11 | --- 12 | 13 | ## Concepto Core 14 | 15 | - **Qué es**: Window function calcula valor para cada fila basado en "ventana" de filas (grupo + orden) 16 | - **Por qué importa**: Fundamental en transformaciones. Ranking, sumas acumulativas, lead/lag son muy comunes. Demuestra dominio de Spark 17 | - **Principio clave**: Window = PARTITION BY + ORDER BY. Aplica función dentro de cada ventana 18 | 19 | --- 20 | 21 | ## Memory Trick 22 | 23 | **"Ventanas deslizantes"** — Imagina ventana que se desliza sobre datos. Cada fila tiene su propia ventana (grupo + contexto). La función se aplica en cada ventana. 24 | 25 | --- 26 | 27 | ## Cómo explicarlo en entrevista 28 | 29 | **Paso 1**: "Window functions hacen cálculos 'sobre ventanas' de filas, no solo colapsando grupos" 30 | 31 | **Paso 2**: "Defino ventana con `Window.partitionBy(col).orderBy(col)` — es como GROUP BY + ORDER BY" 32 | 33 | **Paso 3**: "Aplico función window: `row_number()`, `rank()`, `lag()`, `sum().over(window)`" 34 | 35 | **Paso 4**: "Resultado: cada fila original + valor calculado dentro de su ventana" 36 | 37 | --- 38 | 39 | ## Código/Query ejemplo 40 | 41 | ### Datos: Sales por empleado 42 | 43 | | id_empleado | nombre | departamento | monto | fecha | 44 | | ----------- | ------- | ------------ | ----- | ---------- | 45 | | 1 | Alice | Ventas | 1000 | 2024-01-01 | 46 | | 2 | Bob | Ventas | 1500 | 2024-01-02 | 47 | | 3 | Charlie | TI | 2000 | 2024-01-03 | 48 | | 1 | Alice | Ventas | 800 | 2024-01-04 | 49 | | 4 | David | TI | 2500 | 2024-01-05 | 50 | | 2 | Bob | Ventas | 900 | 2024-01-06 | 51 | 52 | --- 53 | 54 | ### Problema 1: Ranking Dentro de Cada Departamento 55 | 56 | ```python 57 | from pyspark.sql import Window 58 | from pyspark.sql.functions import row_number, rank, dense_rank, col 59 | 60 | spark = SparkSession.builder.appName("Window Functions").getOrCreate() 61 | 62 | sales = spark.read.parquet("sales.parquet") 63 | 64 | # Define la ventana: particiona por department, ordena por amount (descendente) 65 | window = Window.partitionBy("departamento").orderBy(col("monto").desc()) 66 | 67 | # Aplica ranking dentro de cada ventana 68 | result = ( 69 | sales 70 | .withColumn("rank_en_dpto", rank().over(window)) 71 | .withColumn("dense_rank_en_dpto", dense_rank().over(window)) 72 | .withColumn("row_number_en_dpto", row_number().over(window)) 73 | .select("id_empleado", "nombre", "departamento", "monto", "rank_en_dpto", "dense_rank_en_dpto", "row_number_en_dpto") 74 | ) 75 | 76 | result.show() 77 | ``` 78 | 79 | **Resultado:** 80 | id_empleado | nombre | departamento | monto | rank_en_dpto | dense_rank_en_dpto | row_number_en_dpto 81 | ---|---|---|---|---|---|--- 82 | 4 | David | TI | 2500 | 1 | 1 | 1 83 | 3 | Charlie | TI | 2000 | 2 | 2 | 2 84 | 2 | Bob | Ventas | 1500 | 1 | 1 | 1 85 | 1 | Alice | Ventas | 1000 | 2 | 2 | 2 86 | 2 | Bob | Ventas | 900 | 3 | 3 | 3 87 | 1 | Alice | Ventas | 800 | 4 | 4 | 4 88 | 89 | --- 90 | 91 | ### Problema 2: Running Total (Suma Acumulativa) 92 | 93 | ```python 94 | # Window: particiona por employee, ordena cronológicamente 95 | window = Window.partitionBy("id_empleado").orderBy("fecha") 96 | 97 | result = ( 98 | sales 99 | .withColumn("total_acumulado", sum("monto").over(window)) 100 | .select("id_empleado", "nombre", "fecha", "monto", "total_acumulado") 101 | ) 102 | 103 | result.show() 104 | ``` 105 | 106 | **Resultado:** 107 | id_empleado | nombre | fecha | monto | total_acumulado 108 | ---|---|---|---|--- 109 | 1 | Alice | 2024-01-01 | 1000 | 1000 110 | 1 | Alice | 2024-01-04 | 800 | 1800 111 | 2 | Bob | 2024-01-02 | 1500 | 1500 112 | 2 | Bob | 2024-01-06 | 900 | 2400 113 | 114 | --- 115 | 116 | ### Problema 3: LAG y LEAD (Fila Anterior/Siguiente) 117 | 118 | ```python 119 | # LAG: valor de fila anterior 120 | # LEAD: valor de fila siguiente 121 | window = Window.partitionBy("id_empleado").orderBy("fecha") 122 | 123 | result = ( 124 | sales 125 | .withColumn("monto_anterior", lag("monto").over(window)) 126 | .withColumn("monto_siguiente", lead("monto").over(window)) 127 | .withColumn("diferencia_monto", col("monto") - col("monto_anterior")) 128 | .select("id_empleado", "fecha", "monto", "monto_anterior", "monto_siguiente", "diferencia_monto") 129 | ) 130 | 131 | result.show() 132 | ``` 133 | 134 | **Resultado:** 135 | id_empleado | fecha | monto | monto_anterior | monto_siguiente | diferencia_monto 136 | ---|---|---|---|---|--- 137 | 1 | 2024-01-01 | 1000 | NULL | 800 | NULL 138 | 1 | 2024-01-04 | 800 | 1000 | NULL | -200 139 | 2 | 2024-01-02 | 1500 | NULL | 900 | NULL 140 | 2 | 2024-01-06 | 900 | 1500 | NULL | -600 141 | 142 | --- 143 | 144 | ### Problema 4: Window con ROWS (Ventanas Específicas) 145 | 146 | ```python 147 | # Window ROWS: especifica rango (últimas 2 filas, etc.) 148 | # UNBOUNDED PRECEDING: desde inicio 149 | # CURRENT ROW: fila actual 150 | # n FOLLOWING: n filas después 151 | # Última suma de 2 filas (incluida actual) 152 | window_2rows = ( 153 | Window 154 | .partitionBy("id_empleado") 155 | .orderBy("fecha") 156 | .rowsBetween(-1, 0) # 1 fila anterior + actual 157 | ) 158 | 159 | result = ( 160 | sales 161 | .withColumn("suma_ultimas_2", sum("monto").over(window_2rows)) 162 | .select("id_empleado", "fecha", "monto", "suma_ultimas_2") 163 | ) 164 | 165 | result.show() 166 | ``` 167 | 168 | **Resultado:** 169 | id_empleado | fecha | monto | suma_ultimas_2 170 | ---|---|---|--- 171 | 1 | 2024-01-01 | 1000 | 1000 (solo actual, no hay anterior) 172 | 1 | 2024-01-04 | 800 | 1800 (1000 + 800) 173 | 2 | 2024-01-02 | 1500 | 1500 (solo actual) 174 | 2 | 2024-01-06 | 900 | 2400 (1500 + 900) 175 | 176 | --- 177 | 178 | ### Problema 5: Top N por Grupo (Patrón Común) 179 | 180 | ```python 181 | # Ranking + Filter = Top N por grupo 182 | window = Window.partitionBy("departamento").orderBy(col("monto").desc()) 183 | 184 | result = ( 185 | sales 186 | .withColumn("rank", rank().over(window)) 187 | .filter(col("rank") <= 2) # Top 2 por departamento 188 | .select("id_empleado", "nombre", "departamento", "monto", "rank") 189 | ) 190 | 191 | result.show() 192 | ``` 193 | 194 | **Resultado:** 195 | id_empleado | nombre | departamento | monto | rank 196 | ---|---|---|---|--- 197 | 4 | David | TI | 2500 | 1 198 | 3 | Charlie | TI | 2000 | 2 199 | 2 | Bob | Ventas | 1500 | 1 200 | 1 | Alice | Ventas | 1000 | 2 201 | 202 | --- 203 | 204 | ## Window Functions Comunes 205 | 206 | | Función | Uso | Ejemplo | 207 | | ------------------- | ----------------- | ------------------------ | 208 | | `row_number()` | Número secuencial | 1, 2, 3, 4 | 209 | | `rank()` | Rank con saltos | 1, 2, 2, 4 | 210 | | `dense_rank()` | Rank sin saltos | 1, 2, 2, 3 | 211 | | `lag(col, offset)` | Fila anterior | Valor de -1 fila | 212 | | `lead(col, offset)` | Fila siguiente | Valor de +1 fila | 213 | | `sum(col).over()` | Suma acumulativa | Suma hasta fila actual | 214 | | `avg(col).over()` | Promedio ventana | Promedio en ventana | 215 | | `max(col).over()` | Máximo ventana | Máximo en ventana | 216 | | `min(col).over()` | Mínimo ventana | Mínimo en ventana | 217 | | `count(col).over()` | Contar ventana | Cuántas filas en ventana | 218 | | `first(col).over()` | Primer valor | Valor de primera fila | 219 | | `last(col).over()` | Último valor | Valor de última fila | 220 | 221 | --- 222 | 223 | ## Window Specifications 224 | 225 | ```python 226 | # Ventana básica: todo el dataset, sin partición 227 | window_all = Window.orderBy("fecha") 228 | 229 | # Partición única: grupo + orden 230 | window_dept = Window.partitionBy("departamento").orderBy(col("monto").desc()) 231 | 232 | # Múltiples particiones: grupo por 2+ columnas 233 | window_multi = Window.partitionBy("departamento", "region").orderBy("fecha") 234 | 235 | # Sin orden: solo partición 236 | window_no_order = Window.partitionBy("departamento") 237 | 238 | # ROWS specification: cuántas filas antes/después 239 | window_rows = ( 240 | Window 241 | .partitionBy("departamento") 242 | .orderBy("fecha") 243 | .rowsBetween(-2, 1) # 2 filas antes + actual + 1 fila después 244 | ) 245 | 246 | # RANGE specification: por valor, no filas 247 | window_range = ( 248 | Window 249 | .partitionBy("departamento") 250 | .orderBy("monto") 251 | .rangeBetween(-100, 100) # Rango ±100 de monto actual 252 | ) 253 | ``` 254 | 255 | --- 256 | 257 | ## Errores comunes en entrevista 258 | 259 | - **Error**: Olvidar `orderBy()` en window → **Solución**: `partitionBy()` sin `orderBy()` da orden arbitrario. Siempre especifica ambos 260 | 261 | - **Error**: Usar `rank()` cuando necesitas `row_number()` → **Solución**: `rank()` salta (1,2,2,4), `row_number()` secuencial (1,2,3,4). Conoce diferencia 262 | 263 | - **Error**: No filtrar después de window function → **Solución**: Top N requiere `rank().over() then filter(rank <= N)` 264 | 265 | - **Error**: Aplicar window a columna que no existe → **Solución**: Asegúrate que columna está en select antes de window 266 | 267 | --- 268 | 269 | ## Preguntas de seguimiento típicas 270 | 271 | 1. **"¿Diferencia entre `rowsBetween` y `rangeBetween`?"** 272 | - `rowsBetween`: Contar filas (1 anterior, actual, 1 siguiente) 273 | - `rangeBetween`: Rango de valores (valores dentro de rango) 274 | 275 | 2. **"¿Cómo haces running total por mes?"** 276 | - Particiona por mes: `Window.partitionBy(month()).orderBy(fecha)` 277 | - Reset automático cada mes 278 | 279 | 3. **"¿Puedes aplicar múltiples window functions?"** 280 | - Sí: `.withColumn("rank", rank().over(w1)).withColumn("lag", lag().over(w2))` 281 | 282 | 4. **"¿Performance de window functions?"** 283 | - Shuffle ocurre en `partitionBy`. Minimiza particiones si posible 284 | - `rowsBetween` es más rápido que `rangeBetween` 285 | 286 | --- 287 | 288 | ## Real-World: Customer Lifetime Value 289 | 290 | ```python 291 | # Cálculo: última compra + total gasto + ranking por valor 292 | window_customer = Window.partitionBy("id_cliente").orderBy(col("fecha").desc()) 293 | window_all = Window.orderBy(col("total_gastado").desc()) 294 | 295 | result = ( 296 | sales 297 | .groupBy("id_cliente") 298 | .agg( 299 | sum("monto").alias("total_gastado"), 300 | max("fecha").alias("ultima_compra"), 301 | count("*").alias("num_compras") 302 | ) 303 | .withColumn("dias_desde_ultima_compra", 304 | datediff(current_date(), col("ultima_compra"))) 305 | .withColumn("rank_cliente", rank().over(window_all)) 306 | .filter(col("rank_cliente") <= 100) 307 | .orderBy("rank_cliente") 308 | ) 309 | 310 | result.show() 311 | ``` 312 | 313 | --- 314 | 315 | ## References 316 | 317 | - [Window Functions - PySpark Docs](https://spark.apache.org/docs/latest/api/python/reference/pyspark.sql/functions.html#window-functions) 318 | - [Window Class - API](https://spark.apache.org/docs/latest/api/python/reference/pyspark.sql/window.html) 319 | -------------------------------------------------------------------------------- /python-dsa/01-strings-text-processing.md: -------------------------------------------------------------------------------- 1 | # Strings & Text Processing en Python 2 | 3 | **Tags**: #python #strings #regex #text-processing #data-cleaning #real-interview 4 | 5 | --- 6 | 7 | ## TL;DR 8 | 9 | Strings en Python: immutable, indexable, iterable. Métodos útiles: `.split()`, `.join()`, `.strip()`, `.replace()`. Regex: `re.findall()`, `re.sub()`, `.match()` para patrones complejos. Para data: validación (emails, phones), parsing (logs, CSVs), normalizacion (lowercase, trim, remove special chars). Performance: strings son O(n), usa `.join()` en loops, no `+`. 10 | 11 | --- 12 | 13 | ## Concepto Core 14 | 15 | - **Qué es**: Strings son secuencias de caracteres. En data: logs, emails, nombres, IDs → todos strings 16 | - **Por qué importa**: 80% de data cleaning es string manipulation. Regex salva horas. Demuestra idiomaticity en Python 17 | - **Principio clave**: Strings inmutables → siempre creas nuevos. Para loops, `.join()` > `+` 18 | 19 | --- 20 | 21 | ## Memory Trick 22 | 23 | **"Inspector de datos"** — Strings son tus herramientas para inspeccionar, validar, limpiar datos. Regex es tu lupa. 24 | 25 | --- 26 | 27 | ## Cómo explicarlo en entrevista 28 | 29 | **Paso 1**: "Strings son fundamentales. 90% de data es strings inicialmente (logs, names, emails)" 30 | 31 | **Paso 2**: "Python tiene métodos built-in (split, strip, replace) y regex para patterns" 32 | 33 | **Paso 3**: "Para performance: join() es 100x más rápido que + en loops" 34 | 35 | **Paso 4**: "Validación: regex para emails, phones. Parsing: regex para logs, CSVs" 36 | 37 | --- 38 | 39 | ## Código/Ejemplos 40 | 41 | ### Parte 1: String Basics 42 | 43 | ```python 44 | # ===== BASICS ===== 45 | s = "Hello, World!" 46 | 47 | # Indexing (0-indexed) 48 | print(s[0]) # 'H' 49 | print(s[-1]) # '!' 50 | print(s[0:5]) # 'Hello' 51 | 52 | # Methods: Built-in 53 | print(s.lower()) # 'hello, world!' 54 | print(s.upper()) # 'HELLO, WORLD!' 55 | print(s.strip()) # Remove leading/trailing whitespace 56 | print(s.replace("World", "Python")) # 'Hello, Python!' 57 | print(s.split(", ")) # ['Hello', 'World!'] 58 | 59 | # ===== f-STRINGS (Modern Python) ===== 60 | name = "Alice" 61 | age = 28 62 | print(f"{name} is {age} years old") # 'Alice is 28 years old' 63 | print(f"{name.upper()} is {age * 2} in two years") # 'ALICE is 56 in two years' 64 | 65 | # ===== STRING OPERATIONS ===== 66 | # Concatenation (slow in loops!) 67 | result = "" 68 | for word in ["Python", "is", "awesome"]: 69 | result += word + " " # ❌ BAD: Creates new string each iteration 70 | print(result) 71 | 72 | # Better: join() 73 | result = " ".join(["Python", "is", "awesome"]) # ✅ GOOD: One allocation 74 | print(result) 75 | ``` 76 | 77 | --- 78 | 79 | ### Parte 2: Validation & Parsing 80 | 81 | ```python 82 | import re 83 | 84 | # ===== EMAIL VALIDATION ===== 85 | def is_valid_email(email): 86 | pattern = r'^[a-zA-Z0-9.*%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' 87 | return bool(re.match(pattern, email)) 88 | 89 | print(is_valid_email("alice@example.com")) # True 90 | print(is_valid_email("alice@example")) # False 91 | print(is_valid_email("alice.smith@example.co.uk")) # True 92 | 93 | # ===== PHONE VALIDATION ===== 94 | def is_valid_phone(phone): 95 | pattern = r'^(+1)?[-.\s]?\(?([0-9]{3})?\)?[-.\s]?[0-9]{3}[-.\s]?[0-9]{4}$' 96 | return bool(re.match(pattern, phone)) 97 | 98 | print(is_valid_phone("555-1234-5678")) # True 99 | print(is_valid_phone("+1 (555) 1234-5678")) # True 100 | print(is_valid_phone("555")) # False 101 | 102 | # ===== IP ADDRESS VALIDATION ===== 103 | def is_valid_ipv4(ip): 104 | pattern = r'^(\d{1,3}\.){3}\d{1,3}$' 105 | if not re.match(pattern, ip): 106 | return False 107 | parts = ip.split('.') 108 | return all(0 <= int(p) <= 255 for p in parts) 109 | 110 | print(is_valid_ipv4("192.168.1.1")) # True 111 | print(is_valid_ipv4("192.168.1.256")) # False 112 | ``` 113 | 114 | --- 115 | 116 | ### Parte 3: Data Cleaning 117 | 118 | ```python 119 | # ===== CLEANING LOGS ===== 120 | log_line = " [ERROR] 2024-01-15 10:30:45 Database connection failed " 121 | 122 | # Step 1: Strip whitespace 123 | cleaned = log_line.strip() 124 | 125 | # Step 2: Extract level 126 | level_match = re.search(r'^\[(\w+)\]', cleaned) 127 | level = level_match.group(1) if level_match else "UNKNOWN" 128 | 129 | # Step 3: Extract timestamp 130 | timestamp_match = re.search(r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})', cleaned) 131 | timestamp = timestamp_match.group(1) if timestamp_match else None 132 | 133 | # Step 4: Extract message 134 | message_match = re.search(r'\] (.+)$', cleaned) 135 | message = message_match.group(1).strip() if message_match else "" 136 | 137 | print(f"Level: {level}, Time: {timestamp}, Message: {message}") 138 | # Output: Level: ERROR, Time: 2024-01-15 10:30:45, Message: Database connection failed 139 | 140 | # ===== PARSING CSV (poor man's approach) ===== 141 | csv_line = 'Alice,"123 Main St",alice@example.com,28' 142 | 143 | # Split by comma (naive approach, fails if comma in quoted fields) 144 | fields_naive = csv_line.split(',') # ❌ Wrong if commas in data 145 | 146 | # Better: Use csv module or handle quotes 147 | import csv 148 | import io 149 | 150 | reader = csv.reader(io.StringIO(csv_line)) 151 | fields = next(reader) 152 | print(fields) # ['Alice', '123 Main St', 'alice@example.com', '28'] 153 | 154 | # ===== NORMALIZE NAMES ===== 155 | names = [" JOHN SMITH ", "jane_doe", "robert.johnson", "Maria-Garcia"] 156 | 157 | def normalize_name(name): 158 | # Strip whitespace 159 | name = name.strip() 160 | 161 | # Convert to title case 162 | name = name.title() 163 | 164 | # Remove underscores, dots, hyphens 165 | name = re.sub(r'[_.\-]', ' ', name) 166 | 167 | # Remove multiple spaces 168 | name = re.sub(r'\s+', ' ', name) 169 | return name 170 | 171 | normalized = [normalize_name(n) for n in names] 172 | print(normalized) 173 | # ['John Smith', 'Jane Doe', 'Robert Johnson', 'Maria Garcia'] 174 | ``` 175 | 176 | --- 177 | 178 | ### Parte 4: Regex Patterns (Common) 179 | 180 | ```python 181 | import re 182 | 183 | # ===== FINDALL: Encontrar todas ocurrencias ===== 184 | text = "Emails: alice@example.com, bob@test.org, charlie@company.co.uk" 185 | 186 | emails = re.findall(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', text) 187 | print(emails) 188 | # ['alice@example.com', 'bob@test.org', 'charlie@company.co.uk'] 189 | 190 | # ===== SUB: Reemplazar patrones ===== 191 | phone = "Call me at 555-123-4567 or 555.987.6543" 192 | 193 | # Replace all phone formats with [REDACTED] 194 | redacted = re.sub(r'\d{3}[-.]?\d{3}[-.]?\d{4}', '[PHONE]', phone) 195 | print(redacted) 196 | # 'Call me at [PHONE] or [PHONE]' 197 | 198 | # ===== SPLIT con regex ===== 199 | text = "apple, banana; orange | grape" 200 | 201 | # Split by comma, semicolon, or pipe 202 | fruits = re.split(r'[,;|]', text) 203 | print(fruits) 204 | # ['apple', ' banana', ' orange ', ' grape'] 205 | 206 | # Clean up 207 | fruits = [f.strip() for f in fruits] 208 | print(fruits) 209 | # ['apple', 'banana', 'orange', 'grape'] 210 | 211 | # ===== GROUPS: Extraer partes ===== 212 | date_str = "2024-01-15" 213 | 214 | match = re.match(r'(\d{4})-(\d{2})-(\d{2})', date_str) 215 | if match: 216 | year, month, day = match.groups() 217 | print(f"Year: {year}, Month: {month}, Day: {day}") 218 | # Year: 2024, Month: 01, Day: 15 219 | ``` 220 | 221 | --- 222 | 223 | ### Parte 5: Performance (Importante!) 224 | 225 | ```python 226 | import time 227 | 228 | # ❌ SLOW: String concatenation in loop 229 | def slow_join(): 230 | result = "" 231 | for i in range(100000): 232 | result += f"Item {i}, " 233 | return result 234 | 235 | # ✅ FAST: Using join() 236 | def fast_join(): 237 | items = [f"Item {i}" for i in range(100000)] 238 | return ", ".join(items) 239 | 240 | # Benchmark 241 | start = time.time() 242 | slow_join() 243 | slow_time = time.time() - start 244 | 245 | start = time.time() 246 | fast_join() 247 | fast_time = time.time() - start 248 | 249 | print(f"Slow: {slow_time:.3f}s") # ~0.5s (slow!) 250 | print(f"Fast: {fast_time:.3f}s") # ~0.01s (100x faster!) 251 | 252 | # ===== RULE ===== 253 | # For loops with string concat: ALWAYS use list + join(), not += 254 | ``` 255 | 256 | --- 257 | 258 | ## Regex Patterns Útiles (Copy-Paste) 259 | 260 | ```python 261 | patterns = { 262 | "email": r'^[a-zA-Z0-9.%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', 263 | "phone_us": r'^(+1)?[-.\s]?\(?([0-9]{3})?\)?[-.\s]?[0-9]{3}[-.\s]?[0-9]{4}$', 264 | "ipv4": r'^(\d{1,3}\.){3}\d{1,3}$', 265 | "url": r'https?://[^\s]+', 266 | "date_iso": r'\d{4}-\d{2}-\d{2}', 267 | "hex_color": r'^#[0-9A-Fa-f]{6}$', 268 | "credit_card": r'^\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}$', 269 | "ssn": r'^\d{3}-\d{2}-\d{4}$', 270 | "uuid": r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', 271 | "username": r'^[a-zA-Z0-9]{3,16}$', 272 | } 273 | ``` 274 | 275 | --- 276 | 277 | ## Errores Comunes en Entrevista 278 | 279 | - **Error**: Usar `+` en loops para strings → **Solución**: `.join()` siempre 280 | 281 | - **Error**: Forgetting about regex groups → **Solución**: `match.group(1)`, `match.groups()` son potentes 282 | 283 | - **Error**: Not handling edge cases (None, empty, special chars) → **Solución**: Valida input primero 284 | 285 | - **Error**: Regex pattern es demasiado permisivo → **Solución**: Test con casos edge (edge@example, +++, etc) 286 | 287 | --- 288 | 289 | ## Preguntas de Seguimiento 290 | 291 | 1. **"¿Cuándo usas regex vs built-in methods?"** 292 | - `.split()`, `.replace()` para casos simples 293 | - Regex para patrones complejos 294 | 295 | 2. **"¿Performance de regex?"** 296 | - Regex es lento vs built-in 297 | - Pero correctness > speed (en validation) 298 | 299 | 3. **"¿Cómo debuggeas regex?"** 300 | - regex101.com (online tester) 301 | - Test strings systematically 302 | 303 | 4. **"¿Encoding issues (UTF-8, Unicode)?"** 304 | - Python 3 = Unicode por defecto (safe) 305 | - Python 2 = `.decode('utf-8')` (legacy) 306 | 307 | --- 308 | 309 | ## Real-World: Data Cleaning Pipeline 310 | 311 | ```python 312 | def clean_customer_data(raw_data): 313 | """Clean y validate customer records""" 314 | cleaned = [] 315 | errors = [] 316 | 317 | for record in raw_data: 318 | try: 319 | name = record['name'].strip().title() 320 | 321 | # Validate email 322 | email = record['email'].lower().strip() 323 | if not is_valid_email(email): 324 | raise ValueError(f"Invalid email: {email}") 325 | 326 | # Validate phone 327 | phone = record['phone'] 328 | if phone and not is_valid_phone(phone): 329 | raise ValueError(f"Invalid phone: {phone}") 330 | 331 | # Clean address (remove extra spaces) 332 | address = re.sub(r'\s+', ' ', record['address'].strip()) 333 | 334 | cleaned.append({ 335 | 'name': name, 336 | 'email': email, 337 | 'phone': phone, 338 | 'address': address 339 | }) 340 | 341 | except ValueError as e: 342 | errors.append(f"Record {record['id']}: {str(e)}") 343 | 344 | return cleaned, errors 345 | 346 | # Usage 347 | raw_records = [ 348 | {'id': 1, 'name': ' ALICE SMITH ', 'email': 'ALICE@EXAMPLE.COM', 'phone': '555-123-4567', 'address': '123 Main St'}, 349 | {'id': 2, 'name': 'bob jones', 'email': 'invalid-email', 'phone': '555.987.6543', 'address': '456 Oak Ave'}, 350 | ] 351 | 352 | cleaned, errors = clean_customer_data(raw_records) 353 | print(f"Cleaned: {len(cleaned)}, Errors: {len(errors)}") 354 | # Cleaned: 1, Errors: 1 355 | ``` 356 | 357 | --- 358 | 359 | ## References 360 | 361 | - [Python Strings - Official Docs](https://docs.python.org/3/tutorial/datastructures.html#strings) 362 | - [Regex Module - re documentation](https://docs.python.org/3/library/re.html) 363 | -------------------------------------------------------------------------------- /pyspark/03-optimization-caching.md: -------------------------------------------------------------------------------- 1 | # PySpark Optimization & Caching Strategies 2 | 3 | **Tags**: #pyspark #optimization #caching #performance #senior #real-interview 4 | 5 | --- 6 | 7 | ## TL;DR 8 | 9 | Optimiza Spark con: (1) **Partitioning** — divide datos por clave, (2) **Caching** — reutiliza RDDs caros, (3) **Broadcasting** — reparte variables pequeñas a workers, (4) **Bucketing** — hash-basado, (5) **Predicate Pushdown** — filtra temprano. Monitorea con `.explain()` y Spark UI. 10 | 11 | --- 12 | 13 | ## Concepto Core 14 | 15 | - **Qué es**: Optimization es hacer queries correr más rápido. Caching reutiliza datos costosos. Broadcasting minimiza network traffic 16 | - **Por qué importa**: Diferencia entre query en 5min vs 1hr. Critical para data engineers. Demuestra madurez 17 | - **Principio clave**: Identifica bottlenecks (shuffle, network), aplica técnica apropiada 18 | 19 | --- 20 | 21 | ## Memory Trick 22 | 23 | **"Fábrica eficiente"**: 24 | 25 | - Partitioning = producción distribuida (cada factory = parte de datos) 26 | - Caching = almacén temporal (reutiliza products populares) 27 | - Broadcasting = manuales en cada factory (información pequeña, no transmite) 28 | 29 | --- 30 | 31 | ## Cómo explicarlo en entrevista 32 | 33 | **Paso 1**: "Spark es distribuido. El bottleneck = network (shuffle). Optimización = minimizar shuffle" 34 | 35 | **Paso 2**: "Técnicas: Partitioning (menos shuffle), Caching (reutiliza), Broadcasting (info pequeña sin red)" 36 | 37 | **Paso 3**: "Siempre usa `.explain()` para identificar DONDE está el problema. Luego aplica técnica" 38 | 39 | **Paso 4**: "Monitor en Spark UI: busca long-running tasks. Eso = área a optimizar" 40 | 41 | --- 42 | 43 | ## Código/Query ejemplo 44 | 45 | ### Escenario: Join Gigante (Problema típico) 46 | 47 | **Datos:** 48 | 49 | - orders: 1 millón filas (1 GB) — distribuidao 50 | - customers: 1 millón filas (500 MB) — pequeña 51 | - products: 100k filas (50 MB) — muy pequeña 52 | 53 | ```python 54 | from pyspark.sql import SparkSession 55 | from pyspark.sql.functions import col, broadcast 56 | 57 | spark = SparkSession.builder.appName("Optimization").getOrCreate() 58 | 59 | orders = spark.read.parquet("orders/") # 1 GB 60 | customers = spark.read.parquet("customers/") # 500 MB 61 | products = spark.read.parquet("products/") # 50 MB 62 | 63 | # ❌ PROBLEMA: Join sin optimización 64 | slow_result = ( 65 | orders 66 | .join(customers, "customer_id") # Shuffle gigante: 1GB ↔ 500MB 67 | .join(products, "product_id") # Shuffle de nuevo 68 | ) 69 | 70 | print("Tiempo esperado: 15+ minutos (shuffle masivo)") 71 | ``` 72 | 73 | **Lo que pasa sin optimización:** 74 | 75 | ``` 76 | Shuffle orders por customer_id (1GB enviado por red) 77 | ↓ 78 | Shuffle customers por customer_id (500MB enviado por red) 79 | ↓ 80 | Match local (finalmente) 81 | ↓ 82 | Repeat para products 83 | ``` 84 | 85 | --- 86 | 87 | ### ✅ Opción 1: Broadcasting (Recomendado para este caso) 88 | 89 | ```python 90 | # products es pequeña (50 MB). Broadcast a todos workers 91 | fast_result = ( 92 | orders 93 | .join(broadcast(customers), "customer_id") # ← Broadcast 500MB a cada worker 94 | .join(broadcast(products), "product_id") # ← Broadcast 50MB a cada worker 95 | ) 96 | 97 | # Tiempo: 1-2 minutos 98 | # Qué pasó: Cada worker tiene copia de customers+products, local join 99 | ``` 100 | 101 | **Timeline:** 102 | 103 | ``` 104 | BROADCAST (instantáneo, < 1 GB cada): 105 | customers (500MB) enviado 1 sola vez a cada worker 106 | products (50MB) enviado 1 sola vez a cada worker 107 | 108 | JOIN (local, rápido): 109 | En cada worker: orders [local] ← join → customers [local] ← join → products [local] 110 | ``` 111 | 112 | --- 113 | 114 | ### ✅ Opción 2: Partitioning (Para joins recurrentes) 115 | 116 | ```python 117 | # Si haces este join muchas veces, particiona 118 | customers_partitioned = customers.repartition(col("customer_id")) 119 | products_partitioned = products.repartition(col("product_id")) 120 | 121 | # Cacheamos particiones 122 | customers_partitioned.cache() 123 | products_partitioned.cache() 124 | 125 | result = ( 126 | orders 127 | .join(customers_partitioned, "customer_id") # Menos shuffle 128 | .join(products_partitioned, "product_id") 129 | ) 130 | 131 | # Primera ejecución: 5 minutos (partitioning + caching) 132 | # Siguientes: 1 minuto (usa cache) 133 | ``` 134 | 135 | --- 136 | 137 | ### ✅ Opción 3: Bucketing (Pre-partitioned en Storage) 138 | 139 | ```python 140 | # Para queries recurrentes, guarda datos bucketed 141 | # Writes ONE TIME: 142 | customers.write.mode("overwrite").bucketBy(10, "customer_id").saveAsTable("customers_bucketed") 143 | products.write.mode("overwrite").bucketBy(5, "product_id").saveAsTable("products_bucketed") 144 | 145 | # Luego: 146 | customers_b = spark.table("customers_bucketed") 147 | products_b = spark.table("products_bucketed") 148 | 149 | result = orders.join(customers_b, "customer_id").join(products_b, "product_id") 150 | 151 | # Spark SABE que ya está bucketed, no hace shuffle (sorttable merge join) 152 | # Tiempo: 1-2 minutos SIEMPRE (óptimo) 153 | ``` 154 | 155 | --- 156 | 157 | ## Caching Strategies 158 | 159 | ### Cuándo Cachear 160 | 161 | ```python 162 | # ❌ NO cachear todo 163 | result.cache() # Desperdicia memoria 164 | 165 | # ✅ Cachear transformations CARAS 166 | parsed_data = raw_data.flatMap(expensive_parsing_function) 167 | parsed_data.cache() 168 | 169 | # Usado múltiples veces: 170 | result1 = parsed_data.filter(...) 171 | result2 = parsed_data.groupBy(...) 172 | result3 = parsed_data.join(...) 173 | ``` 174 | 175 | --- 176 | 177 | ### Storage Levels 178 | 179 | ```python 180 | from pyspark.storagelevel import StorageLevel 181 | 182 | # MEMORY (rápido, puede faltar espacio) 183 | df.cache() # Equivalente a MEMORY 184 | df.persist(StorageLevel.MEMORY_ONLY) 185 | 186 | # MEMORY_AND_DISK (safe, más lento) 187 | df.persist(StorageLevel.MEMORY_AND_DISK) 188 | 189 | # DISK_ONLY (último recurso) 190 | df.persist(StorageLevel.DISK_ONLY) 191 | 192 | # MEMORY_SERIALIZED (comprimido, más lento pero compacto) 193 | df.persist(StorageLevel.MEMORY_ONLY_SER) 194 | 195 | # Recomendación: MEMORY_AND_DISK para data > 1GB 196 | ``` 197 | 198 | --- 199 | 200 | ### Uncache 201 | 202 | ```python 203 | # Liberar memoria después de usar 204 | df.unpersist() 205 | 206 | # Lazy unpersist (después de siguiente action) 207 | df.unpersist(blocking=False) 208 | ``` 209 | 210 | --- 211 | 212 | ## Broadcasting Variables 213 | 214 | Para datos pequeños (< 2GB): 215 | 216 | ```python 217 | # Diccionario de config (pequeño) 218 | config = {"USD": 1.0, "EUR": 0.85, "GBP": 0.73} 219 | 220 | # SIN broadcast (❌ ineficiente) 221 | def convert_currency(row): 222 | # Accede a 'config' en cada worker (ineficiente) 223 | return config.get(row.currency) 224 | 225 | result = df.rdd.map(convert_currency) 226 | 227 | # CON broadcast (✅ eficiente) 228 | broadcast_config = spark.broadcast(config) 229 | 230 | def convert_currency_optimized(row): 231 | # Accede a copia local en worker 232 | return broadcast_config.value.get(row.currency) 233 | 234 | result = df.rdd.map(convert_currency_optimized) 235 | ``` 236 | 237 | --- 238 | 239 | ## Partitioning Strategies 240 | 241 | ### Repartition vs Coalesce 242 | 243 | ```python 244 | # REPARTITION: Siempre rehash (shuffle) 245 | df.repartition(100) # Útil si aumentas particiones 246 | 247 | # COALESCE: Merge particiones sin shuffle 248 | df.coalesce(10) # Útil si disminuyes particiones (10x más rápido) 249 | 250 | # Regla: coalesce si menos particiones, repartition si más 251 | ``` 252 | 253 | --- 254 | 255 | ### Partitioning Smart 256 | 257 | ```python 258 | # ¿Cuántas particiones? 259 | # Regla: 1 partition = 128 MB idealmente 260 | # Para 1 GB de datos: 261 | df.repartition(10) # ≈ 100 MB por partición (óptimo) 262 | 263 | # Para 10 GB: 264 | df.repartition(100) # ≈ 100 MB por partición 265 | 266 | # Demasiadas particiones = overhead 267 | # Pocas particiones = bajo paralelismo 268 | ``` 269 | 270 | --- 271 | 272 | ## Identify Bottlenecks 273 | 274 | ### Via `.explain()` 275 | 276 | ```python 277 | # Modo simple (básico) 278 | df.explain(mode="simple") 279 | 280 | # Modo extended (Catalyst optimizations) 281 | df.explain(mode="extended") 282 | ``` 283 | 284 | Busca estos problemas: 285 | 286 | - Exchange (SHUFFLE) - caro 287 | - Cartesian Product - muy caro 288 | - Broadcast Join vs Sort Merge Join 289 | 290 | ```python 291 | result = orders.join(customers, "customer_id") 292 | result.explain(mode="extended") 293 | ``` 294 | 295 | Si ves "Exchange" → Reduce con broadcast/partition 296 | Si ves "Cartesian" → Hay bug en join condition 297 | 298 | --- 299 | 300 | ### Via Spark UI 301 | 302 | **URL:** `http://localhost:4040` (durante ejecución) 303 | 304 | **Qué ver:** 305 | 306 | - **Stages**: Cuál es más lento? 307 | - **Tasks**: Task que corre > 1 minuto? 308 | - **Shuffle**: Cuántos bytes se movieron? 309 | - **Skewed**: Un task mucho más lento que otros? 310 | 311 | **Acción:** 312 | 313 | - Task lento = aumento particiones en ese stage 314 | - Shuffle grande = considera broadcast/bucketing 315 | 316 | --- 317 | 318 | ## Real-World: Full Optimization Pipeline 319 | 320 | ```python 321 | # Escenario: Daily report, 10 GB orders, 1 GB customers 322 | orders = spark.read.parquet("orders/daily") 323 | customers = spark.read.parquet("customers/") 324 | 325 | # Step 1: Broadcast small dimension 326 | customers_bcast = broadcast(customers) 327 | 328 | # Step 2: Partition orders by date (pre-filter) 329 | orders_filtered = orders.filter(col("date") >= "2024-01-01") 330 | 331 | # Step 3: Join with broadcast 332 | joined = orders_filtered.join(customers_bcast, "customer_id") 333 | 334 | # Step 4: Cache before multiple aggregations 335 | joined.cache() 336 | 337 | # Step 5: Multiple aggregations (usan cache) 338 | by_customer = joined.groupBy("customer_id").agg(sum("amount")) 339 | by_product = joined.groupBy("product").agg(count("*")) 340 | by_date = joined.groupBy("date").agg(avg("amount")) 341 | 342 | # Step 6: Write results 343 | by_customer.write.parquet("output/by_customer") 344 | by_product.write.parquet("output/by_product") 345 | by_date.write.parquet("output/by_date") 346 | 347 | # Performance: 348 | # - Sin optimización: 30 minutos 349 | # - Con broadcasts: 10 minutos 350 | # - Con caching: 5 minutos (primero) + 1 min (otros) 351 | ``` 352 | 353 | --- 354 | 355 | ## Errores comunes en entrevista 356 | 357 | - **Error**: Cachear DataFrame gigante (no cabe en memoria) → **Solución**: Usa `StorageLevel.MEMORY_AND_DISK` 358 | 359 | - **Error**: Usar `collect()` después de caching → **Solución**: `.show()` o `.write()` (evita OOM) 360 | 361 | - **Error**: No usar broadcast para joins pequeños → **Solución**: 2GB → broadcast automático, < 2GB → manual broadcast 362 | 363 | - **Error**: No uncache después de usar → **Solución**: Libera memoria: `df.unpersist()` 364 | 365 | --- 366 | 367 | ## Preguntas de seguimiento típicas 368 | 369 | 1. **"¿Cuándo usas broadcast vs partitioning?"** 370 | - Broadcast: 1 tabla pequeña, múltiples tables grandes 371 | - Partitioning: recurrente, > 10 GB total 372 | 373 | 2. **"¿Cómo detectas skewed tasks?"** 374 | - Spark UI: ver si un task toma 10x más tiempo 375 | - Solución: repartition data de manera más equitativa 376 | 377 | 3. **"¿Cuántas particiones debería usar?"** 378 | - Regla: `cores * 2` a `cores * 4` 379 | - O: ~100 MB por partición 380 | 381 | 4. **"Diferencia entre cache() y checkpoint()?"** 382 | - cache(): memoria, rápido, perdible si ejecutor falla 383 | - checkpoint(): disk, lento, persistente (para RDDs problematic) 384 | 385 | --- 386 | 387 | ## References 388 | 389 | - [Caching & Persistence - Spark Docs](https://spark.apache.org/docs/latest/rdd-programming-guide.html#rdd-persistence) 390 | - [Broadcasting - Spark Docs](https://spark.apache.org/docs/latest/rdd-programming-guide.html#broadcast-variables) 391 | - [Bucketing - Spark SQL](https://spark.apache.org/docs/latest/sql-data-sources-parquet.html#partition-discovery) 392 | -------------------------------------------------------------------------------- /cloud/01-aws-data-stack.md: -------------------------------------------------------------------------------- 1 | # AWS Data Stack para Data Engineering 2 | 3 | **Tags**: #cloud #aws #s3 #redshift #emr #lambda #real-interview 4 | 5 | --- 6 | 7 | ## TL;DR 8 | 9 | AWS data stack: **S3 (storage)** → **EC2/EMR (compute)** → **Redshift (warehouse)** → **Lambda (functions)** 10 | Servicios clave: S3 (barato, duradero), EMR (Spark managed), Redshift (OLAP warehouse), Lambda (serverless), Glue (ETL), Kinesis (streaming) 11 | Trade-off: Costo vs Conveniencia vs Control. 12 | Mejor: S3 para storage, EMR para batch, Redshift para analytics. 13 | 14 | --- 15 | 16 | ## Concepto Core 17 | 18 | - Qué es: AWS = infraestructura como servicio. Puedes armar cualquier pipeline. 19 | - Por qué importa: AWS es industry standard. 40% de data engineers usan AWS. 20 | - Principio clave: Storage (S3) ≠ Compute (EMR). Separados → flexible. 21 | 22 | --- 23 | 24 | ## Cómo explicarlo en entrevista 25 | 26 | 1. "AWS data stack = S3 (storage) + EMR (compute) + Redshift (warehouse)" 27 | 2. "S3 es data lake. EMR corre Spark. Redshift es warehouse. Separación = flexible" 28 | 3. "Alternativas: Glue (managed ETL), Lambda (serverless), Kinesis (streaming)" 29 | 4. "Trade-off: Control/Flexibilidad vs Conveniencia vs Costo" 30 | 31 | --- 32 | 33 | ## Arquitectura 34 | 35 | ``` 36 | ┌──────────────────────────────────────┐ 37 | │ DATA SOURCES │ 38 | │ ├─ Databases (RDS) │ 39 | │ ├─ APIs │ 40 | │ ├─ On-prem systems │ 41 | │ └─ 3rd party SaaS │ 42 | └───────────────┬──────────────────────┘ 43 | │ 44 | ▼ 45 | ┌──────────────────────────────────────┐ 46 | │ AWS INGESTION │ 47 | │ ├─ Direct Connect (fast) │ 48 | │ ├─ S3 Transfer Acceleration │ 49 | │ ├─ Kinesis (real-time) │ 50 | │ └─ Database Migration Service (DMS) │ 51 | └───────────────┬──────────────────────┘ 52 | │ 53 | ▼ 54 | ┌──────────────────────────────────────┐ 55 | │ S3 DATA LAKE (Raw) │ 56 | │ ├─ s3://data-lake/raw/ │ 57 | │ ├─ Partitioned by source/date │ 58 | │ └─ Lifecycle policies (archive old) │ 59 | └───────────────┬──────────────────────┘ 60 | │ 61 | ┌────────┴───────────┐ 62 | │ │ 63 | ▼ ▼ 64 | ┌─────────────┐ ┌──────────────┐ 65 | │ EMR Cluster │ │ AWS Glue │ 66 | │ (Spark) │ │ (Managed ETL)│ 67 | └──────┬──────┘ └──────┬───────┘ 68 | │ │ 69 | └────────┬───────────┘ 70 | │ 71 | ▼ 72 | ┌──────────────────────────────────────┐ 73 | │ S3 DATA LAKE (Processed) │ 74 | │ ├─ Parquet format │ 75 | │ ├─ Partitioned smart │ 76 | │ └─ Ready for analytics │ 77 | └───────────────┬──────────────────────┘ 78 | │ 79 | ▼ 80 | ┌──────────────────────────────────────┐ 81 | │ REDSHIFT WAREHOUSE │ 82 | │ ├─ Loaded via COPY command │ 83 | │ ├─ Optimized for OLAP queries │ 84 | │ └─ Ready for BI tools │ 85 | └───────────────┬──────────────────────┘ 86 | │ 87 | ┌────────┴───────────┐ 88 | │ │ 89 | ▼ ▼ 90 | ┌──────────────┐ ┌──────────────┐ 91 | │ Dashboards │ │ Analysts │ 92 | │ (Tableau) │ │ SQL queries │ 93 | └──────────────┘ └──────────────┘ 94 | ``` 95 | 96 | --- 97 | 98 | ## Servicios Clave 99 | 100 | ### 1. S3 (Simple Storage Service) 101 | 102 | **Qué:** Object storage (blobs, no files) 103 | **Por qué:** Barato ($0.023/GB/month), duradero (99.999999999%), escalable 104 | **Contras:** Eventually consistent, latencia ≈100ms 105 | 106 | **Pricing:** 107 | 108 | - Storage: $0.023/GB/month 109 | - Transfer out: $0.09/GB 110 | - API calls: $0.0004 per 1000 requests 111 | 112 | **Architecture:** 113 | 114 | ``` 115 | s3://bucket/path/object 116 | ├─ Bucket: Namespace (único globalmente) 117 | ├─ Path: Estructura tipo folder (no folders reales) 118 | └─ Object: Archivo real 119 | ``` 120 | 121 | **Optimización:** 122 | 123 | - Partitioning: `s3://bucket/year=2024/month=01/day=15/` 124 | → Permite el partition pruning. 125 | - Formato: Parquet > CSV/JSON 126 | → Parquet: columnar, comprimido, 10x más pequeño. 127 | - Lifecycle: Mover datos antiguos a Glacier (archive, 90% más barato). 128 | 129 | --- 130 | 131 | ### 2. EMR (Elastic MapReduce) 132 | 133 | **Qué:** Managed Hadoop/Spark cluster 134 | **Por qué:** No hay necesidad de gestionar la infraestructura, solo ejecutar Spark 135 | **Contras:** Más caro que raw EC2, overkill para jobs pequeños 136 | 137 | **Example:** 138 | 139 | ``` 140 | # Launch 10-node Spark cluster 141 | aws emr create-cluster \ 142 | --name "daily-etl" \ 143 | --release-label emr-6.9.0 \ 144 | --applications Name=Spark \ 145 | --instance-count 10 \ 146 | --instance-type m5.xlarge 147 | 148 | # Submit Spark job 149 | spark-submit s3://code/etl.py 150 | ``` 151 | 152 | El cluster auto-escalado y se apaga cuando termina. 153 | 154 | --- 155 | 156 | ### 3. Redshift (Data Warehouse) 157 | 158 | **Qué:** Base de datos MPP (Massively Parallel Processing), OLAP optimized 159 | **Por qué:** Consulta 100GB en segundos, barato para analytics 160 | **Contras:** Necesita schema design, no real-time 161 | 162 | **Ejemplo de arquitectura:** 163 | 164 | - 4 nodes × 2TB cada uno = 8TB de capacidad 165 | - Columnar storage → consultas rápidas 166 | - Ejecución distribuida 167 | 168 | **Pricing:** 169 | 170 | - `dc2.large`: $0.43/hour per node 171 | - 4 nodes × $0.43 = $1.72/hour 172 | - ≈ $1200/month (24/7) 173 | 174 | **Optimizaciones:** 175 | 176 | - Compression: zstd (90% más pequeño) 177 | - Sort keys: ordenar por columnas de filtro comunes 178 | - Dist keys: distribuir por join key 179 | - Vacuum: recuperar espacio de las eliminaciones 180 | 181 | --- 182 | 183 | ### 4. Lambda (Serverless Functions) 184 | 185 | **Qué:** Ejecutar código sin gestionar servers 186 | **Por qué:** Auto-scales, paga por ejecución 187 | **Contras:** Cold start (~1s), timeout máximo 15 min 188 | 189 | **Casos de uso:** 190 | 191 | - Trigger ETL en subida a S3 192 | - Procesar streaming data 193 | - API backends 194 | 195 | **Ejemplo:** Trigger Lambda en subida a S3 196 | 197 | ``` 198 | @lambda_handler 199 | def handler(event, context): 200 | bucket = event['Records']['s3']['bucket']['name'] 201 | key = event['Records']['s3']['object']['key'] 202 | 203 | df = pd.read_csv(f"s3://{bucket}/{key}") 204 | result = process(df) 205 | 206 | result.to_parquet(f"s3://output/{key}.parquet") 207 | return {'statusCode': 200} 208 | ``` 209 | 210 | **Pricing:** $0.20 per 1M invocations + compute time. 211 | 212 | --- 213 | 214 | ### 5. Glue (Managed ETL) 215 | 216 | **Qué:** ETL gestionado por AWS para serverless pipelines 217 | **Por qué:** Auto-scales, se integra con Redshift/Athena 218 | **Contras:** Control limitado vs pure Spark 219 | 220 | **Características:** 221 | 222 | - **Data Catalog:** registro de metadata 223 | - **Crawlers:** auto-detectar schema desde S3 224 | - **Jobs:** ejecutar PySpark/Scala 225 | - **Triggers:** basados en schedule o eventos 226 | 227 | **Example:** 228 | 229 | ``` 230 | import awsglue 231 | from awsglue.context import GlueContext 232 | from pyspark.context import SparkContext 233 | 234 | sc = SparkContext() 235 | glueContext = GlueContext(sc) 236 | 237 | # Read from S3 238 | dyf = glueContext.create_dynamic_frame.from_options( 239 | "s3", 240 | {"path": "s3://data-lake/raw/customers/"} 241 | ) 242 | 243 | # Transform 244 | transformed = dyf.map(lambda x: clean_data(x)) 245 | 246 | # Write to Redshift 247 | glueContext.write_dynamic_frame.from_jdbc_conf( 248 | frame=transformed, 249 | catalog_connection="redshift-connection", 250 | connection_options={"dbtable": "public.customers"}, 251 | transformation_ctx="to_redshift" 252 | ) 253 | ``` 254 | 255 | --- 256 | 257 | ### 6. Kinesis (Streaming) 258 | 259 | **Qué:** Alternativa managed a Kafka 260 | **Por qué:** Escala automáticamente, AWS-native 261 | **Contras:** Mayor costo, menos control 262 | 263 | **Casos de uso:** 264 | 265 | - Real-time ingestion 266 | - Event streaming 267 | - On-the-fly analytics 268 | 269 | **Arquitectura:** 270 | 271 | ``` 272 | Data Source → Kinesis Streams → Lambda/Spark → S3/Redshift 273 | ``` 274 | 275 | **Especificaciones:** 276 | 277 | - 1 shard = 1 MB/sec ingestion 278 | - 10 shards = 10 MB/sec 279 | - Auto-scaling disponible 280 | - Pricing: $0.035/shard-hour 281 | 282 | --- 283 | 284 | ## Mundo Real: End-to-End Pipeline 285 | 286 | **Escenario:** ETL diario de e-commerce 287 | 288 | **Pasos:** 289 | 290 | 1. Lambda triggered por subida a S3 291 | 2. EMR Spark cluster se inicia 292 | 3. Leer raw S3 → Transformar → Escribir processed S3 293 | 4. Redshift COPY desde S3 294 | 5. Analytics en Redshift 295 | 296 | **Architecture:** 297 | 298 | ``` 299 | AWS_ARCHITECTURE = { 300 | "ingestion": "S3 (raw data subida diariamente)", 301 | "processing": "EMR (Spark cluster, 5 nodes)", 302 | "storage": "S3 (processed Parquet)", 303 | "warehouse": "Redshift (4 nodes, 8TB)", 304 | "orchestration": "Lambda + EventBridge (cron triggers)", 305 | "monitoring": "CloudWatch (logs, metrics, alarms)", 306 | "cost": "$2000/month (S3 $100, EMR $500, Redshift $1200, Lambda $50, misc $150)" 307 | } 308 | ``` 309 | 310 | **Alternativas:** 311 | 312 | - AWS Glue only: más fácil pero menos control 313 | - Databricks on EC2: mayor control, mayor complejidad 314 | 315 | --- 316 | 317 | ## Trade-offs: AWS vs Alternativas 318 | 319 | | Criterios | AWS | GCP | Azure | 320 | | -------------------- | -------- | --------------- | ------------ | 321 | | Equivalente a S3 | S3 | GCS | Blob Storage | 322 | | Spark managed | EMR | Dataproc | HDInsight | 323 | | Warehouse | Redshift | BigQuery | Synapse | 324 | | Serverless functions | Lambda | Cloud Functions | Functions | 325 | | Streaming | Kinesis | Pub/Sub | Event Hubs | 326 | | Costo | Medio | Bajo (BigQuery) | Medio | 327 | | Facilidad | Medio | Alto (BigQuery) | Medio | 328 | | Control | Alto | Medio | Medio | 329 | 330 | --- 331 | 332 | ## Errores Comunes en Entrevista 333 | 334 | - **"EMR es siempre más barato que Glue"** → Depende: para pequeños jobs, Glue es más barato. 335 | - **"S3 es ilimitado"** → Sí, pero el costo/performance importan. Haz partition smart. 336 | - **Ignorar Data Transfer Out Cost** → Transferir fuera de AWS cuesta $0.09/GB. 337 | - **Usar Redshift para datos real-time** → Redshift es batch-oriented. Usa Kinesis o Lambda. 338 | 339 | --- 340 | 341 | ## Preguntas de Seguimiento 342 | 343 | 1. **¿Cuándo usas EMR vs Glue?** 344 | - EMR: Control, custom Spark, big clusters. 345 | - Glue: Managed, serverless, smaller jobs. 346 | 347 | 2. **¿Redshift vs Athena?** 348 | - Redshift: Persistent warehouse, datos preloaded, complex queries. 349 | - Athena: Query S3 directamente, barato y ad-hoc. 350 | 351 | 3. **¿Cómo optimizas S3 para costo?** 352 | - Partitioning (partition pruning). 353 | - Compression (Parquet). 354 | - Lifecycle (archivar data vieja). 355 | - Right format (Parquet > CSV). 356 | 357 | 4. **¿Cold start de EMR clusters?** 358 | - 5–10 minutos para launch. 359 | - Solución: mantener warm clusters (más caro, más rápido). 360 | 361 | --- 362 | 363 | ## Referencias 364 | 365 | - [AWS S3 - Official Docs](https://docs.aws.amazon.com/s3/) 366 | - [EMR - Spark on AWS](https://docs.aws.amazon.com/emr/) 367 | - [Redshift - Data Warehouse](https://docs.aws.amazon.com/redshift/) 368 | - [AWS Glue - ETL Service](https://docs.aws.amazon.com/glue/) 369 | - [Kinesis - Streaming](https://docs.aws.amazon.com/kinesis/) 370 | -------------------------------------------------------------------------------- /pyspark/04-data-types-schemas.md: -------------------------------------------------------------------------------- 1 | # Tipos de Datos y Schemas en PySpark 2 | 3 | **Tags**: #pyspark #schemas #data-types #structtype #real-interview 4 | 5 | --- 6 | 7 | ## TL;DR 8 | 9 | Schema define estructura de DataFrame (nombre columna, tipo, nullable). Define con `StructType + StructField` (recomendado) o inferencia automática (lento, unreliable). En producción, SIEMPRE schema explícito. Tipos comunes: StringType, IntegerType, DoubleType, TimestampType, ArrayType, MapType. 10 | 11 | --- 12 | 13 | ## Concepto Core 14 | 15 | - **Qué es**: Schema es "plano" de DataFrame. Define qué columnas existen, qué tipos, si pueden ser NULL 16 | - **Por qué importa**: Schema explícito = production-ready. Inferencia = lento y propenso a errores. Demuestra profesionalismo 17 | - **Principio clave**: Especifica schema siempre. La inferencia es para exploración, nunca para producción 18 | 19 | --- 20 | 21 | ## Memory Trick 22 | 23 | **"Blueprint de casa"** — Schema es el blueprint. Si construyes sin blueprint (inferencia), todo es caótico. Con blueprint, claro y eficiente. 24 | 25 | --- 26 | 27 | ## Cómo explicarlo en entrevista 28 | 29 | **Paso 1**: "Schema define estructura del DataFrame: columnas, tipos, nullability" 30 | 31 | **Paso 2**: "Hay 2 formas: inferencia (automática) vs explícita (StructType). Inferencia = lento (2 scans), explícita = rápido (1 scan)" 32 | 33 | **Paso 3**: "En producción, SIEMPRE explícita. Inferencia es solo para exploración rápida" 34 | 35 | **Paso 4**: "Define con StructType([StructField(...), ...]). Especifica tipo y nullable por cada columna" 36 | 37 | --- 38 | 39 | ## Código/Query ejemplo 40 | 41 | ### Datos: JSON sin estructura clara 42 | 43 | ```json 44 | {"name": "Alice", "age": 28, "salary": 50000.50, "hired_date": "2024-01-15", "tags": ["python", "spark"]} 45 | {"name": "Bob", "age": 35, "salary": 60000.00, "hired_date": "2024-02-20", "tags": ["java"]} 46 | {"name": "Charlie", "age": null, "salary": 55000.75, "hired_date": "2024-03-10", "tags": []} 47 | ``` 48 | 49 | --- 50 | 51 | ### ❌ Opción 1: Inferencia Automática (No recomendado) 52 | 53 | ```python 54 | from pyspark.sql import SparkSession 55 | 56 | spark = SparkSession.builder.appName("Schema Inference").getOrCreate() 57 | 58 | # Spark INFIERE el schema automáticamente 59 | df = spark.read.json("employees.json") 60 | 61 | # ¿Qué schema creó Spark? 62 | df.printSchema() 63 | ``` 64 | 65 | **Problema:** 66 | 67 | - Spark lee TODO el archivo 2 veces (ineficiente) 68 | - Si age es NULL en primeras filas, asume StringType (incorrecto) 69 | - StringType en age = problemas después (conversiones) 70 | 71 | **Output:** 72 | 73 | ``` 74 | root 75 | |-- name: string (nullable = true) 76 | |-- age: long (nullable = true) 77 | |-- salary: double (nullable = true) 78 | |-- hired_date: string (nullable = true) 79 | |-- tags: array (nullable = true) 80 | | |-- element: string (containsNull = true) 81 | ``` 82 | 83 | ⚠️ **Problemas**: 84 | 85 | - hired_date es STRING (debería ser DATE) 86 | - tags es ARRAY (OK, pero Spark tuvo que inferir) 87 | - 2 scans del archivo (lento) 88 | 89 | --- 90 | 91 | ### ✅ Opción 2: Schema Explícito (Recomendado) 92 | 93 | ```python 94 | from pyspark.sql.types import ( 95 | StructType, StructField, 96 | StringType, IntegerType, DoubleType, 97 | DateType, ArrayType 98 | ) 99 | 100 | # Define schema explícitamente 101 | schema = StructType([ 102 | StructField("name", StringType(), nullable=False), # No puede ser NULL 103 | StructField("age", IntegerType(), nullable=True), # Puede ser NULL 104 | StructField("salary", DoubleType(), nullable=False), # No puede ser NULL 105 | StructField("hired_date", DateType(), nullable=False), # Tipo correcto: DATE 106 | StructField("tags", ArrayType(StringType()), nullable=True) # Array de strings 107 | ]) 108 | 109 | # Lee con schema explícito 110 | df = spark.read.schema(schema).json("employees.json") 111 | 112 | df.printSchema() 113 | ``` 114 | 115 | **Ventajas:** 116 | 117 | - Solo 1 scan del archivo (rápido) 118 | - Tipos correctos (DateType en hired_date) 119 | - Nullable especificado (contrato claro) 120 | 121 | **Output:** 122 | 123 | ``` 124 | root 125 | |-- name: string (nullable = false) 126 | |-- age: integer (nullable = true) 127 | |-- salary: double (nullable = false) 128 | |-- hired_date: date (nullable = false) 129 | |-- tags: array (nullable = true) 130 | | |-- element: string (containsNull = true) 131 | ``` 132 | 133 | --- 134 | 135 | ## Data Types Comunes 136 | 137 | | Tipo | Python | Rango | Ejemplo | 138 | | ------------- | ----------------- | ---------------- | ---------------------------- | 139 | | StringType | str | Any length | "Alice" | 140 | | IntegerType | int | -2^31 to 2^31-1 | 28 | 141 | | LongType | int | -2^63 to 2^63-1 | 123456789 | 142 | | DoubleType | float | IEEE 754 | 50000.50 | 143 | | FloatType | float | IEEE 754 32-bit | 50000.5 | 144 | | BooleanType | bool | true/false | True | 145 | | DateType | datetime.date | YYYY-MM-DD | 2024-01-15 | 146 | | TimestampType | datetime.datetime | with timezone | 2024-01-15 10:30:00 | 147 | | DecimalType | Decimal | Precision/Scale | 99999.99 | 148 | | BinaryType | bytes | Binary data | b"abc" | 149 | | ArrayType | list | Variable length | ["a", "b"] | 150 | | MapType | dict | Key-value | {"key": "value"} | 151 | | StructType | dict | Nested structure | {"name": "Alice", "age": 28} | 152 | 153 | --- 154 | 155 | ## Esquemas Complejos 156 | 157 | ### Array de Structs 158 | 159 | ```python 160 | schema = StructType([ 161 | StructField("employee_id", IntegerType(), False), 162 | StructField("name", StringType(), False), 163 | StructField("orders", ArrayType( 164 | StructType([ 165 | StructField("order_id", IntegerType(), False), 166 | StructField("amount", DoubleType(), False), 167 | StructField("date", DateType(), False) 168 | ]) 169 | ), True) 170 | ]) 171 | ``` 172 | 173 | Datos: 174 | 175 | ```json 176 | { 177 | "employee_id": 1, 178 | "name": "Alice", 179 | "orders": [ 180 | { "order_id": 101, "amount": 500.0, "date": "2024-01-10" }, 181 | { "order_id": 102, "amount": 750.0, "date": "2024-01-15" } 182 | ] 183 | } 184 | ``` 185 | 186 | --- 187 | 188 | ### Nested Structs 189 | 190 | ```python 191 | schema = StructType([ 192 | StructField("id", IntegerType(), False), 193 | StructField("person", StructType([ 194 | StructField("name", StringType(), False), 195 | StructField("email", StringType(), False), 196 | StructField("address", StructType([ 197 | StructField("street", StringType(), True), 198 | StructField("city", StringType(), False), 199 | StructField("zip", StringType(), True) 200 | ]), True) 201 | ]), False) 202 | ]) 203 | ``` 204 | 205 | Datos: 206 | 207 | ```json 208 | { 209 | "id": 1, 210 | "person": { 211 | "name": "Alice", 212 | "email": "alice@example.com", 213 | "address": { 214 | "street": "123 Main St", 215 | "city": "NYC", 216 | "zip": "10001" 217 | } 218 | } 219 | } 220 | ``` 221 | 222 | --- 223 | 224 | ## Inferencia desde String (Hack útil) 225 | 226 | Si tienes schema como string (de API, config, etc.) 227 | 228 | ```python 229 | schema_string = "name STRING, age INT, salary DOUBLE, hired_date DATE" 230 | 231 | df = spark.read.schema(schema_string).json("employees.json") 232 | ``` 233 | 234 | Equivalente a StructType explícito pero más legible 235 | 236 | --- 237 | 238 | ## Nullable: Qué Significa 239 | 240 | ```python 241 | nullable=False: Valor NUNCA puede ser NULL (constraint de integridad) 242 | StructField("id", IntegerType(), nullable=False) 243 | 244 | nullable=True: Valor PUEDE ser NULL (permite faltantes) 245 | StructField("middle_name", StringType(), nullable=True) 246 | ``` 247 | 248 | En SQL: 249 | nullable=False → NOT NULL constraint 250 | nullable=True → permite NULL 251 | 252 | --- 253 | 254 | ## Validación de Schema 255 | 256 | ```python 257 | # Verificar qué schema Spark inferió (para debugging) 258 | df.printSchema() 259 | 260 | # Obtener schema como JSON (útil para guardar/documentar) 261 | schema_json = df.schema.json() 262 | print(schema_json) 263 | 264 | # Comparar schemas 265 | schema1 = StructType([StructField("id", IntegerType())]) 266 | schema2 = StructType([StructField("id", LongType())]) 267 | print(schema1 == schema2) # False (IntegerType ≠ LongType) 268 | ``` 269 | 270 | --- 271 | 272 | ## Performance: Schema Explícito vs Inferencia 273 | 274 | ```python 275 | # Inferencia (2 scans) 276 | df_inferred = spark.read.json("big_file.json") # Scan 1: Infer schema, Scan 2: Read data 277 | # Tiempo: ~20 segundos para 1 GB 278 | 279 | # Explícito (1 scan) 280 | df_explicit = spark.read.schema(schema).json("big_file.json") # Solo 1 scan 281 | # Tiempo: ~5 segundos para 1 GB (4x más rápido) 282 | ``` 283 | 284 | --- 285 | 286 | ## Errores comunes en entrevista 287 | 288 | - **Error**: Usar inferencia en producción → **Solución**: Siempre schema explícito. Si no conoces schema, descúbrelo primero 289 | 290 | - **Error**: Tipo incorrecto (edad como STRING en lugar de INT) → **Solución**: Datos tendrán problemas después. Define tipos correctos upfront 291 | 292 | - **Error**: nullable=False cuando datos tienen NULLs → **Solución**: Causará error al read. Usa nullable=True o limpia data primero 293 | 294 | - **Error**: No documentar schema → **Solución**: Guarda schema como JSON en repo (documentación) 295 | 296 | --- 297 | 298 | ## Preguntas de seguimiento típicas 299 | 300 | 1. **"¿Diferencia entre DateType y TimestampType?"** 301 | - DateType: Solo fecha (YYYY-MM-DD) 302 | - TimestampType: Fecha + hora + timezone 303 | 304 | 2. **"¿Cuándo usas MapType o ArrayType?"** 305 | - ArrayType: Listas variable-length (tags: ["python", "spark"]) 306 | - MapType: Key-value pairs (config: {"env": "prod", "region": "us-west"}) 307 | 308 | 3. **"¿Cómo cambias tipo de columna después de leer?"** 309 | - `df.withColumn("age", col("age").cast(IntegerType()))` 310 | - Pero mejor evitar si defines schema correcto upfront 311 | 312 | 4. **"¿Cómo manejas schema mismatch si datos son inconsistentes?"** 313 | - Validación pre-read 314 | - O: `mode="permissive"` (default, NULLs para mismatch) vs `"failfast"` (error) 315 | 316 | --- 317 | 318 | ## Real-World: Production Schema Storage 319 | 320 | ```python 321 | # Guarda schema en repo para documentación 322 | import json 323 | 324 | schema_dict = { 325 | "fields": [ 326 | {"name": "id", "type": "integer", "nullable": False}, 327 | {"name": "name", "type": "string", "nullable": False}, 328 | {"name": "salary", "type": "double", "nullable": False} 329 | ] 330 | } 331 | 332 | # En archivo: schemas/employees.json 333 | with open("schemas/employees.json", "w") as f: 334 | json.dump(schema_dict, f, indent=2) 335 | 336 | # En código: carga schema 337 | with open("schemas/employees.json") as f: 338 | schema_dict = json.load(f) 339 | 340 | # Convierte a StructType 341 | def dict_to_struct(schema_dict): 342 | fields = [] 343 | for field in schema_dict["fields"]: 344 | fields.append(StructField( 345 | field["name"], 346 | get_type(field["type"]), 347 | field["nullable"] 348 | )) 349 | return StructType(fields) 350 | 351 | schema = dict_to_struct(schema_dict) 352 | df = spark.read.schema(schema).json("data.json") 353 | ``` 354 | 355 | --- 356 | 357 | ## References 358 | 359 | - https://spark.apache.org/docs/latest/building-spark.html 360 | -------------------------------------------------------------------------------- /system-design/04-data-quality-monitoring.md: -------------------------------------------------------------------------------- 1 | # Monitoreo de Calidad de Datos a Escala 2 | 3 | **Tags**: #data-quality #monitoring #alerts #production #real-interview 4 | 5 | --- 6 | 7 | ## TL;DR 8 | 9 | Monitoreo de calidad de datos = detectar automáticamente problemas (NULLs, duplicados, cambios de esquema, datos tardíos). Métricas: null_percentage, duplicate_count, row_count_delta, schema_validation, freshness. Alertas si las métricas exceden umbrales predefinidos. Herramientas: Great Expectations (automatiza tests), DataDog/Prometheus + Grafana, Slack/Email. Trade-offs: Strict (muchas alertas) vs Loose (menos alertas). Para producción: Balance entre detección temprana vs fatiga de alertas. 10 | 11 | --- 12 | 13 | ## Concepto 14 | 15 | - **Qué es**: Monitoreo de calidad de datos = verificar que los datos cumplen con estándares de calidad 16 | - **Por qué importa**: Los datos malos conducen a análisis incorrectos y decisiones empresariales malas. En producción, el 80% del tiempo se gasta en calidad de datos 17 | - **Principio clave**: Calidad de datos = "Basura dentro, basura fuera" (GIGO). El monitoreo automático es esencial 18 | 19 | --- 20 | 21 | ## Framework de Monitoreo de Calidad de Datos 22 | 23 | ### Great Expectations (Automatización de Validaciones) 24 | 25 | ```python 26 | from great_expectations import GreatExpectations 27 | from pyspark.sql import SparkSession 28 | 29 | class DataQualityChecks(GreatExpectations): 30 | def __init__(self, spark): 31 | self.spark = spark 32 | 33 | def validate_customer_schema(self, df): 34 | """Valida esquema de clientes""" 35 | return self.expect_dataframe_to_have_columns(df, ['customer_id', 'name', 'email']) 36 | return self.expect_column_values_to_be_unique('customer_id') 37 | return self.expect_column_values_to_not_be_null('email') 38 | 39 | def validate_orders_schema(self, df): 40 | """Valida esquema de pedidos""" 41 | return self.expect_column_values_to_be_positive('amount') 42 | return self.expect_column_values_to_be_between('quantity', 1, 1000) 43 | 44 | def validate_row_count(self, table, expected_range): 45 | """Valida que el conteo de filas esté en rango esperado""" 46 | actual_count = self.spark.sql(f"SELECT COUNT(*) FROM {table}") 47 | min_val, max_val = expected_range 48 | actual_count = actual_count.collect()[0][0] 49 | 50 | if not (min_val <= actual_count <= max_val): 51 | raise Exception(f"Row count fuera de rango: {actual_count} (esperado: {min_val}-{max_val}") 52 | 53 | def validate_freshness(self, table, max_hours): 54 | """Valida frescura de datos""" 55 | max_timestamp = self.spark.sql(f"SELECT MAX(updated_at) FROM {table}") 56 | hours_old = (datetime.now() - max_timestamp).total_seconds() / 3600 57 | 58 | if hours_old > max_hours: 59 | raise Exception(f"Datos demasiado viejos: {hours_old} horas") 60 | 61 | def validate_duplicates(self, table, key_cols): 62 | """Detecta duplicados por clave""" 63 | dups = self.spark.sql(f""" 64 | SELECT {', '.join(key_cols), COUNT(*) as count 65 | FROM {table} 66 | GROUP BY {', '.join(key_cols) 67 | HAVING COUNT(*) > 1 68 | """) 69 | 70 | if dups.count() > 0: 71 | raise Exception(f"Se detectaron {dups.count()} duplicados en {table}") 72 | 73 | def validate_null_percentage(self, table, column, threshold=5): 74 | """Verifica porcentaje de nulos""" 75 | null_pct = self.spark.sql(f""" 76 | SELECT (COUNT(*) - COUNT({column}) * 100 / COUNT(*)) 77 | FROM {table} 78 | """).collect()[0][0] 79 | 80 | if null_pct > threshold: 81 | raise Exception(f"Porcentaje de nulos en {table}.{column}: {null_pct}%") 82 | 83 | def run_all_checks(self, tables_info): 84 | """Ejecuta todas las validaciones""" 85 | results = {} 86 | 87 | for table_name, config in tables_info.items(): 88 | df = self.spark.table(table_name) 89 | 90 | # Validación de esquema 91 | self.validate_schema(table_name, df) 92 | 93 | # Validación de datos 94 | self.validate_row_count(table_name, config["expected_range"]) 95 | 96 | # Validación de nulos 97 | self.validate_null_percentage(table_name, config["null_thresholds"]) 98 | 99 | # Validación de duplicados 100 | for key_cols in config["key_columns"]: 101 | self.validate_duplicates(table_name, key_cols) 102 | 103 | results[table_name] = { 104 | "row_count": df.count(), 105 | "schema_validation": "PASS", 106 | "null_percentage": null_pct, 107 | "duplicate_count": dups.count() 108 | } 109 | 110 | return results 111 | ``` 112 | 113 | ### Sistema de Alertas 114 | 115 | ```python 116 | class AlertManager: 117 | def __init__(self, alert_webhook, email_smtp): 118 | self.alert_webhook = alert_webhook 119 | self.email_smtp = email_smtp 120 | 121 | def send_alert(self, alert_type, message, details): 122 | """Envía alerta según el tipo""" 123 | alert_data = { 124 | "type": alert_type, 125 | "message": message, 126 | "details": details, 127 | "timestamp": datetime.now() 128 | } 129 | 130 | self.alert_webhook(alert_data) 131 | 132 | def send_data_quality_alert(self, table, metric, value, threshold): 133 | """Envía alerta si la métrica excede el umbral""" 134 | if value > threshold: 135 | self.send_alert("DATA_QUALITY", f"{table}.{metric}: {value} (threshold: {threshold})") 136 | 137 | def send_fraud_alert(self, table, fraud_count): 138 | """Alerta si hay demasiados fraudes""" 139 | if fraud_count > threshold: 140 | self.send_alert("FRAUD", f"{table}: {fraud_count} fraudes detectados") 141 | 142 | def send_latency_alert(self, component, latency_ms, threshold_ms): 143 | """Alerta si la latencia excede el umbral""" 144 | if latency_ms > threshold_ms: 145 | self.send_alert("LATENCY", f"{component}: {latency_ms}ms (threshold: {threshold_ms}ms") 146 | ``` 147 | 148 | --- 149 | 150 | ## Métricas Clave 151 | 152 | ### Métricas de Calidad de Datos 153 | 154 | ```python 155 | class DataQualityMetrics: 156 | def calculate_null_percentage(self, table, column): 157 | null_count = self.spark.sql(f""" 158 | SELECT (COUNT(*) - COUNT({column}) * 100 / COUNT(*)) 159 | FROM {table} 160 | """).collect()[0][0] 161 | 162 | return null_count / self.spark.sql(f"SELECT COUNT(*) FROM {table}").collect()[0][0] * 100 163 | 164 | def calculate_duplicate_count(self, table, key_cols): 165 | dups = self.spark.sql(f""" 166 | SELECT {', '.join(key_cols), COUNT(*) as count 167 | FROM {table} 168 | GROUP BY {', '.join(key_cols) 169 | HAVING COUNT(*) > 1 170 | """).collect() 171 | 172 | return dups 173 | 174 | def calculate_row_count_delta(self, table, hours=24): 175 | today_count = self.spark.sql(f""" 176 | SELECT COUNT(*) FROM {table} 177 | WHERE DATE(updated_at) >= DATE_SUB(CURRENT_DATE, INTERVAL '{hours} HOUR) 178 | """).collect()[0][0] 179 | 180 | yesterday_count = self.spark.sql(f""" 181 | SELECT COUNT(*) FROM {table} 182 | WHERE DATE(updated_at) BETWEEN DATE_SUB(CURRENT_DATE, INTERVAL '{hours+24} HOUR, DATE_SUB(CURRENT_DATE, INTERVAL '{hours} HOUR) 183 | """).collect()[0][0] 184 | 185 | delta = today_count - yesterday_count 186 | 187 | return { 188 | "table": table, 189 | "today_count": today_count, 190 | "yesterday_count": yesterday_count, 191 | "row_count_delta": delta, 192 | "pct_change": delta / yesterday_count * 100 193 | } 194 | ``` 195 | 196 | ### Métricas de Rendimiento 197 | 198 | ```python 199 | class PerformanceMetrics: 200 | def calculate_processing_time(self, table): 201 | """Calcula tiempo de procesamiento""" 202 | start_time = time.time() 203 | 204 | self.spark.sql(f"SELECT COUNT(*) FROM {table}").collect()[0][0] 205 | 206 | end_time = time.time() 207 | processing_time = end_time - start_time 208 | 209 | return processing_time 210 | 211 | def calculate_storage_size(self, table): 212 | """Calcula tamaño de almacenamiento""" 213 | table_size_mb = self.spark.sql(f""" 214 | SELECT SUM(size_bytes) / (1024 * 1024) AS size_mb 215 | FROM information_schema.tables 216 | WHERE table = '{table}' 217 | """).collect()[0][0] 218 | 219 | return table_size_mb 220 | 221 | def calculate_query_performance(self, query): 222 | """Calcula tiempo de consulta""" 223 | start_time = time.time() 224 | 225 | result = self.spark.sql(query) 226 | end_time = time.time() 227 | 228 | return end_time - start_time 229 | ``` 230 | 231 | --- 232 | 233 | ## Implementación de Alertas 234 | 235 | ### Sistema de Alertas 236 | 237 | ```python 238 | class AlertSystem: 239 | def __init__(self): 240 | self.alert_rules = { 241 | "row_count_delta": {"threshold": 10, "severity": "HIGH"}, 242 | "null_percentage": {"threshold": 5, "severity": "MEDIUM"}, 243 | "duplicate_count": {"threshold": 1, "severity": "HIGH"}, 244 | "freshness": {"threshold": 2, "severity": "MEDIUM"}, 245 | "processing_time": {"threshold": 500, "severity": "MEDIUM"}, 246 | "query_performance": {"threshold": 1000, "severity": "HIGH"} 247 | } 248 | } 249 | 250 | def evaluate_metric(self, metric_name, table, value): 251 | rule = self.alert_rules.get(metric_name) 252 | 253 | if value > rule["threshold"]: 254 | severity = rule["severity"] 255 | message = f"{metric_name}: {value} (threshold: {rule['threshold']})" 256 | 257 | self.send_alert(severity, message, { 258 | "table": table, 259 | "metric": metric_name, 260 | "value": value 261 | }) 262 | 263 | # Uso 264 | alert_system = AlertSystem() 265 | alert_system.evaluate_metric("row_count_delta", "customers", 15) # Excede umbral de 10% 266 | # Alerta: "HIGH: row_count_delta: 15% (threshold: 10%) 267 | ``` 268 | 269 | --- 270 | 271 | ## Errores Comunes en Entrevista 272 | 273 | - **Error**: "Monitorear TODO" → **Solución**: Priorizar métricas críticas y establecer alertas automáticas 274 | 275 | - **Error**: "Alertar demasiado" → **Solución**: Implementar umbrales de alerta para evitar fatiga de alertas 276 | 277 | - **Error**: "No monitorear el rendimiento" → **Solución**: Incluir métricas de rendimiento y latencia 278 | 279 | - **Error**: "No alertar cambios en el esquema" → **Solución**: Detectar cambios en el esquema y alertar 280 | 281 | --- 282 | 283 | ## Preguntas de Seguimiento Típicas 284 | 285 | 1. **"¿Cómo manejas el ruido en las métricas?"** 286 | - Suavizado de métricas 287 | - Filtros de ruido 288 | - Umbral adaptativo según la hora del día 289 | 290 | 2. **"¿Cómo manejas alertas falsos positivos?"** 291 | - Confirmación con datos históricos 292 | - Validación cruzada 293 | - Alertas en cascada con diferentes niveles de severidad 294 | 295 | 3. **"¿Cómo escalas a 1000+ pipelines?"** 296 | - Plataforma centralizada con estándares predefinidos 297 | - Automatización de alertas 298 | - Jerarquía de alertas 299 | 300 | 4. **"¿Cómo manejas cambios en el esquema?"** 301 | - Versionado de esquemas 302 | - Validación de compatibilidad hacia atrás 303 | - Alertas sobre cambios de esquema 304 | 305 | --- 306 | 307 | ## Referencias 308 | 309 | - https://learn.microsoft.com/en-us/azure/databricks/data-quality-monitoring/ 310 | - https://www.ibm.com/think/topics/data-quality-monitoring-techniques 311 | --------------------------------------------------------------------------------