Saltar al contenido principal
DB-601 • 36 Horas • Nivel Avanzado / Principal Lakehouse & OLAP Architect

Arquitectura Lakehouse Moderna: Apache Iceberg, Delta Lake, ClickHouse Columnar, PySpark y Polars

Formatos de tabla transaccionales sobre Object Storage (Iceberg Metadata Tree, Copy-on-Write vs Merge-on-Read), particionamiento oculto, curvas de llenado de espacio Z-Order / Hilbert, motores vectorizados (Polars/Arrow) y ClickHouse MergeTree.

Módulo 1: Módulo 1: Internals de Apache Iceberg y Delta Lake: Árbol de Metadatos, Snapshots ACID y MoR vs CoW

Anatomía Interna de Apache Iceberg: Catalog Pointer, Metadata JSON, Manifest Lists y Concurrencia Optimista (55 min)

Anatomía Interna de Apache Iceberg: Catalog Pointer, Metadata JSON, Manifest Files y Concurrencia Optimista

1. Por qué Apache Hive Table Format Fracasó en Object Storage (S3 / GCS)

En tablas Hive clásicas, la fuente de verdad de qué archivos pertenecen a una partición es listar los directorios físicos en S3 (LISTObjectsV2): - Es una operación costosa y lenta ($\mathcal{O}(N_{\text{directorios}})$ llamadas HTTP). - Carece de atomicidad: si un job de Spark falla a mitad de escribir $200$ archivos Parquet, quedan archivos huérfanos visibles para los lectores concurrentes.

Apache Iceberg traslada el seguimiento al nivel de archivo individual mediante un árbol inmutable de tres capas gobernado por un puntero atómico Compare-and-Swap (CAS) en el Catálogo (REST Catalog / AWS Glue / Nessie / Polaris):

flowchart TD
    CAT["Iceberg Catalog (Atomic CAS Pointer)"] --> META["Table Metadata File (v42.metadata.json)"]
    META --> S1["Snapshot s_41 (Time Travel Histórico)"]
    META --> S2["Snapshot Actual s_42 (Manifest List Avro)"]
    S2 --> MF1["Manifest File 1 (Estadísticas Min/Max por Archivo)"]
    S2 --> MF2["Manifest File 2 (Pruning de Particiones)"]
    MF1 --> P1["Data File 001.parquet (Columnar)"]
    MF1 --> P2["Position Delete File del_01.parquet"]

2. Poda de Archivos en Dos Fases (Two-Level Metadata Pruning) Sin Tocar Datos

1. Poda en Manifest List: Cada entrada en el Manifest List almacena el rango $[\min, \max]$ de las columnas de partición para un Manifest File entero, descartando miles de archivos en milisegundos. 2. Poda en Manifest File: Cada Manifest File (formato Avro) contiene los límites exactos lower_bounds y upper_bounds, conteo de nulos y conteo de filas por cada columna de cada archivo Parquet individual.

Estrategias de Mutación en Lakehouse: Copy-on-Write (CoW) vs Merge-on-Read (MoR) con Position y Equality Deletes (55 min)

Estrategias de Mutación en Lakehouse: Copy-on-Write (CoW) vs Merge-on-Read (MoR) con Position y Equality Deletes

1. El Dilema de Borrar 1 Fila dentro de un Archivo Parquet Inmutable de 512 MB

Dado que los archivos Parquet en S3 son inmutables, una sentencia DELETE o MERGE INTO (por ejemplo, cumplimiento GDPR o CDC de alta frecuencia) puede resolverse bajo dos paradigmas con costos opuestos de amplificación de escritura (Write Amplification) y lectura (Read Amplification):

| Característica Arquitectónica | Copy-on-Write (CoW) | Merge-on-Read (MoR — Iceberg v2) | | :--- | :--- | :--- | | Mecánica de UPDATE / DELETE | Reescribe íntegramente el archivo Parquet de $512\text{ MB}$ excluyendo la fila borrada | Escribe únicamente un pequeño Delete File ($ ext{KB}$) apuntando a $( ext{file_path}, ext{row_pos})$ | | Amplificación de Escritura | Muy Alta ($W_{\text{amp}} \gg 100\times$ en actualizaciones dispersas) | Mínima ($W_{\text{amp}} \approx 1\times$, ideal para streaming CDC) | | Penalización en Lectura OLAP | Cero (lectura secuencial pura de archivos Parquet limpios) | Moderada (el lector debe aplicar un anti-join o bitmap mask contra los Delete Files hasta la siguiente compactación) |

2. Position Deletes vs Equality Deletes

1. Position Delete File: Almacena tuplas exactas $(\text{file\_path}, \; \text{pos} \in \mathbb{N})$. En tiempo de lectura, el motor construye un Roaring Bitmap en memoria y salta la posición física de fila sin comparar valores. 2. Equality Delete File: Almacena predicados por valor (ej. user_id = 'U-991'). Permite que Flink emita borrados CDC a velocidad de línea sin siquiera buscar en qué archivo Parquet residía la fila antigua, delegando la reconciliación al trabajo asíncrono de compactación (rewrite_data_files).'

Particionamiento Oculto (Hidden Partitioning) y Ordenamiento Multidimensional con Curvas Z-Order y Hilbert (50 min)

Particionamiento Oculto (Hidden Partitioning) y Ordenamiento Multidimensional con Curvas Z-Order y Hilbert

1. Evolución de Particiones Sin Reescribir Petabytes (Partition Evolution)

En Hive/Spark legacy, particionar por fecha exigía crear una columna física redundante event_date_str = '2026-10-21'; si un analista filtraba WHERE event_ts >= '2026-10-21 00:00:00' olvidando incluir AND event_date_str = '2026-10-21', el motor ejecutaba un Full Table Scan catastrófico. Hidden Partitioning en Iceberg almacena la transformación (day(event_ts), bucket(64, tenant_id), truncate(8, sku)) en los metadatos y la evalúa automáticamente sobre el predicado original del usuario.

2. Localidad Multidimensional mediante Intercalado de Bits (Morton Z-Order Curve)

Ordenar un archivo Parquet de forma jerárquica ORDER BY (col_A, col_B) favorece exclusivamente los filtros sobre col_A, dejando los valores de col_B dispersos en todos los Row Groups. La Curva Z-Order (Código de Morton) intercala los bits binarios de las coordenadas $(x, y)$ para preservar localidad espacial en ambas dimensiones simultáneamente:

$Z(x, y) = \sum_{k=0}^{B-1} \left( x_k \cdot 2^{2k} + y_k \cdot 2^{2k+1} \right)$

/
 * Calcula el índice Z-Order (Morton Code de 32 bits) intercalando los bits
 * de dos coordenadas enteras de 16 bits (x, y) para clustering multidimensional.
 */
export function computeMortonZOrder2D(x: number, y: number): number {
  const spreadBits16 = (v: number): number => {
    let n = v & 0xffff;
    n = (n | (n << 8)) & 0x00ff00ff;
    n = (n | (n << 4)) & 0x0f0f0f0f;
    n = (n | (n << 2)) & 0x33333333;
    n = (n | (n << 1)) & 0x55555555;
    return n;
  };
  return spreadBits16(x) | (spreadBits16(y) << 1);
}

Módulo 2: Módulo 2: Motores Analíticos Columnas en Tiempo Real: ClickHouse MergeTree y Parquet Internals

Anatomía Binaria de Apache Parquet: Row Groups, Column Chunks, RLE/Dictionary Encoding y Split Block Bloom Filters (55 min)

Anatomía Binaria de Apache Parquet: Row Groups, Column Chunks, RLE/Dictionary Encoding y Split Block Bloom Filters

1. Modelo Híbrido PAX (Partition Attributes Across)

Apache Parquet organiza los datos horizontalmente en Row Groups (típicamente $128\text{ MB}$ a $512\text{ MB}$ o $10^6$ filas) y verticalmente dentro de cada Row Group en Column Chunks divididos en Data Pages ($1\text{ MB}$). El Footer al final del archivo permite que un motor lector descargue primero los últimos kilobytes mediante una petición HTTP Range: bytes=-65536, lea los índices de página (OffsetIndex y ColumnIndex), y descargue por red únicamente los rangos de bytes exactos de las columnas y páginas que superan el predicado WHERE.

2. Codificación Híbrida RLE + Bit-Packing Dictionary y Niveles Dremel

Para representar estructuras JSON/Protobuf arbitrariamente anidadas y repetidas sin almacenar punteros costosos, Parquet emplea el algoritmo de Google Dremel basado en dos enteros pequeños por valor: - Definition Level ($d$): Cuántos campos opcionales ancestros en la ruta del esquema están definidos (distingue entre una lista vacía y un campo padre NULL). - Repetition Level ($r$): En qué nivel de lista repetida se inicia una nueva entrada.'

Internals de ClickHouse: Índice Primario Disperso (Granules de 8192 Filas), ReplacingMergeTree y Agregación SIMD (55 min)

Internals de ClickHouse: Índice Primario Disperso (Granules de 8192 Filas), ReplacingMergeTree y Agregación SIMD

1. Por qué el Índice Primario de ClickHouse Cabe en Memoria RAM para 100 Billones de Filas

A diferencia de un árbol B+ de PostgreSQL que indexa cada fila individual ($1$ entrada por fila), ClickHouse MergeTree ordena físicamente los datos en disco por la tupla ORDER BY y construye un Índice Primario Disperso (Sparse Index) que almacena únicamente una marca (Mark) cada 1 Gránulo = $8,192$ filas:

$N_{\text{marcas\_en\_RAM}} = \left\lceil \frac{N_{\text{filas\_totales}}}{8192} \right\rceil$

Para una tabla colosal de $8.192 \times 10^{10}$ filas ($81.9$ mil millones de eventos), el índice primario completo tiene solo $10^7$ entradas, consumiendo menos de $160\text{ MB}$ de RAM.

2. Semántica Asíncrona de Deduplicación y Pre-Agregación en la Familia MergeTree

| Motor ClickHouse | Comportamiento Durante la Fusión de Partes en Segundo Plano | Patrón de Consulta Correcto Antes de la Fusión | | :--- | :--- | :--- | | MergeTree | Concatena y ordena partes sin eliminar duplicados | SELECT ... FROM tbl | | ReplacingMergeTree(ver) | Conserva solo la fila con mayor versión ver para la misma clave ORDER BY | Usar argMax(col, ver) GROUP BY key (preferido sobre FINAL) | | AggregatingMergeTree | Combina estados parciales (AggregateFunction(uniq, UUID), quantilesState) | SELECT uniqMerge(u_state) ... GROUP BY ... |'

Formato en Memoria Apache Arrow: Vectores de Validez Bitmap, Zero-Copy IPC y Arrow Flight gRPC (50 min)

Formato en Memoria Apache Arrow: Vectores de Validez Bitmap, Zero-Copy IPC y Arrow Flight gRPC

1. Eliminación del Impuesto de Serialización/Deserialización (SerDe Tax)

En conectores de bases de datos tradicionales (JDBC / ODBC), hasta el $75\%$ del tiempo de CPU se desperdicia convirtiendo buffers columnares en objetos fila individuales en memoria. Apache Arrow define un layout de memoria columnar alineado a $64\text{ bytes}$ (línea de caché AVX-512) idéntico en C++, Rust, Python (Polars) y Java, permitiendo compartir gigabytes entre procesos mediante memoria compartida (mmap) con Cero Copias y transferir flujos paralelos por red vía Arrow Flight.

Módulo 3: Módulo 3: Procesamiento Distribuido y Vectorizado: PySpark AQE vs Polars Lazy Execution

Optimización Avanzada en Apache Spark 3.5+: Adaptive Query Execution (AQE), Coalescing y Mitigación de Data Skew (55 min)

Optimización Avanzada en Apache Spark 3.5+: Adaptive Query Execution (AQE), Coalescing y Mitigación de Data Skew

1. Anatomía del Problema de las Tareas Rezagadas (Straggler Tasks) por Sesgo de Clave (Data Skew)

En un SortMergeJoin distribuido sobre $200$ particiones de Shuffle, los registros se enrutan por $\text{hash}(\text{key}) \bmod 200$. Si un único cliente mayorista (customer_id = 'GUEST_CHECKOUT' o 'TENANT_MEGA') concentra el $45\%$ de los $1,000$ millones de pedidos, 199 ejecutores terminan en 20 segundos y 1 único ejecutor sufre Spill a disco y OOM durante 40 minutos.

2. Las Tres Optimizaciones Dinámicas en Tiempo de Ejecución de Spark AQE

1. Dynamically Coalescing Shuffle Partitions: Une particiones post-shuffle pequeñas adyacentes para evitar abrir miles de archivos diminutos. 2. Dynamically Switching Join Strategies: Si tras aplicar un filtro la tabla derecha cae por debajo de spark.sql.autoBroadcastJoinThreshold ($10\text{ MB}$–$64\text{ MB}$), convierte en caliente un SortMergeJoin costoso en un BroadcastHashJoin sin Shuffle. 3. Dynamically Optimizing Skew Joins: Detecta particiones cuyo tamaño supera $\max(\text{mediana} \times 5, \; 256\text{ MB})$ y las subdivide automáticamente en sub-particiones paralelas replicando el bloque correspondiente del lado opuesto del join.'

Ingeniería de Alto Rendimiento en un Solo Nodo con Polars (Rust): Lazy Logical Plan y Out-of-Core Streaming (55 min)

Ingeniería de Alto Rendimiento en un Solo Nodo con Polars (Rust): Lazy Logical Plan y Out-of-Core Streaming

1. Cuándo NO Necesitas un Clúster de Spark: La Regla de los 100 GB en NVMe Moderno

Levantar un clúster distribuido de PySpark para procesar datasets de $5\text{ GB}$ a $150\text{ GB}$ introduce minutos de sobrecarga de JVM, serialización Python-JVM (Py4J) y Shuffle por red. Polars (escrito en Rust sobre Apache Arrow con paralelismo Work-Stealing de Rayon) ejecuta un optimizador de consultas relacional completo en una sola instancia vertical:

- Predicate Pushdown: Empuja los filtros pl.col('amount') > 1000 hasta el lector de Row Groups de Parquet antes de materializar tensores en RAM. - Projection Pushdown: Solo decodifica las columnas estrictamente referenciadas en el grafo acíclico dirigido (DAG). - Common Subplan Elimination (CSE) y ejecución por lotes Out-of-Core Streaming (.collect(streaming=True)) para procesar tablas mayores que la memoria física disponible.

Mantenimiento de Tablas Lakehouse y FinOps: Compactación Bin-Packing y Expiración Segura de Snapshots (50 min)

Mantenimiento de Tablas Lakehouse y FinOps: Compactación Bin-Packing y Expiración Segura de Snapshots

1. El Problema de los Archivos Pequeños (Small File Problem)

Si un trabajo de streaming escribe un commit en Iceberg cada $60\text{ segundos}$ sobre $32$ particiones, genera $32 \times 1440 = 46,080$ archivos Parquet de pocos kilobytes por día. Las consultas analíticas colapsan por latencia de apertura de objetos HTTP GET en S3.

2. Algoritmo de Compactación Bin-Packing y Limpieza de Archivos Huérfanos

export interface DataFileStat {
  filePath: string;
  sizeBytes: number;
  deleteRatio: number; // Fracción de filas marcadas en Position Delete Files
}

/ * Selecciona qué archivos deben reescribirse en un trabajo de compactación * Bin-Packing evitando reescribir archivos que ya tienen el tamaño óptimo. */ export function planIcebergCompactionBins( files: DataFileStat[], targetFileSizeBytes = 512 1024 1024, // 512 MB minFileSizeThreshold = 128 1024 1024, maxDeleteRatioThreshold = 0.15 ): { candidateFilesCount: number; estimatedOutputFiles: number; bytesToRewrite: number } { const candidates = files.filter( (f) => f.sizeBytes < minFileSizeThreshold || f.deleteRatio >= maxDeleteRatioThreshold );

const bytesToRewrite = candidates.reduce( (acc, f) => acc + Math.round(f.sizeBytes * (1 - f.deleteRatio)), 0 );

const estimatedOutputFiles = bytesToRewrite === 0 ? 0 : Math.max(1, Math.ceil(bytesToRewrite / targetFileSizeBytes));

return { candidateFilesCount: candidates.length, estimatedOutputFiles, bytesToRewrite }; }

Módulo 4: Módulo 4: Gobernanza de Catálogo, Auditoría de Compactación en SQL y Certificación

Interoperabilidad Multi-Motor sobre un Único Almacenamiento: Catálogos REST, Git-for-Data (Nessie) y Contratos WAP (50 min)

Interoperabilidad Multi-Motor sobre un Único Almacenamiento: Catálogos REST, Git-for-Data (Nessie) y Contratos WAP

1. El Patrón Write-Audit-Publish (WAP) Mediante Ramas de Metadatos

Con soporte de ramificación a nivel de catálogo en Apache Iceberg: 1. Write: El job de ingesta de PySpark o Polars escribe los nuevos archivos Parquet en una rama aislada audit-branch-20261021 sin alterar el snapshot main que están consultando los dashboards de BI. 2. Audit: Un validador automático verifica invariantes (ausencia de claves primarias duplicadas, completitud de particiones, PSI drift $< 0.10$). 3. Publish: Si el control pasa al $100\%$, el catálogo ejecuta un Fast-Forward Merge atómico de metadatos en $< 10\text{ ms}$ exponiendo los datos a producción; si falla, la rama se descarta sin haber ensuciado jamás los reportes ejecutivos.

Laboratorio Práctico SQL: Planificador de Compactación de Particiones Iceberg (Merge-on-Read y Small Files) (60 min)

Laboratorio Práctico SQL: Planificador de Compactación de Particiones Iceberg (Merge-on-Read y Small Files)

Contexto del Laboratorio

Operas un Data Lakehouse sobre Apache Iceberg v2 en modo Merge-on-Read (MoR). La tabla de metadatos iceberg_partition_files registra los archivos de datos Parquet de cada partición (partition_spec, file_id, file_size_mb, total_rows, deleted_rows).

Reglas de Negocio a Consultar

Para cada partition_spec, calcula: 1. total_files: cantidad total de archivos de datos en la partición (COUNT(*)::INT). 2. small_files_count: cantidad de archivos cuyo file_size_mb < 128 (COUNT(*) FILTER (WHERE file_size_mb < 128)::INT). 3. overall_delete_ratio: razón entre la suma de deleted_rows y la suma de total_rows de la partición, redondeada a 4 decimales (ROUND(SUM(deleted_rows)::NUMERIC / SUM(total_rows), 4)). 4. active_size_mb: suma de MB efectivos excluyendo las filas borradas, redondeada a entero (ROUND(SUM(file_size_mb * (1.0 - (deleted_rows::NUMERIC / total_rows))))::INT). 5. compaction_priority: - 'P1_URGENT_MOR_COMPACTION' si overall_delete_ratio >= 0.2000 O small_files_count >= 3. - 'P2_SCHEDULED_BINPACK' si no es P1 pero small_files_count >= 1. - 'OPTIMAL_NO_ACTION' en caso contrario. 6. Ordena el resultado por overall_delete_ratio DESC, partition_spec ASC.

Examen de Certificación: Principal Lakehouse, Iceberg & Real-Time OLAP Architect (DB-601) (45 min)

Examen de Certificación: Principal Lakehouse, Iceberg & Real-Time OLAP Architect (DB-601)

Demuestra tu dominio de nivel Principal sobre la arquitectura interna de Apache Iceberg, estrategias CoW vs MoR, curvas de ordenamiento espacial Z-Order, ClickHouse MergeTree y motores vectorizados (Spark AQE y Polars).