Módulo 7: Catalogs Maintenance And Delta Lake By Contrast

Presentación del módulo: catálogos, mantenimiento y Delta Lake por contraste

Por qué existe este módulo

Los módulos 1 a 6 construyeron, pieza a pieza, una tabla Iceberg real: la cargaste (módulo 1), le mapeaste la anatomía completa —catálogo, metadata, manifest list, manifest files, archivos de datos— (módulo 2), viajaste en el tiempo sobre ella (módulo 3), le evolucionaste el esquema y la partición sin reescribir nada (módulos 4 y 5), y le aplicaste cambios parciales con MERGE INTO y upsert() (módulo 6). En ningún momento de esos seis módulos preguntaste algo que cualquier equipo que opere esta tabla en producción va a preguntar tarde o temprano: ¿qué le pasa a kiosko.dim_product si nadie la toca durante seis meses, salvo para escribir en ella todos los días?

La respuesta, con evidencia que vas a generar tú mismo en la lección 3, es incómoda: crece. No las filas —siguen siendo cuatro, una por producto—, sino el número de snapshots archivados, el número de archivos Parquet físicos que ya nadie necesita leer, y el tamaño del propio archivo de metadata. Cada escritura que hiciste en los módulos 3 a 6 —cada append(), cada overwrite(), cada upsert()— dejó, a propósito, un rastro permanente: ningún archivo de datos se borra nunca por una escritura normal, ningún snapshot desaparece solo. Eso es exactamente lo que hace posible el time travel del módulo 3. Y es exactamente lo que, sin ningún tipo de mantenimiento, convierte una tabla sana en una tabla cara de escanear y cara de almacenar.

Este módulo resuelve tres preguntas que ningún módulo anterior tocó:

  1. ¿Quién es el catálogo cuando no es un archivo SQLite en tu laptop? Los módulos 1 a 6 usaron un catálogo kiosko respaldado por SQLite, perfecto para aprender y perfecto para $0 de costo, pero ninguna empresa real pone su lakehouse de producción sobre un archivo .db en un solo disco. Esta lección nombra, sin implementar ninguno, los cuatro catálogos que sí se usan en producción: REST, AWS Glue Data Catalog, Unity Catalog y Apache Polaris — y qué garantía exacta resuelve cada uno.
  2. ¿Cómo se poda una tabla sin perder el time travel que sí necesitas? expire_snapshots, compactación de archivos pequeños, remove_orphan_files — las tres operaciones de mantenimiento que la documentación oficial de Iceberg agrupa bajo "Maintenance". Vas a correr una de verdad, con PyIceberg 0.11.1 puro Python, y vas a documentar con la misma honestidad de siempre por qué las otras dos no corren en este entorno.
  3. ¿Por qué el mercado nombra Iceberg y Delta Lake casi en la misma frase? Esta es la única lección de las ocho de esta guía que menciona Delta Lake — una sola vez, por contraste, sin construir una sola tabla Delta.

El caso que nos acompaña: kiosko.dim_product, seis meses después

Este módulo no inventa una tabla nueva para hablar de mantenimiento. Vuelve a kiosko.dim_product — la misma tabla que el módulo 3 dejó con tres snapshots (append de V1, delete + append del cambio de P002) y que el módulo 6 confirmó, explícitamente, que no tocó. Este módulo reconstruye ese estado exacto en un directorio de trabajo nuevo, y le agrega lo que le faltaba para que el problema de mantenimiento sea real y no hipotético: cinco noches más de un pipeline operativo que —sin mala intención, solo sin verificar si hacía falta— vuelve a cargar el catálogo completo de productos cada noche, incluso cuando nada cambió. Vas a ver, con table.history() ejecutado de verdad, cómo esas cinco noches redundantes multiplican por más de cuatro el número de snapshots archivados, sin agregar ni un solo bit de información de negocio nueva.

Y, por separado, vas a ver el otro problema clásico de mantenimiento —los archivos pequeños— sobre una tabla nueva y dedicada, kiosko.fact_orders_daily_batches: la misma semana de 40 órdenes de siempre, cargada esta vez día por día en vez de en un solo append(), exactamente como llegaría en un pipeline de producción real que recibe un archivo por día.

Una analogía: podar el álbum de bodas sin arrancar las páginas que la abuela todavía quiere ver

Un álbum de fotos de una boda, con los años, acumula algo que nadie planeó: la fotógrafa entregó veinte copias casi idénticas de la misma toma del brindis —el fotógrafo disparó en ráfaga, y nadie se tomó el trabajo de descartar los duplicados antes de archivar el rollo completo—. Con el tiempo, el álbum pesa el triple de lo que debería, y encontrar la foto exacta del brindis se vuelve más lento, no porque falte organización, sino porque hay demasiadas páginas casi iguales entre las que buscar.

Podar ese álbum bien hecho tiene dos reglas, no una. La primera: tirar los duplicados —diecinueve de las veinte copias del brindis pueden desaparecer sin que nadie note la diferencia, exactamente lo que hace expire_snapshots con snapshots que nadie va a volver a consultar—. La segunda, la que un podador descuidado rompe: nunca arrancar la página que la abuela todavía quiere ver —la foto, quizás borrosa, del momento exacto en que llegó el abuelo que ya no está, la única copia que existe de ese instante—. Eso es, con precisión, el snap_v1 de esta guía: el snapshot que guarda cómo era P002 antes del 2026-08-15, el que hace posible recuperar el margen correcto (10.8) con time travel. Podar bien esta tabla significa borrar las diecinueve copias redundantes del brindis, y proteger, explícitamente, la foto que todavía importa.

Diagrama: de dónde venías, a dónde vas

flowchart TB
    M3["Modulo 3:\nkiosko.dim_product\n3 snapshots -- snap_v1 protegido"]
    M6["Modulo 6:\nno toco dim_product,\nlo confirmo explicito"]
    M3 --> M7A["Este modulo:\n+5 noches redundantes\n= 13 snapshots, 7 archivos, 1 vivo"]
    M6 -.-> M7A
    M7A --> L2["L2: catalogos de produccion\nREST / Glue / Unity / Polaris\n(nombrados, no implementados)"]
    M7A --> L3["L3: por que acumula costo\n(evidencia real)"]
    L3 --> L4["L4: compactar archivos chicos\n(representativo -- Spark)"]
    L3 --> L5["L5: expire_snapshots\n(REAL, PyIceberg puro)"]
    L5 --> L6["L6: remove_orphan_files\n(representativo -- Spark)"]
    L4 --> L7["L7: Delta Lake por contraste\n(citado, no implementado)"]
    L6 --> L7
    L7 --> L8["L8: proyecto de cierre,\nassert automaticos"]
    L8 --> M8["Modulo 8:\ncapstone del lakehouse"]

El mapa de este módulo

Leccion   Que resuelve
────────  ──────────────────────────────────────────────────────────────
L1        (esta) Por que existe este modulo, el caso de las 5 noches
          redundantes, y el mapa completo
L2        Los catalogos de produccion -- REST, Glue, Unity Catalog,
          Polaris -- nombrados por su garantia, sin implementar ninguno
L3        Por que los snapshots acumulan costo -- evidencia real:
          13 snapshots, 7 archivos, 1 vivo
L4        Compactando archivos chicos -- kiosko.fact_orders_daily_batches,
          7 archivos vivos y diminutos (representativo: Spark)
L5        expire_snapshots() -- REAL, PyIceberg puro Python, con
          snap_v1 protegido y verificado
L6        remove_orphan_files -- los 5 archivos huerfanos que dejo L5,
          verificados con codigo real, borrados con Spark (representativo)
L7        Delta Lake por contraste -- una sola vez, mismo problema,
          log plano vs arbol de metadata, convergencia 2026
L8        Proyecto: la tabla mantenida de Kiosko, de punta a punta

Una advertencia técnica, declarada desde ahora

Este módulo corre enteramente sobre PyIceberg 0.11.1 puro Python —el mismo vehículo de los módulos 1 a 5 y 7 a 8, sin JVM, sin Spark—. Pero, a diferencia de esos módulos, acá vas a encontrar la primera vez en toda la guía en que PyIceberg no puede ejecutar de verdad todo lo que el mercado llama "mantenimiento de Iceberg". Se verificó, contra el código fuente instalado de PyIceberg 0.11.1 y contra su referencia de API oficial, exactamente qué existe y qué no:

Operación¿Existe en PyIceberg 0.11.1 puro Python?Tratamiento en este módulo
expire_snapshotstable.maintenance.expire_snapshots().by_id(...)/.by_ids(...)/.older_than(...).commit()L5, ejecutado de verdad
Compactación (rewrite_data_files)No — no existe ningún método equivalente en Table ni en table.maintenanceL4, representativo (Spark)
remove_orphan_filesNo — no existe ningún método equivalenteL6, representativo (Spark)

Y una precisión más fina todavía, que la lección 5 desarrolla con evidencia: incluso el expire_snapshots() que sí corre en PyIceberg no borra ningún archivo físico — solo reescribe la metadata para dejar de referenciar los snapshots expirados. La documentación oficial de Iceberg, escrita pensando en la implementación de Java/Spark, dice que expirar snapshots "elimina los archivos de datos que ya no son necesarios" — eso es cierto para la acción de Spark, pero no para el método de PyIceberg 0.11.1, verificado leyendo su propio código fuente en la lección 5. Ninguna guía anterior de este ecosistema tuvo que hacer esta distinción tan fina entre "qué dice la documentación general del proyecto" y "qué hace, literalmente, la versión instalada del cliente Python" — esta lección la hace, con evidencia, porque es exactamente el tipo de diferencia que rompe un plan de mantenimiento real si nadie la verifica primero.

La frontera: qué NO entra en este módulo

Este módulo no se conecta a ningún catálogo gestionado real: nombrar REST, Glue, Unity Catalog y Polaris no significa tener una cuenta de AWS, un workspace de Databricks o un servidor Polaris corriendo — eso, con credenciales reales y permisos, es terreno de aws-core-services-guide. Tampoco es un módulo de FinOps: calcular cuánto cuesta, en dólares, mantener snapshots viejos en S3 —o decidir con qué frecuencia correr expire_snapshots en un cluster de producción según el presupuesto— pertenece a cost-optimization-caching-guide; acá "mantenimiento" es higiene local de una tabla, verificada con conteos antes/después, no un sistema de costos. Y Delta Lake, en la lección 7, se nombra exactamente una vez, con su propia sintaxis citada contra su documentación oficial — esta guía no construye una segunda implementación paralela en ningún momento.

Errores comunes

Asumir que "PyIceberg 0.11.1" y "Apache Iceberg" tienen siempre las mismas capacidades. Qué pasa: alguien lee la documentación general de Apache Iceberg —escrita, en su mayoría, pensando en la implementación de Java y en las acciones de Spark—, encuentra ahí una operación descrita con total naturalidad, y da por sentado que pip install pyiceberg le da acceso a exactamente lo mismo. Por qué pasa: el proyecto Apache Iceberg es uno solo, con una especificación uno solo, pero tres implementaciones de cliente independientes —Java, Python (PyIceberg), Rust—, y cada una implementa un subconjunto distinto de la especificación completa, a su propio ritmo. Cómo detectarlo: si vas a depender de una operación de mantenimiento en un pipeline 100% Python, verifica primero, como hace este módulo, que el método existe de verdad en la versión instalada —dir(table.maintenance), o la referencia de API oficial de PyIceberg, nunca la documentación general del proyecto sin más—. Cómo corregirlo: las lecciones 4 y 6 de este módulo son el ejemplo permanente de cómo proceder cuando la respuesta es "no, todavía no": documentar la sintaxis real de la alternativa que sí existe (Spark, en este caso), marcarla explícitamente como representativa, y no fingir una ejecución que no ocurrió.

Pensar que "acumular snapshots" es siempre un error de diseño que hay que evitar desde el principio. Qué pasa: alguien, después de ver el problema de las cinco noches redundantes en la lección 3, concluye que la tabla del módulo 3 "estuvo mal hecha" desde el principio, o que debería haberse limpiado después de cada escritura. Por qué pasa: es fácil confundir "esto genera un costo que hay que gestionar" con "esto es un defecto que hay que prevenir". Cómo detectarlo: si terminas este módulo pensando que cada append() o overwrite() debería ir seguido de un expire_snapshots() inmediato, revisa la lección 5 — el snap_v1 que protege el time travel del módulo 3 es, literalmente, un snapshot "viejo" que no se expira, a propósito. Cómo corregirlo: acumular snapshots no es el error — no tener una política explícita de cuáles proteger y cuáles podar es el error. Este módulo enseña exactamente esa política: nunca expirar lo que el negocio todavía necesita para time travel, siempre podar lo que fue puro ruido operativo.

Ejercicios

Ejercicio 1 — Antes de leer la lección 2, predice: ¿qué tienen en común los cuatro catálogos de producción que vas a conocer, más allá de sus nombres? Piensa en lo que el módulo 2 de esta guía ya enseñó sobre el catálogo kiosko/SQLite: ¿qué garantía mínima tiene que cumplir cualquier catálogo de Iceberg, sea local o de producción?

Ver solución

Los cuatro —REST, Glue, Unity Catalog, Polaris— cumplen la misma garantía mínima que ya viste en el módulo 2 sobre el catálogo kiosko: mantener un solo puntero al archivo de metadata vigente de cada tabla, y actualizar ese puntero de forma atómica (nunca a medias) cuando una escritura nueva se confirma. La diferencia entre kiosko/SQLite y los cuatro catálogos de producción no es esa garantía —la tienen todos—, es para cuántos escritores simultáneos, desde cuántos motores distintos, y con qué mecanismo de control de concurrencia la sostienen. La lección 2 desarrolla esa diferencia con evidencia.

Ejercicio 2 — Calcula tú mismo: si el pipeline de las cinco noches redundantes hubiera usado table.upsert() en vez de table.overwrite(), ¿cuántos snapshots nuevos habría producido? Usa lo que ya confirmaste en el Ejercicio 2 de la lección 6 del módulo 6 (correr upsert() una segunda vez con datos idénticos).

Ver solución

Cero. El Ejercicio 2 de la lección 6 del módulo 6 ya demostró, con evidencia ejecutada, que table.upsert() compara cada fila contra lo que ya existe, y no produce ningún snapshot nuevo cuando no hay ningún cambio real (UpsertResult(rows_updated=0, rows_inserted=0), table.history() sin crecer). Si el pipeline nocturno de este módulo hubiera usado upsert() en vez de overwrite() ciego, las cinco noches sin cambios reales no habrían dejado ningún rastro adicional — el problema completo que la lección 3 de este módulo va a cuantificar directamente no habría existido. Este es el mismo patrón que la lección 7 del módulo 6 ya adelantó: la elección de la herramienta de escritura tiene consecuencias que van más allá del resultado inmediato.

Ejercicio 3 — Explica, en tus propias palabras, por qué esta guía elige by_ids() con snapshots capturados en variable en vez de older_than() con un timestamp de "ahora menos algunos días" para la lección 5. Piensa en la regla dura de esta guía sobre datetime.now().

Ver solución

older_than(dt) necesita un datetime de umbral, y construir ese umbral a partir de "el momento actual menos N días" implicaría, en algún punto del código, una llamada a datetime.now() o equivalente — exactamente la fuente de no-determinismo que la regla dura de esta guía prohíbe en cualquier código que alimente un bloque "Qué esperar". Con by_ids(), en cambio, la lista de snapshots a expirar se construye leyendo table.history() en el momento de la corrida y filtrando por los snapshot_id que sí conoces de antemano (snap_v1, el vigente) — ningún reloj de pared entra en la decisión, así que cualquiera que corra este módulo, en cualquier momento, obtiene exactamente el mismo resultado estructural. La lección 5 sí ejecuta older_than() una vez, en una demo aislada, para mostrar que existe y funciona — pero el flujo principal de Kiosko usa by_ids(), por esta misma razón de determinismo.

Resumen y siguiente paso

En esta lección viste por qué existe este módulo: seis módulos de esta guía construyeron una tabla Iceberg real, pero ninguno preguntó qué le pasa a esa tabla con el paso del tiempo y sin ningún mantenimiento. Conociste el mapa completo de las ocho lecciones, y la advertencia técnica central: PyIceberg 0.11.1 ejecuta de verdad expire_snapshots, pero no compactación ni remove_orphan_files — ambas se documentan como representativas, con la sintaxis exacta de Spark, verificada contra la documentación oficial.

Antes de avanzar deberías poder: nombrar las tres operaciones de mantenimiento de este módulo y cuál de las tres corre de verdad en este entorno; y explicar por qué "podar" y "proteger snap_v1" no son objetivos contradictorios.

La lección 2 no toca kiosko.dim_product todavía — nombra, uno por uno, los cuatro catálogos de producción que este módulo nunca implementa, y la garantía exacta que cada uno resuelve.

Recursos

  • Apache Iceberg — documentación oficial, "Maintenance", la fuente de las tres operaciones que organiza este módulo completo. iceberg.apache.org/docs/latest/maintenance. En inglés.
  • PyIceberg — referencia de API, la sección table.maintenance, única fuente confiable de qué corre de verdad en Python puro. py.iceberg.apache.org/api. En inglés.
  • PyIceberg — PyPI, versión vigente 0.11.1, la misma que instalaste desde el módulo 1. pypi.org/project/pyiceberg. En inglés.
  • src/paths/data-engineering-ecosystem/VALIDACION.md — la auditoría de mercado que pide, de forma explícita, que esta guía cubra "compactación y catálogos, que es donde está la discusión abierta". Documento interno del repo. En español.
  • DISEÑO de esta guía — el mapa completo de los ocho módulos, incluida la sección del módulo 7 completo. src/guides/lakehouse-and-iceberg-guide/DISENO.md. En español.