Módulo 1: From File Format To Table Format
Creando tu primer namespace y tabla
Descripción
Con el catálogo cargado en la lección 4, esta lección crea las dos piezas que le faltan para que exista, de verdad, la primera tabla Iceberg de Kiosko: el namespace kiosko —el agrupador lógico, equivalente a una base de datos o un esquema en SQL— y la tabla kiosko.fact_orders, con un esquema declarado explícitamente, columna por columna. Al final de esta lección la tabla existe, con su propio archivo de metadata en disco — pero todavía sin ninguna fila adentro. Cargar los datos reales es, con precisión, el trabajo de la lección 6.
Conexión con el módulo. Esta es la lección donde el vocabulario de las lecciones 1 a 3 se vuelve código ejecutable: el "índice del álbum" de la analogía es, ahora, un namespace y una tabla real, registrados en el catálogo que instalaste en la lección 4.
Una analogía: abrir el álbum vacío, con su índice ya impreso
Piensa en el bibliotecario de la lección anterior, ya instalado y listo. Esta lección le pide dos cosas concretas: primero, que abra una sección nueva en el estante —el namespace kiosko, un lugar donde van a vivir todos los álbumes de este negocio, separados de cualquier otro álbum que la misma biblioteca pudiera contener—; segundo, que reserve un álbum nuevo dentro de esa sección, con su portada ya impresa indicando exactamente qué columnas de información va a tener cada foto que se agregue —order_id, store_id, product_id, quantity, unit_price, revenue, order_ts—. El álbum existe, tiene portada, tiene índice — pero todavía no tiene ni una sola foto pegada adentro. Esa portada impresa de antemano, con las columnas ya declaradas, es exactamente lo que un esquema explícito de Iceberg representa.
Ejemplo trabajado: namespace, esquema y tabla, en código real
Paso 1 — Crea el namespace kiosko
# create_namespace_and_table.py
import os
from pyiceberg.catalog import load_catalog
from pyiceberg.schema import Schema
from pyiceberg.types import DoubleType, IntegerType, NestedField, StringType, TimestampType
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}",
)
catalog.create_namespace("kiosko")
print("Namespaces tras create_namespace:", catalog.list_namespaces())
Qué esperar (verificado corriendo el script real):
Namespaces tras create_namespace: [('kiosko', )]
Un namespace es, con precisión, un agrupador — no contiene ninguna fila de datos por sí mismo, solo organiza qué tablas pertenecen juntas. En un catálogo de producción real, un namespace suele mapear a algo parecido a una base de datos o un esquema SQL; aquí, kiosko va a contener, para el final de esta guía, cinco tablas: fact_orders, dim_store, dim_product, dim_date y fact_orders_at_scale.
Paso 2 — Declara el esquema, columna por columna
PyIceberg necesita un Schema explícito para crear una tabla — no lo infiere solo, aunque en la lección 6 vas a ver que sí puede verificar que un pyarrow.Table coincide con este esquema antes de cargarlo. Cada columna se declara como un NestedField, con un field_id propio —un identificador numérico interno de Iceberg, distinto del nombre de la columna, que el módulo 4 de esta guía va a usar para explicar cómo la evolución de esquema no rompe archivos viejos—:
fact_orders_schema = Schema(
NestedField(field_id=1, name="order_id", field_type=StringType(), required=True),
NestedField(field_id=2, name="store_id", field_type=StringType(), required=True),
NestedField(field_id=3, name="product_id", field_type=StringType(), required=True),
NestedField(field_id=4, name="quantity", field_type=IntegerType(), required=True),
NestedField(field_id=5, name="unit_price", field_type=DoubleType(), required=True),
NestedField(field_id=6, name="revenue", field_type=DoubleType(), required=True),
NestedField(field_id=7, name="order_ts", field_type=TimestampType(), required=True),
)
Fíjate en required=True en las siete columnas: es la declaración explícita de que ninguna de las columnas de fact_orders acepta NULL — la misma garantía que ya declaraste, sin pensarlo dos veces, como NOT NULL en el CREATE TABLE fact_orders de DuckDB en data-modeling-for-analytics-guide. El esquema de Iceberg no es una idea nueva — es el mismo control de calidad de siempre, ahora expresado en la sintaxis propia de esta biblioteca.
Paso 3 — Crea la tabla
table = catalog.create_table("kiosko.fact_orders", schema=fact_orders_schema)
print("Tabla creada:", table.name())
print()
print(table.schema())
print()
print("Tablas en el namespace kiosko:", catalog.list_tables("kiosko"))
Qué esperar (verificado corriendo el script real):
Tabla creada: ('kiosko', 'fact_orders')
table {
1: order_id: required string
2: store_id: required string
3: product_id: required string
4: quantity: required int
5: unit_price: required double
6: revenue: required double
7: order_ts: required timestamp
}
Tablas en el namespace kiosko: [('kiosko', 'fact_orders')]
table.name() devuelve una tupla de dos partes —('kiosko', 'fact_orders')—, el mismo identificador de dos niveles (namespace, table) que vas a usar durante el resto de esta guía como el string "kiosko.fact_orders". table.schema() imprime, con el mismo formato que declaraste, las siete columnas con sus tipos y su obligatoriedad — la confirmación de que Iceberg registró exactamente lo que le pediste, ni una columna más ni una menos.
Paso 4 — Confirma que la tabla existe, pero está vacía
print("Snapshot actual:", table.current_snapshot())
print("table.scan().to_arrow().num_rows =", table.scan().to_arrow().num_rows)
Qué esperar (verificado corriendo el script real):
Snapshot actual: None
table.scan().to_arrow().num_rows = 0
Esto es exactamente lo que la analogía predijo: el álbum existe, con su portada y su índice ya declarados, pero current_snapshot() devuelve None — todavía no hubo ninguna escritura, así que no existe ni un solo snapshot—, y table.scan().to_arrow() confirma cero filas. La tabla kiosko.fact_orders es, en este momento exacto, una tabla real, registrada en el catálogo, con un esquema válido — y completamente vacía. Cargar las cuarenta filas reales de Kiosko es, con precisión, el trabajo de la lección 6.
Diagrama: lo que existe en disco después de esta lección
flowchart TB
A["kiosko_catalog.db\n(SQLite)"] -->|"registra"| B["namespace: kiosko"]
B -->|"contiene"| C["tabla: kiosko.fact_orders\nschema declarado, 0 filas"]
C -->|"apunta a"| D["kiosko_warehouse/kiosko/fact_orders/\nmetadata/00000-....metadata.json"]
D -.->|"sin snapshots todavia\n(leccion 6 crea el primero)"| E["( sin archivos de datos )"]
En disco, después de esta lección, existe exactamente un archivo: kiosko_warehouse/kiosko/fact_orders/metadata/00000-<uuid>.metadata.json — el primer archivo de metadata, con el esquema que acabas de declarar, pero sin ninguna referencia a un snapshot todavía. El módulo 2 de esta guía abre ese archivo y explica, campo por campo, qué contiene.
Profundización: field_id, el identificador que sobrevive a un RENAME COLUMN
Vale la pena detenerse en un detalle que hoy parece un tecnicismo, pero que el módulo 4 de esta guía convierte en la explicación central de por qué la evolución de esquema es segura: cada NestedField tiene un field_id propio, además de su name. Un archivo Parquet, por dentro, no guarda los datos de una tabla Iceberg indexados por el nombre de la columna — los guarda indexados por su field_id. Esto significa que si en el módulo 4 renombras country a nation (un ejemplo hipotético, no parte de esta guía, pero útil para la intuición), Iceberg no necesita reescribir ni un solo archivo Parquet existente: el field_id sigue siendo el mismo, solo cambia la etiqueta con la que el catálogo lo presenta. Esta lección no profundiza más en esto —el módulo 4 completo está dedicado a evolución de esquema—, pero vale la pena que la primera vez que ves un field_id, en el esquema de fact_orders, sepas que no es un detalle decorativo: es, con precisión, la pieza que hace posible que "agregar o renombrar una columna sin reescribir datos" sea una garantía real y no una promesa de marketing.
Errores comunes
Intentar crear la tabla antes de crear el namespace. Qué pasa: alguien salta directo al paso 3 de esta lección, sin haber corrido catalog.create_namespace("kiosko") primero, y catalog.create_table("kiosko.fact_orders", ...) falla con un error indicando que el namespace no existe. Por qué pasa: es fácil asumir que un identificador de dos partes como "kiosko.fact_orders" crea automáticamente ambos niveles, como pasa en algunos sistemas de archivos con mkdir -p. Cómo detectarlo: si create_table() falla con un error mencionando NoSuchNamespaceError o similar, revisa si el namespace ya existe con catalog.list_namespaces(). Cómo corregirlo: siempre crea el namespace antes de cualquier tabla que viva dentro de él — el orden del ejemplo trabajado de esta lección (namespace primero, tabla después) no es arbitrario.
Declarar required=False (o dejarlo por defecto) en columnas que sí deberían ser obligatorias. Qué pasa: alguien copia el patrón de esta lección pero omite required=True en alguna columna, sin darse cuenta de que el valor por defecto de NestedField es required=False (columna opcional, acepta NULL). Por qué pasa: es fácil pasar por alto un parámetro con valor por defecto, especialmente si el ejemplo de referencia lo declara explícitamente en las siete columnas. Cómo detectarlo: si más adelante logras insertar filas con NULL en una columna que debería ser obligatoria (por ejemplo, un order_id vacío), revisa cómo declaraste esa columna en el Schema. Cómo corregirlo: para fact_orders, las siete columnas son obligatorias por diseño de negocio —una orden sin order_id, sin tienda o sin producto no tiene sentido—, así que las siete deben llevar required=True, exactamente como en el ejemplo trabajado.
Confundir table.current_snapshot() devolviendo None con un error. Qué pasa: alguien, al ver Snapshot actual: None después del paso 4, asume que algo falló en la creación de la tabla. Por qué pasa: None suele asociarse, por costumbre, con un valor faltante o un error, en vez de con un estado válido y esperado. Cómo detectarlo: si el resto del script (que sí imprimió el esquema correctamente en el paso 3) funcionó sin ningún error, None en current_snapshot() no es un fallo — es el estado correcto de cualquier tabla Iceberg recién creada, antes de su primera escritura. Cómo corregirlo: nada que corregir — current_snapshot() is None es, de hecho, exactamente la condición que confirma que la tabla existe pero está vacía, tal como predijo la analogía de esta lección. La lección 6 crea el primer snapshot real.
Ejercicios
Ejercicio 1 — Reproduce las cuatro partes tú mismo. Con PyIceberg ya instalado (lección 4), corre las cuatro partes del ejemplo trabajado de esta lección en tu propia máquina. Confirma que ves Namespaces tras create_namespace: [('kiosko', )], el esquema de siete columnas impreso correctamente, y Snapshot actual: None.
Ver solución
Si seguiste los cuatro pasos exactamente, tu salida debería ser idéntica a la de esta lección — a diferencia de un snapshot_id (que vas a ver por primera vez en la lección 6), nada en esta lección depende del momento en que la corras, así que tu salida debería coincidir byte a byte con la mostrada aquí. Si create_table() falla, revisa primero el primer error común de esta lección — la causa más frecuente es no haber creado el namespace antes.
Ejercicio 2 — Explica por qué field_id no es lo mismo que el orden de la columna. Sin mirar la Profundización de esta lección todavía, formula tu propia hipótesis: ¿por qué crees que Iceberg le asigna a cada columna un field_id numérico explícito, en vez de simplemente usar su posición (columna 1, columna 2, ...) dentro del esquema?
Ver solución
Si Iceberg usara solo la posición de una columna para identificarla dentro de los archivos Parquet, cualquier operación que cambiara el orden de las columnas —o que insertara una columna nueva en medio del esquema, no al final— rompería la correspondencia entre lo que un archivo viejo tiene guardado y lo que el esquema actual espera encontrar en cada posición. Un field_id explícito, asignado una vez y nunca reutilizado ni reordenado, resuelve ese problema: no importa en qué posición aparezca order_id en una versión futura del esquema, todos los archivos —viejos y nuevos— coinciden en que "lo que tiene field_id=1" es, siempre, la columna order_id. La Profundización de esta lección confirma esta intuición, y el módulo 4 la usa para explicar por qué add_column/rename_column/delete_column no reescriben datos existentes.
Ejercicio 3 — Predicción: ¿qué pasaría si intentaras crear kiosko.fact_orders una segunda vez? Sin correrlo todavía, predice: si ejecutas de nuevo catalog.create_table("kiosko.fact_orders", schema=fact_orders_schema) inmediatamente después de la lección, sin haber borrado nada, ¿qué esperas que pase? Justifica tu respuesta pensando en que el catálogo ya registró esa tabla.
Ver solución
Debería fallar, con un error indicando que la tabla ya existe (TableAlreadyExistsError o similar) — el catálogo, tal como lo describió la analogía de la lección 4, es exactamente el registro que sabe "esta tabla ya existe, con esta metadata vigente", así que pedirle que cree la misma tabla otra vez es una operación que el catálogo debe rechazar para evitar ambigüedad sobre cuál de las dos versiones sería "la vigente". Si de verdad necesitaras recrear la tabla desde cero durante esta guía, tendrías que borrarla explícitamente primero con catalog.drop_table("kiosko.fact_orders") — una operación que esta lección no usa, porque no hace falta: la tabla se crea una sola vez, y las lecciones siguientes solo le agregan datos.
Resumen y siguiente paso
En esta lección creaste el namespace kiosko y la tabla kiosko.fact_orders, con un esquema explícito de siete columnas —cada una con su field_id, su tipo y su obligatoriedad—, dentro del catálogo local que instalaste en la lección 4. Confirmaste, con table.current_snapshot() is None y table.scan().to_arrow().num_rows == 0, que la tabla existe formalmente pero todavía no tiene ninguna fila.
Antes de avanzar deberías poder: explicar la diferencia entre un namespace y una tabla; declarar un Schema de PyIceberg con NestedField, field_id y tipos correctos; y explicar por qué field_id no depende de la posición ni del nombre de una columna.
El álbum tiene portada e índice, pero sigue vacío. La lección 6 carga, por fin, las cuarenta filas reales de la semana de Kiosko —reconstruidas como fact_orders.parquet con pyarrow— dentro de esta tabla, con table.append().
Recursos
- PyIceberg — referencia de API,
Schema,NestedFieldy los tipos disponibles (StringType,IntegerType,DoubleType,TimestampType, entre otros). py.iceberg.apache.org/api. En inglés. - Apache Iceberg — documentación oficial, especificación de tabla, sección de
field-idcomo identificador estable de columna. iceberg.apache.org/docs/latest. En inglés. - DISEÑO de
data-modeling-for-analytics-guide— fuente delCREATE TABLE fact_ordersoriginal con sus columnasNOT NULL, que esta lección reproduce conrequired=True.src/guides/data-modeling-for-analytics-guide/DISENO.md. En español. - DISEÑO de esta guía — el mapa completo de los ocho módulos, incluida la sección de evolución de esquema que retoma
field_iden el módulo 4.src/guides/lakehouse-and-iceberg-guide/DISENO.md. En español.