Módulo 1: From File Format To Table Format
Cuatro veces que Parquet solo no fue suficiente
Descripción
Esta lección no enseña nada nuevo de Iceberg todavía — es, con precisión, la lección de evidencia. Vas a repasar, una por una, con código citado literal de cada guía anterior, las cuatro veces exactas en que el ecosistema completo de Kiosko chocó con el mismo techo: un archivo Parquet, por más bien escrito que esté, no sabe describirse a sí mismo más allá de sus propias filas. Cada una de las cuatro guías anteriores construyó una solución real, que funciona — esta lección no las critica—, pero cada solución construyó esa capacidad por fuera del archivo, con una herramienta o una disciplina distinta cada vez.
Conexión con el módulo. Esta lección es el mapa que conecta cada módulo siguiente de esta guía con un dolor real y ya vivido. El módulo 3 (snapshots y time travel) resuelve directamente los problemas 2 y 3 de esta lección. El módulo 4 (evolución de esquema) resuelve el problema 1. El módulo 5 (partición oculta) resuelve el problema 4. Y el módulo 6 (MERGE INTO nativo) vuelve a poner los problemas 2 y 3 lado a lado con una cuarta solución. Sin esta lección, cada módulo siguiente se sentiría como una herramienta nueva sin conexión con lo ya aprendido; con ella, cada módulo es, con precisión, la respuesta a un problema que ya nombraste aquí, con código real.
Una analogía: la misma gotera, parchada cuatro veces, en cuatro pisos distintos
Imagina un edificio de cuatro pisos con un defecto estructural en la misma tubería principal: cada vez que llueve fuerte, gotea. El defecto no está en ningún piso en particular — está en la tubería misma, que no tiene ninguna válvula de cierre automático ni ningún sensor que avise cuándo empezó a gotear. Cuatro equipos de mantenimiento, en cuatro pisos distintos, resolvieron el problema del piso 1 no gotea con cuatro soluciones distintas: el primero puso baldes y un protocolo estricto de "vaciar antes de que se llene" (funciona, pero exige disciplina manual constante); el segundo instaló un medidor con etiquetas escritas a mano —"esta gota cayó el lunes", "esta el martes"— para poder reconstruir el historial si alguna vez lo necesitan; el tercero compró una máquina que pega esas mismas etiquetas automáticamente, sin que nadie tenga que escribirlas a mano; el cuarto, en el piso más alto, simplemente organizó baldes por zona geográfica del piso, de forma que cualquiera que sepa la distribución exacta puede encontrar el balde correcto sin preguntar.
Las cuatro soluciones funcionan. Ninguna arregla la tubería. Esta lección visita, uno por uno, cada uno de los cuatro pisos —cada una de las cuatro guías anteriores de este ecosistema— y muestra, con el código real que cada equipo escribió, exactamente qué parche construyeron. El resto de esta guía es, con toda propiedad, la reparación de la tubería misma.
Problema 1 — data-engineering-foundations-guide (módulo 6): el overwrite-partition que nunca fue atómico
data-engineering-foundations-guide resolvió, en su módulo 6, un problema real: si un pipeline se corre dos veces sobre el mismo día por error —algo que pasa en producción, no una situación hipotética—, un INSERT que solo agrega duplica cada fila. La solución que construyó esa guía se llama overwrite-partition, un término acuñado por Maxime Beauchemin (creador de Apache Airflow) en su ensayo Functional Data Engineering: antes de insertar los datos nuevos de una fecha, borra por completo lo que esa fecha tenía antes.
def load_overwrite_partition(con: sqlite3.Connection, rows: list[dict], partition_date: str) -> None:
"""Patron correcto: borra la particion de esa fecha ANTES de insertar."""
con.execute(
"CREATE TABLE IF NOT EXISTS staging_demo (order_id TEXT, revenue REAL, dt TEXT)"
)
con.execute("DELETE FROM staging_demo WHERE dt = ?", (partition_date,))
con.executemany(
"INSERT INTO staging_demo (order_id, revenue, dt) VALUES (?, ?, ?)",
[(r["order_id"], r["revenue"], partition_date) for r in rows],
)
con.commit()
Esta función es correcta, y la propia guía lo demostró corriéndola dos veces seguidas: la primera corrida deja 8 filas, la segunda también deja 8 —nunca 16—. Pero esa misma guía fue honesta, en su propia profundización, sobre el costo real de esta solución:
"Hay un costo real en esta decisión, y vale la pena nombrarlo con honestidad: entre el
DELETEy elINSERT, existe un instante donde la partición está vacía. Si algo falla exactamente en ese instante —el proceso se cae a la mitad—, la fecha queda temporalmente sin datos [...]. Envolver elDELETEy elINSERTen una única transacción atómica [...] es una mejora real sobre este diseño".
Ahí está el problema, con nombre y apellido: DELETE seguido de INSERT son dos operaciones separadas, y entre una y otra hay una ventana real de riesgo. Ningún archivo Parquet, ni ninguna base de datos sin soporte transaccional explícito para esta operación conjunta, puede garantizar que ambas ocurran como una sola unidad indivisible. El módulo 4 de esta guía (schema-evolution-without-rewriting) retoma esta cita exacta y muestra por qué una escritura Iceberg —table.overwrite()— sí es atómica: nunca hay un instante donde la tabla esté "a medias" entre lo viejo y lo nuevo.
Problema 2 — data-modeling-for-analytics-guide (módulos 4-5): las columnas de historia escritas a mano
data-modeling-for-analytics-guide resolvió un problema distinto: cuando P002 Energy Bar cambia de categoría y costo (snacks/0.60 → health-snacks/0.68, con fecha de vigencia 2026-08-15), cualquier consulta histórica sobre ventas anteriores a ese cambio debe seguir viendo la categoría vieja —de lo contrario, el margen calculado queda mal: 10.8 correcto (con snacks) contra 9.36 roto (si se le asigna, por error, health-snacks a ventas que ocurrieron antes de que esa categoría existiera). La solución de esa guía fue el patrón SCD tipo 2: tres columnas nuevas en dim_product_scd —valid_from, valid_to, is_current— y un MERGE INTO escrito a mano que cierra la versión vieja y abre la nueva:
MERGE INTO dim_product_scd AS target
USING staging_product AS source
ON target.product_id = source.product_id AND target.is_current = true
WHEN MATCHED AND (
target.unit_cost <> source.unit_cost OR
target.category <> source.category
) THEN UPDATE SET
valid_to = DATE '2026-08-15' - INTERVAL 1 DAY,
is_current = false
seguido de un INSERT de la fila nueva con valid_from = DATE '2026-08-15', valid_to = NULL, is_current = true. Funciona, y la propia guía lo verificó con dos corridas reales del MERGE. Pero fíjate en lo que exige de quien modela la tabla: alguien tuvo que decidir declarar esas tres columnas, alguien tuvo que escribir el MERGE con la condición exacta target.is_current = true, y cualquier consulta que quiera el estado histórico correcto tiene que acordarse de usar BETWEEN valid_from AND valid_to en vez de simplemente leer la tabla. La historia existe, pero vive en columnas que el modelador tuvo que diseñar — no en ninguna capacidad propia del formato de almacenamiento. El módulo 3 de esta guía (snapshots-and-time-travel) reproduce este mismo cambio de P002 con overwrite() normal, sin declarar ni una sola columna de historia, y recupera el estado anterior con table.scan(snapshot_id=...).
Problema 3 — dbt-analytics-engineering-guide (módulo 5): la misma técnica, automatizada por una herramienta externa
dbt-analytics-engineering-guide resolvió el mismo problema exacto —el mismo cambio de P002— con dbt snapshot: en vez de escribir el MERGE INTO a mano, declaras una vez el mecanismo, y dbt genera automáticamente tres columnas equivalentes en cada corrida:
dbt_valid_from— desde cuándo es vigente esta versión de la fila.dbt_valid_to— hasta cuándo lo fue (NULLmientras sigue vigente).dbt_scd_id— una clave única por cada versión de cada fila, calculada automáticamente.
La propia guía lo resume con precisión: "un snapshot de dbt hace [...] cada vez que corre [...] compara el estado actual de la fuente [...] contra el último fotograma archivado [...] Si cambió, ejecuta dos acciones en el mismo paso: cierra la fila vieja [...] y abre una fila nueva [...] Es, con precisión, el mismo patrón de 'cerrar antes de abrir' que data-modeling-for-analytics-guide ya construyó a mano [...] y después automatizó con MERGE INTO — un snapshot de dbt es esa misma automatización, ahora detrás de un solo comando de terminal".
Fíjate en la frase clave de esa cita: es la misma automatización, no una idea distinta. dbt snapshot resuelve el problema de "tener que escribir el MERGE a mano" —un problema real, de tecleo y de disciplina—, pero no resuelve el problema de fondo: la historia sigue viviendo en columnas (dbt_valid_from, dbt_valid_to, dbt_scd_id) que alguien tuvo que declarar, y sigue exigiendo que cualquier consulta histórica sepa filtrar correctamente por esas columnas. El módulo 3 de esta guía retoma este contraste explícitamente: kiosko.dim_product sobre Iceberg no tiene ninguna columna equivalente a dbt_valid_from — el propio motor guarda el historial completo, sin que nadie lo diseñe.
Problema 4 — spark-and-distributed-processing-guide (módulo 7): las carpetas Hive que hay que conocer de memoria
spark-and-distributed-processing-guide resolvió un problema de escala: con diez millones de filas en fact_orders_at_scale, leer la tabla completa para filtrar por una sola tienda es un desperdicio de trabajo. La solución estándar de la industria —documentada como el plan de esa guía para su módulo 7— es particionar el Parquet físicamente por carpetas, con partitionBy:
fact_orders_at_scale.write.partitionBy("store_id").parquet("kiosko_orders_at_scale.parquet")
Esto produce, en disco, una estructura de carpetas donde cada valor de store_id vive en su propio directorio —el patrón que la industria llama particionado Hive—:
kiosko_orders_at_scale.parquet/
├── store_id=S01/
│ └── part-00000-....snappy.parquet
├── store_id=S02/
│ └── part-00001-....snappy.parquet
└── store_id=S03/
└── part-00002-....snappy.parquet
Cuando alguien filtra con .filter(col("store_id") == "S01"), Spark es lo bastante inteligente como para leer solo el directorio store_id=S01/, sin tocar los otros dos — una optimización real, verificable con .explain() mostrando el plan de ejecución podado. Pero fíjate en qué depende esa optimización: depende de que la consulta filtre exactamente por la columna que decidió la estructura de carpetas, y de que el motor que lee sepa interpretar la convención columna=valor del nombre de carpeta. Si mañana Kiosko decide que la pregunta más frecuente ya no es "por tienda" sino "por día", cambiar el esquema de partición significa reescribir los diez millones de filas desde cero — la estructura de carpetas está grabada en piedra el día que se escribió. El módulo 5 de esta guía (hidden-partitioning-and-partition-evolution) contrasta esto exactamente con la partición oculta de Iceberg: la consulta sigue filtrando por store_id, sin mencionar ninguna carpeta, y el esquema de partición se puede evolucionar hacia adelante sin reescribir un solo archivo ya existente.
El mapa completo: qué construyó cada guía, y qué le faltó
| # | Guía | Qué construyó | Qué exigió de quien lo mantiene | Dónde lo resuelve Iceberg |
|---|---|---|---|---|
| 1 | data-engineering-foundations-guide (M6) | overwrite-partition: DELETE seguido de INSERT | Aceptar una ventana de riesgo real entre ambas operaciones | Módulo 4 — cada escritura es atómica |
| 2 | data-modeling-for-analytics-guide (M4-5) | valid_from/valid_to/is_current + MERGE INTO a mano | Diseñar las columnas, escribir el MERGE, recordar el JOIN correcto en cada consulta | Módulo 3 — time travel sin ninguna columna de historia |
| 3 | dbt-analytics-engineering-guide (M5) | dbt snapshot: dbt_valid_from/dbt_valid_to/dbt_scd_id | Instalar y aprender una herramienta externa que automatiza la misma técnica de columnas | Módulo 3 — el motor guarda el historial, sin columnas |
| 4 | spark-and-distributed-processing-guide (M7) | Particionado Hive: partitionBy("store_id") | Conocer de memoria la estructura de carpetas para aprovechar la poda de partición | Módulo 5 — partición oculta y evolución de partición |
Diagrama: cuatro parches, un mismo techo debajo
flowchart TB
A["foundations M6:\nDELETE + INSERT\n(ventana de riesgo)"] --> E["El mismo techo debajo\nde las cuatro soluciones:\nParquet no se describe a si mismo"]
B["data-modeling M4-5:\nvalid_from / valid_to / is_current\n(a mano)"] --> E
C["dbt M5:\ndbt_valid_from / dbt_valid_to / dbt_scd_id\n(automatizado)"] --> E
D["spark M7:\npartitionBy('store_id')\n(carpetas Hive)"] --> E
E --> F["Esta guia: Apache Iceberg\nel formato de tabla que resuelve\nlas cuatro, desde el motor"]
Profundización: por qué las cuatro soluciones son correctas Y limitadas al mismo tiempo
Vale la pena decirlo con toda claridad, porque es fácil leer esta lección como una crítica retroactiva a las seis guías anteriores: ninguna de las cuatro soluciones está mal. overwrite-partition sigue siendo, hoy, un patrón válido y ampliamente usado en la industria para pipelines batch simples. SCD tipo 2 a mano sigue siendo, en muchos equipos, exactamente lo que hay que saber hacer. dbt snapshot sigue siendo la forma estándar de historizar dimensiones dentro de un proyecto dbt. El particionado Hive sigue siendo la base de casi todo lago de datos construido en los últimos quince años, y Iceberg mismo lo soporta como una opción (partición explícita, sin transforms) para quien la necesite por compatibilidad.
Lo que las cuatro comparten no es un error — es un límite estructural de trabajar directamente con archivos: cualquier garantía que quieras (atomicidad, historia, layout eficiente) tiene que construirse por fuera del archivo, con disciplina, con columnas adicionales, o con una herramienta externa. Apache Iceberg no inventa una idea nueva para ninguno de estos cuatro problemas — mueve la responsabilidad de resolverlos desde la persona que modela o que orquesta, hacia el formato de tabla mismo, que ahora sabe, sin que nadie se lo enseñe cada vez, cómo ser atómico, cómo recordar su historia, y cómo organizarse en disco.
Errores comunes
Concluir que las seis guías anteriores "estaban mal" o "no sabían de Iceberg". Qué pasa: alguien, al ver el mapa de esta lección, interpreta que cada guía anterior cometió un error que esta guía viene a corregir. Por qué pasa: presentar cuatro soluciones seguidas de "esto se resuelve mejor con Iceberg" puede sonar, sin querer, a una corrección retroactiva. Cómo detectarlo: si tu conclusión de esta lección es "debería haber usado Iceberg desde foundations", perdiste el punto — foundations enseña, a propósito, los fundamentos sin ningún formato de tabla, exactamente porque hay que entender el problema crudo antes de apreciar la solución. Cómo corregirlo: relee la Profundización de esta lección — las cuatro soluciones son correctas dentro del alcance de la guía donde aparecieron, y siguen siendo herramientas legítimas hoy. Esta guía no las invalida: construye sobre ellas, con el mismo caso de Kiosko, para mostrar qué cambia cuando el formato de almacenamiento asume esa responsabilidad.
Memorizar los cuatro problemas sin conectar cada uno con su módulo de solución. Qué pasa: alguien lee esta lección, asiente con las cuatro historias, y sigue adelante sin quedarse con el mapa de "problema → módulo que lo resuelve". Por qué pasa: la lección tiene cuatro historias distintas, y es fácil recordarlas como anécdotas sueltas en vez de como un mapa de navegación. Cómo detectarlo: si al llegar al módulo 4 no reconoces que ese módulo retoma, con nombre, la cita exacta de data-engineering-foundations-guide sobre la ventana de riesgo del overwrite-partition, perdiste la conexión que esta lección construyó a propósito. Cómo corregirlo: usa la tabla de esta lección ("El mapa completo") como referencia activa durante el resto de la guía — cada vez que empieces un módulo nuevo, vuelve a esa tabla y confirma qué problema, de los cuatro, ese módulo específico viene a resolver.
Ejercicios
Ejercicio 1 — Empareja el problema con la solución de Iceberg. Sin mirar la tabla de esta lección, empareja cada uno de los cuatro problemas con el módulo de esta guía que lo resuelve: (a) columnas de historia a mano, (b) ventana de riesgo entre DELETE e INSERT, (c) carpetas Hive que hay que conocer de memoria, (d) columnas de historia automatizadas con una herramienta externa.
Ver solución
(a) y (d) se resuelven en el módulo 3 (snapshots y time travel) — ambos son, en el fondo, el mismo problema (historia de una dimensión), resuelto una vez sin columnas de historia. (b) se resuelve en el módulo 4 (evolución de esquema sin reescribir), que retoma explícitamente la cita de data-engineering-foundations-guide sobre la ventana de riesgo del overwrite-partition y muestra por qué una escritura Iceberg es atómica. (c) se resuelve en el módulo 5 (partición oculta y evolución de partición), contrastando partitionBy("store_id") con la partición oculta de Iceberg.
Ejercicio 2 — Reconstruye el overwrite-partition con tus propias palabras. Sin mirar el código de esta lección, escribe en pseudocódigo (no en SQL ni Python exacto) los dos pasos del patrón overwrite-partition, y en 1-2 frases explica por qué ese patrón no es atómico.
Ver solución
Pseudocódigo: 1. Borrar todos los datos que ya existen para la fecha que se va a cargar. 2. Insertar los datos nuevos de esa misma fecha. No es atómico porque son dos operaciones separadas, ejecutadas en secuencia: entre el paso 1 y el paso 2 existe un instante real donde la partición está vacía. Si el proceso falla exactamente en ese instante, la partición queda sin datos hasta la próxima corrida exitosa — un problema visible, pero real, que ninguna base de datos sin transacciones explícitas sobre ambas operaciones puede prevenir por sí sola.
Ejercicio 3 — Explica la diferencia entre el problema 2 y el problema 3 con tus propias palabras. Los problemas 2 (data-modeling) y 3 (dbt) resuelven, en el fondo, el mismo caso de negocio —el cambio de P002—. En 2-3 frases, explica qué es exactamente lo que cambia entre una solución y la otra, y qué es lo que no cambia.
Ver solución
Lo que cambia es quién escribe el mecanismo de cierre-y-apertura: en data-modeling, una persona escribe el MERGE INTO completo a mano, statement por statement; en dbt, una herramienta externa (dbt snapshot) genera ese mismo mecanismo automáticamente, a partir de una configuración declarativa. Lo que no cambia es la técnica de fondo: ambas soluciones agregan columnas de historia a la tabla (valid_from/valid_to/is_current en un caso, dbt_valid_from/dbt_valid_to/dbt_scd_id en el otro), y ambas exigen que cualquier consulta histórica sepa filtrar correctamente por esas columnas para obtener el resultado correcto. Ninguna de las dos mueve la responsabilidad de guardar historia hacia el formato de almacenamiento mismo — eso es, con precisión, lo que el módulo 3 de esta guía hace por primera vez.
Resumen y siguiente paso
En esta lección repasaste, con código citado literal de cada guía anterior, las cuatro veces exactas en que el ecosistema completo de Kiosko chocó con el mismo techo: overwrite-partition no atómico (foundations), columnas de historia a mano (data-modeling), columnas de historia automatizadas (dbt), y carpetas Hive que hay que conocer de memoria (spark). Construiste el mapa completo que conecta cada uno de esos cuatro problemas con el módulo exacto de esta guía que lo resuelve.
Antes de avanzar deberías poder: nombrar las cuatro guías y su problema específico, de memoria; y explicar por qué las cuatro soluciones son correctas dentro de su alcance, y al mismo tiempo comparten la misma limitación estructural.
La lección 3 da el paso conceptual que hace posible entender por qué Iceberg resuelve los cuatro problemas desde un solo lugar: la distinción precisa entre un formato de archivo y un formato de tabla.
Recursos
- Maxime Beauchemin — "Functional Data Engineering: a modern paradigm for batch data processing", el ensayo que acuñó el término
overwrite-partition, citado pordata-engineering-foundations-guide. maximebeauchemin.medium.com/functional-data-engineering-a-modern-paradigm-for-batch-data-processing-2327ec32c42a. En inglés. - DISEÑO de
data-engineering-foundations-guide— fuente del patrónoverwrite-partitiony la cita exacta sobre la ventana de riesgo.src/guides/data-engineering-foundations-guide/DISENO.md. En español. - DISEÑO de
data-modeling-for-analytics-guide— fuente dedim_product_scd, elMERGE INTOde SCD tipo 2, y los númerosmargin=10.8/9.36.src/guides/data-modeling-for-analytics-guide/DISENO.md. En español. - DISEÑO de
dbt-analytics-engineering-guide— fuente dedbt snapshoty las columnasdbt_valid_from/dbt_valid_to/dbt_scd_id.src/guides/dbt-analytics-engineering-guide/DISENO.md. En español. - DISEÑO de
spark-and-distributed-processing-guide— fuente defact_orders_at_scale,partitionBy("store_id")y el particionado Hive de su módulo 7.src/guides/spark-and-distributed-processing-guide/DISENO.md. En español. - DISEÑO de esta guía — el mapa completo de los ocho módulos y la advertencia de mercado que cita esta lección.
src/guides/lakehouse-and-iceberg-guide/DISENO.md. En español.