Módulo 2: Anatomy Of An Iceberg Table
Manifest lists y manifest files
Descripción
Esta lección abre los dos eslabones del medio de la cadena: el manifest list —el índice de pruebas de esta audiencia, en la analogía del módulo— y los manifest files —las carpetas de evidencia—. A diferencia del archivo de metadata (lección 3), ninguno de los dos es JSON. Ambos son Avro, un formato binario con compresión, diseñado para guardar listas de registros de forma compacta — el mismo tipo de decisión de diseño que llevó a Parquet a ser binario y columnar en vez de texto plano. Esta lección no intenta leerlos como texto —eso es, a propósito, el error que la lección 6 usa para enseñar la lección correcta—; los inspecciona con la herramienta correcta: table.inspect.manifests() de PyIceberg.
Conexión con el módulo. La lección 3 terminó señalando el campo manifest-list dentro del snapshot del archivo de metadata — una ruta a un archivo .avro. Esta lección sigue esa ruta, y la ruta que hay un nivel más adentro: del manifest list a los manifest files que ese list enumera.
Una analogía: el índice de pruebas, y las carpetas que enumera
El índice de pruebas de una audiencia no contiene las pruebas — es una lista corta: "carpeta A, con 3 pruebas nuevas de esta audiencia; carpeta B, heredada de la audiencia anterior, sin cambios". Cada carpeta de evidencia (manifest file), a su vez, tiene su propio sub-índice, más detallado: qué prueba concreta contiene cada una, cuántas son nuevas en esta audiencia, cuántas se heredaron, cuántas se retiraron. Esta lección muestra que un manifest list enumera manifest files —en el caso de Kiosko, uno solo, porque solo hubo una escritura—, y que un manifest file enumera, a su vez, archivos de datos concretos —también uno solo, en este mismo caso—.
Ejemplo trabajado: siguiendo el puntero, con la herramienta correcta
Paso 1 — El snapshot vigente ya te dio la ruta al manifest list
# inspect_manifests.py
import os
from pyiceberg.catalog import load_catalog
warehouse_path = os.path.abspath("kiosko_warehouse")
catalog_db_path = os.path.abspath("kiosko_catalog.db")
catalog = load_catalog(
"kiosko", type="sql",
uri=f"sqlite:///{catalog_db_path}", warehouse=f"file://{warehouse_path}",
)
table = catalog.load_table("kiosko.fact_orders")
print("=== metadata.json apunta al manifest LIST del snapshot vigente ===")
snap = table.current_snapshot()
print("manifest_list:", os.path.basename(snap.manifest_list))
Qué esperar (el nombre incluye el snapshot_id de tu propia corrida, distinto cada vez):
=== metadata.json apunta al manifest LIST del snapshot vigente ===
manifest_list: snap-<snapshot-id>-0-<uuid>.avro
table.current_snapshot() devuelve el mismo objeto que ya viste en la lección 6 del módulo 1, y su atributo .manifest_list es, literal, el campo manifest-list que leíste directamente del JSON en la lección 3 de este módulo. El manifest list en sí —el archivo .avro— no lo vas a abrir como texto: contiene una lista de manifest files, codificada en formato Avro con compresión, y la forma correcta de leerlo es a través de la API de inspección de PyIceberg, que hace exactamente ese trabajo por ti.
Paso 2 — table.inspect.manifests(): una fila por manifest file
print("\n=== table.inspect.manifests() -- una fila por manifest FILE ===")
manifests = table.inspect.manifests().select(
["path", "partition_spec_id", "added_snapshot_id",
"added_data_files_count", "existing_data_files_count", "deleted_data_files_count"]
)
for row in manifests.to_pylist():
print(f" path: {os.path.basename(row['path'])}")
print(f" partition_spec_id: {row['partition_spec_id']}")
print(f" added_snapshot_id: {row['added_snapshot_id']}")
print(f" added_data_files_count: {row['added_data_files_count']}")
print(f" existing_data_files_count: {row['existing_data_files_count']}")
print(f" deleted_data_files_count: {row['deleted_data_files_count']}")
print("\ncantidad de manifest files listados:", manifests.num_rows)
Qué esperar (added_snapshot_id es el snapshot_id de tu propia corrida; el resto de la estructura y los valores de negocio son deterministas):
=== table.inspect.manifests() -- una fila por manifest FILE ===
path: <uuid>-m0.avro
partition_spec_id: 0
added_snapshot_id: <snapshot-id asignado en tu corrida, distinto cada vez>
added_data_files_count: 1
existing_data_files_count: 0
deleted_data_files_count: 0
cantidad de manifest files listados: 1
table.inspect.manifests() devuelve un pyarrow.Table normal —el mismo tipo de objeto que ya usaste en la lección 7 del módulo 1—, con una fila por manifest file. En este caso, exactamente una fila: el único manifest file que existe, con added_data_files_count: 1 —confirma que ese manifest file registra un archivo de datos nuevo, agregado en el snapshot que capturaste—, existing_data_files_count: 0 —no hereda ningún archivo de una escritura anterior, porque esta fue la primera— y deleted_data_files_count: 0 —no elimina nada—. path es la ruta al manifest file en sí, el mismo patrón <uuid>-m0.avro que ya viste en el diagrama de la lección 6 del módulo 1.
Diagrama: la cadena completa, dos niveles, un solo archivo en cada uno
flowchart TB
SNAP["snapshot vigente\n(dentro de metadata.json)"]
MLIST["manifest list\nsnap-<snapshot_id>-0-<uuid>.avro\n(1 entrada: apunta al unico manifest file)"]
MFILE["manifest file\n<uuid>-m0.avro\nadded_data_files_count=1\nexisting=0, deleted=0"]
DATA["archivo de datos\n00000-0-<uuid>.parquet\n(leccion 5)"]
SNAP -->|"snap.manifest_list"| MLIST
MLIST -->|"lista (1 fila)"| MFILE
MFILE -->|"lista (1 fila)"| DATA
Fíjate en que, con una sola escritura, cada eslabón tiene exactamente un elemento: un snapshot, un manifest list, un manifest file, un archivo de datos. Esta cadena de "uno a uno" es la más simple posible, y es a propósito el punto de partida de esta guía — en una tabla real, con muchas escrituras acumuladas, un solo manifest list típicamente enumera varios manifest files (uno por cada operación de escritura relevante, o agrupados por compactación), y cada manifest file puede enumerar varios archivos de datos. El módulo 7 de esta guía, sobre mantenimiento, vuelve a esta misma cadena cuando esos números ya no sean todos "1".
Profundización: por qué Avro, y no JSON, para estos dos eslabones
Vale la pena entender por qué Iceberg elige un formato distinto para la carátula (JSON) que para el índice y las carpetas (Avro). El archivo de metadata se lee completo, una sola vez, cada vez que alguien abre la tabla — es relativamente pequeño (unos pocos KB en esta guía), y su legibilidad como JSON ayuda a depurar problemas a mano, como hiciste en la lección 3. Los manifest files, en cambio, están diseñados para escalar a millones de archivos de datos en una tabla de producción real —piensa en kiosko_orders_at_scale.parquet, la tabla de 10 millones de filas que el módulo 5 de esta guía hereda de spark-and-distributed-processing-guide—; un formato de texto como JSON sería enormemente más pesado de almacenar y de leer a esa escala, mientras que Avro, binario y con compresión (deflate, visible en el propio archivo si lo inspeccionas byte a byte en la lección 6), guarda la misma información en una fracción del espacio, y con un esquema propio embebido en el encabezado del archivo —así que un lector no necesita ningún esquema externo para decodificarlo—. Esta es la misma decisión de diseño, aplicada al mismo problema, que ya justificó por qué los propios archivos de datos son Parquet columnar y no CSV — la lección 5 de este módulo lo retoma desde ese ángulo.
Errores comunes
Intentar contar manifest files sumando entradas del manifest list a mano, abriendo el .avro con un editor hexadecimal. Qué pasa: alguien, motivado por curiosidad técnica genuina, intenta decodificar el manifest list byte a byte para contar cuántos manifest files enumera. Por qué pasa: es una forma válida de aprender el formato Avro en abstracto, pero es un trabajo completamente innecesario para responder la pregunta real. Cómo detectarlo: si te encuentras escribiendo un decodificador Avro a mano para responder "¿cuántos manifest files tiene este snapshot?", detente — esa pregunta ya tiene una respuesta de una línea. Cómo corregirlo: table.inspect.manifests().num_rows responde exactamente esa pregunta, ya decodificada, ya en un formato que puedes filtrar y agregar con pyarrow — el ejemplo trabajado de esta lección lo hace en el paso 2.
Confundir added_data_files_count con el conteo total de filas de datos. Qué pasa: alguien ve added_data_files_count: 1 y asume que significa "una fila de datos", en vez de "un archivo de datos (que puede contener muchas filas)". Por qué pasa: en el contexto de Kiosko, con solo 40 filas en un solo archivo, la confusión no produce un número incorrecto por casualidad —pero sí produce una mala intuición para cuando los volúmenes crezcan—. Cómo detectarlo: compara added_data_files_count (de table.inspect.manifests()) contra total_rows (de table.scan().to_arrow().num_rows, ya usado en la lección 7 del módulo 1) — si esperabas que fueran el mismo número por definición, revisa esta lección de nuevo. Cómo corregirlo: added_data_files_count cuenta archivos, no filas — un solo archivo Parquet puede (y normalmente contiene) miles o millones de filas; el módulo 5 de esta guía, con 10 millones de filas de fact_orders_at_scale, hace esta distinción mucho más visible, porque ahí un manifest file va a registrar varios archivos de datos, cada uno con muchas filas adentro.
Pensar que un manifest list y un manifest file son el mismo tipo de archivo, solo con nombres distintos. Qué pasa: alguien, al ver que ambos son .avro, asume que cumplen el mismo rol y que la distinción es solo de nomenclatura. Por qué pasa: comparten extensión y formato binario, así que a simple vista (con file, por ejemplo) se ven idénticos. Cómo detectarlo: si tu código o tu explicación trata "manifest list" y "manifest file" como sinónimos, revisa el diagrama de esta lección — cada uno enumera un tipo distinto de cosa (uno enumera manifest files, el otro enumera archivos de datos), y viven en niveles distintos de la cadena. Cómo corregirlo: el patrón de nombre ayuda a distinguirlos en disco: un manifest list siempre empieza con snap-<snapshot_id>-; un manifest file termina en -m<número>.avro (-m0.avro, -m1.avro, ...). La lección 6 de este módulo usa exactamente ese patrón para identificar cada uno desde la terminal.
Ejercicios
Ejercicio 1 — Reproduce ambos pasos tú mismo. En tu propia máquina, con el estado del módulo 1 disponible, corre el script completo de esta lección. Confirma que ves added_data_files_count: 1, existing_data_files_count: 0, deleted_data_files_count: 0.
Ver solución
Si tu tabla tiene el único snapshot que dejó el módulo 1, tu salida debería coincidir exactamente en estructura y valores de negocio con la de esta lección — solo el snapshot_id/added_snapshot_id y los nombres de archivo con UUID van a ser distintos. Si manifests.num_rows no es 1, revisa cuántas veces corriste una escritura sobre esta tabla.
Ejercicio 2 — Compara el nombre del manifest file con el patrón de la lección 6 del módulo 1. Vuelve al diagrama de la lección 6 del módulo 1 (06-loading-kioskos-fact-orders-into-iceberg.md) y compara el patrón de nombre que predijo para el manifest file (metadata/<uuid>-m0.avro) contra el path real que obtuviste en el paso 2 de esta lección. ¿Coinciden?
Ver solución
Sí — el patrón <uuid>-m0.avro que el módulo 1 predijo, sin haberlo verificado todavía con código, es exactamente el mismo patrón que table.inspect.manifests() confirma en esta lección. El -m0 indica que es el primer (y, en este caso, único) manifest file asociado a esa escritura; si una sola operación de escritura llegara a generar más de un manifest file (algo que puede pasar con volúmenes grandes, fuera del alcance de esta tabla pequeña), verías -m1, -m2, etc.
Ejercicio 3 — Predicción: ¿qué cambiaría en table.inspect.manifests() si el módulo 3 hiciera un segundo append() en vez de un overwrite()? Sin adelantarte al módulo 3, predice: si en vez de un overwrite() (que reemplaza contenido) se hiciera un segundo table.append() sobre esta misma tabla, ¿esperarías que table.inspect.manifests() devuelva 1 fila o 2 filas? Justifica tu respuesta pensando en qué hizo el manifest file existente hasta ahora.
Ver solución
Depende de si Iceberg decide reutilizar el manifest existente o crear uno nuevo, pero el caso típico —y el que vas a ver en la práctica— es que un segundo append() crea un nuevo manifest file (con added_data_files_count: 1 para el archivo nuevo), y el snapshot resultante referencia, a través de un manifest list nuevo, tanto ese manifest file nuevo como el que ya existía —ahora con status "existing" en vez de "added", desde la perspectiva del snapshot nuevo—. El punto central, más importante que el número exacto, es que el manifest file de la primera escritura no se modifica ni se borra: un manifest list nuevo simplemente lo referencia otra vez, junto con lo nuevo. Esta es la misma garantía de "nada se sobrescribe" que ya viste en el archivo de metadata (lección 3) y en el catálogo (lección 2), ahora un nivel más adentro en la cadena.
Resumen y siguiente paso
En esta lección seguiste la ruta del manifest list que la lección 3 encontró en el snapshot, confirmaste que no es JSON —es Avro binario, con compresión—, y lo inspeccionaste con la herramienta correcta: table.inspect.manifests() de PyIceberg, que devolvió una fila por manifest file, con added_data_files_count: 1, existing_data_files_count: 0, deleted_data_files_count: 0 para el único manifest file que existe hasta ahora.
Antes de avanzar deberías poder: explicar la diferencia entre un manifest list y un manifest file; explicar por qué ambos usan Avro en vez de JSON; y usar table.inspect.manifests() para contar cuántos manifest files tiene el snapshot vigente de una tabla.
La lección 5 sigue la flecha por última vez: abre el archivo de datos que este único manifest file enumera — y, a diferencia de esta lección, ese archivo sí lo vas a poder abrir directamente, porque resulta ser el mismo Parquet que ya conoces de las seis guías anteriores del ecosistema.
Recursos
- Apache Iceberg — documentación oficial, "Table Spec", secciones de "Manifests" y "Manifest Lists", la definición formal de ambos formatos Avro. iceberg.apache.org/spec. En inglés.
- PyIceberg — referencia de API,
table.inspect.manifests()y las columnas exactas que devuelve. py.iceberg.apache.org/api. En inglés. - Apache Avro — documentación oficial del formato de archivo, la especificación que explica el encabezado con esquema embebido y la compresión que este módulo observa en la lección 6. avro.apache.org/docs/. En inglés.
- DISEÑO de esta guía — la advertencia explícita de que los manifest son Avro, no JSON, y deben inspeccionarse con la API de PyIceberg, nunca abriéndolos como texto.
src/guides/lakehouse-and-iceberg-guide/DISENO.md. En español.