Módulo 4: Schema Evolution Without Rewriting
Renombrando y borrando columnas de forma segura
Descripción
La lección 3 demostró que agregar una columna es una operación de metadata pura. Esta lección extiende esa misma garantía a otras dos operaciones: renombrar una columna (rename_column) y borrarla (delete_column). Vas a renombrar store_name de ida y vuelta, confirmando que ningún archivo Parquet se toca ni siquiera cuando el nombre visible de una columna cambia dos veces. Y vas a agregar una columna de práctica, temp_notes, para borrarla en la misma lección — la prueba directa de que un DROP COLUMN en Iceberg tampoco reescribe ningún dato, algo que sorprende a cualquiera que venga de un sistema donde borrar una columna es una operación cara.
Conexión con el módulo. La lección 3 dejó kiosko.dim_store con cuatro columnas, country todavía vacía. Esta lección no toca country para nada —eso es trabajo de la lección 5—; en cambio, usa el resto de la tabla como escenario para completar el mecanismo de update_schema(): ya conoces add_column, esta lección agrega rename_column y delete_column al mismo vocabulario.
Una analogía: cambiar la etiqueta de una gaveta, y vaciar una que ya no hace falta
Piensa en un archivero físico, con gavetas etiquetadas. Renombrar una columna es como despegar la etiqueta vieja de una gaveta y pegar una nueva —"Nombre del local" en vez de "store_name", digamos— sin tocar ni una sola carpeta de adentro: el contenido de la gaveta es exactamente el mismo, solo cambió el papel pegado afuera. Borrar una columna es distinto, pero igual de simple desde el punto de vista del archivero completo: una gaveta que ya no hace falta se retira del índice —nadie vuelve a buscar nada ahí—, pero eso no obliga a vaciar, quemar ni reorganizar el resto del archivero. Las demás gavetas, con sus propias etiquetas, siguen exactamente donde estaban.
Ejemplo trabajado: renombrar de ida y vuelta, y agregar-y-borrar en la misma lección
Paso 1 — rename_column: store_name → outlet_name, y de vuelta
# rename_and_scratch_column.py
import os
from pyiceberg.catalog import load_catalog
from pyiceberg.types import StringType
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.dim_store")
files_before = sorted(f["file_path"].split("/")[-1] for f in table.inspect.files().to_pylist())
snapshots_before = len(table.history())
with table.update_schema() as update:
update.rename_column("store_name", "outlet_name")
print("Esquema tras rename_column('store_name', 'outlet_name'):")
print(table.schema())
print()
print("scan() tras el rename -- los DATOS no cambiaron, solo el nombre de columna:")
for row in table.scan().to_arrow().to_pylist():
print(" ", row)
files_after_rename = sorted(f["file_path"].split("/")[-1] for f in table.inspect.files().to_pylist())
print()
print("Archivos identicos tras el rename:", files_before == files_after_rename)
print("Snapshots antes:", snapshots_before, "-- despues:", len(table.history()))
with table.update_schema() as update:
update.rename_column("outlet_name", "store_name")
print()
print("Esquema tras revertir el rename (vuelve a store_name):")
print(table.schema())
Qué esperar (verificado corriendo el script real):
Esquema tras rename_column('store_name', 'outlet_name'):
table {
1: store_id: required string
2: outlet_name: required string
3: city: required string
4: country: optional string
}
scan() tras el rename -- los DATOS no cambiaron, solo el nombre de columna:
{'store_id': 'S01', 'outlet_name': 'Kiosko Centro', 'city': 'Bogota', 'country': None}
{'store_id': 'S02', 'outlet_name': 'Kiosko Norte', 'city': 'Lima', 'country': None}
{'store_id': 'S03', 'outlet_name': 'Kiosko Sur', 'city': 'Santiago', 'country': None}
Archivos identicos tras el rename: True
Snapshots antes: 1 -- despues: 1
Esquema tras revertir el rename (vuelve a store_name):
table {
1: store_id: required string
2: store_name: required string
3: city: required string
4: country: optional string
}
Fíjate en el field_id de la segunda columna: es 2 antes del rename, 2 durante (como outlet_name), y 2 otra vez al volver a store_name. El nombre visible cambió dos veces; el identificador interno —el que de verdad conecta el esquema con los datos físicos del Parquet— nunca se movió. Eso es lo que hace que renombrar sea seguro: cualquier archivo de datos escrito antes del rename se sigue leyendo correctamente después, porque nunca dependió del nombre de texto, solo del field_id.
Paso 2 — temp_notes: agregada, y borrada, en la misma lección
with table.update_schema() as update:
update.add_column("temp_notes", StringType())
print("\nEsquema tras add_column('temp_notes'):")
print(table.schema())
files_after_temp_notes = sorted(f["file_path"].split("/")[-1] for f in table.inspect.files().to_pylist())
print("Archivos identicos tras agregar temp_notes:", files_before == files_after_temp_notes)
with table.update_schema() as update:
update.delete_column("temp_notes")
print("\nEsquema tras delete_column('temp_notes') -- el DROP:")
print(table.schema())
files_after_drop = sorted(f["file_path"].split("/")[-1] for f in table.inspect.files().to_pylist())
print("Archivos identicos tras el DROP:", files_before == files_after_drop)
print("Snapshots totales tras rename + add + drop (todas operaciones de esquema, cero de datos):",
len(table.history()))
Qué esperar (verificado corriendo el script real, continuando sobre la misma tabla del paso 1):
Esquema tras add_column('temp_notes'):
table {
1: store_id: required string
2: store_name: required string
3: city: required string
4: country: optional string
5: temp_notes: optional string
}
Archivos identicos tras agregar temp_notes: True
Esquema tras delete_column('temp_notes') -- el DROP:
table {
1: store_id: required string
2: store_name: required string
3: city: required string
4: country: optional string
}
Archivos identicos tras el DROP: True
Snapshots totales tras rename + add + drop (todas operaciones de esquema, cero de datos): 1
temp_notes llegó a existir en el esquema, con field_id=5, y desapareció por completo sin que ni el rename, ni el add, ni el drop movieran el conteo de snapshots ni tocaran el único archivo Parquet de esta tabla — sigue siendo 1 desde la lección 3. Cuatro operaciones de esquema distintas —renombrar, renombrar de vuelta, agregar, borrar—, todas confirmadas, y el historial de escrituras de datos no se movió ni una vez.
Paso 3 — El field_id=5 nunca vuelve a usarse
from pyiceberg.types import StringType
with table.update_schema() as update:
update.add_column("scratch_check", StringType())
print("Tras agregar una columna nueva DESPUES del drop de temp_notes (que uso field_id=5):")
for f in table.schema().fields:
print(f" field_id={f.field_id} name={f.name}")
with table.update_schema() as update:
update.delete_column("scratch_check")
Qué esperar (verificado corriendo el script real):
Tras agregar una columna nueva DESPUES del drop de temp_notes (que uso field_id=5):
field_id=1 name=store_id
field_id=2 name=store_name
field_id=3 name=city
field_id=4 name=country
field_id=6 name=scratch_check
scratch_check recibió field_id=6, saltando el 5 que temp_notes ya usó y dejó atrás. Iceberg nunca reutiliza un field_id, ni siquiera después de que la columna original se borre por completo — exactamente la misma garantía que la lección 3 ya demostró para columnas agregadas sobre datos existentes, aplicada aquí a columnas que ya no existen.
Diagrama: cuatro operaciones de esquema, cero snapshots de datos
flowchart LR
A["dim_store\n4 columnas (con country=None)\nfield_id 1..4\n1 snapshot"] -->|"rename_column\nstore_name -> outlet_name"| B["mismo field_id=2,\nnombre nuevo"]
B -->|"rename_column\noutlet_name -> store_name"| C["field_id=2,\nnombre original de vuelta"]
C -->|"add_column\ntemp_notes"| D["field_id=5 nuevo,\n5 columnas"]
D -->|"delete_column\ntemp_notes"| E["field_id=5 retirado,\n4 columnas otra vez"]
E -.->|"1 snapshot en total,\nsin cambios"| F["table.history()\nsigue en 1"]
Profundización: por qué borrar una columna no libera espacio de inmediato
Vale la pena ser preciso sobre qué significa, físicamente, que delete_column("temp_notes") "no reescriba datos". Si temp_notes hubiera llegado a tener valores reales escritos en el Parquet —esta lección la borró antes de poblarla, a propósito, para aislar el mecanismo—, esos valores seguirían existiendo, físicamente, dentro del archivo Parquet después del DROP. Lo que cambia es que el esquema vigente ya no proyecta esa columna: cualquier lectura nueva (table.scan()) simplemente ignora esa porción del archivo, como si no existiera. El espacio en disco que esos valores viejos ocupan no se recupera con delete_column — se recupera, si hace falta, con una operación de mantenimiento explícita como la compactación (rewrite_data_files), que el módulo 7 de esta guía enseña. Esta distinción —"el esquema deja de ver la columna" contra "el disco deja de contener los bytes"— es la misma que ya viste en el módulo 3, cuando un overwrite() "borraba" filas sin borrar, de inmediato, el archivo Parquet que las contenía: en los dos casos, Iceberg prioriza no tocar archivos existentes sobre liberar espacio de inmediato, y deja la limpieza física para un paso de mantenimiento aparte, deliberado.
Errores comunes
Asumir que delete_column en Iceberg es tan costoso como en un sistema que sí reescribe archivos. Qué pasa: alguien, familiarizado con un motor donde borrar una columna de una tabla grande dispara una reescritura completa que puede tardar horas, evita agregar columnas de práctica en Iceberg "por las dudas", pensando que limpiar después va a ser caro. Por qué pasa: en muchos sistemas relacionales tradicionales, y en Parquet plano sin ningún formato de tabla encima, cambiar el esquema de un archivo ya escrito sí exige reescribirlo entero. Cómo detectarlo: si evitas experimentar con add_column/delete_column en una tabla Iceberg por miedo al costo, revisa la evidencia de esta lección — el conteo de snapshots y la lista de archivos no cambiaron ni una vez en las cuatro operaciones. Cómo corregirlo: en Iceberg, agregar y borrar columnas son operaciones de metadata, prácticamente instantáneas sin importar cuántas filas tenga la tabla — el costo de reescribir datos, si alguna vez hace falta, es una decisión separada y explícita (compactación, módulo 7), no una consecuencia automática de evolucionar el esquema.
Confundir "el field_id nunca se reutiliza" con "los nombres de columna nunca se pueden repetir". Qué pasa: alguien, después de borrar temp_notes, intenta volver a agregar una columna con el mismo nombre temp_notes y espera que falle, pensando que el nombre también queda "quemado" como el field_id. Por qué pasa: es fácil generalizar la regla de "nunca se reutiliza" del field_id al nombre visible, que es lo que la mayoría de la gente percibe primero. Cómo detectarlo: si evitas reutilizar un nombre de columna ya borrado por miedo a un conflicto, prueba a agregarlo de nuevo. Cómo corregirlo: el nombre de una columna sí se puede reutilizar libremente después de un DROP —Iceberg simplemente le asigna un field_id nuevo, más alto, a esa columna "nueva" con nombre repetido—; lo único que nunca se reutiliza es el número interno. scratch_check, en el paso 3 de esta lección, podría haberse llamado temp_notes otra vez sin ningún problema, y habría recibido field_id=6 igual.
Ejercicios
Ejercicio 1 — Reproduce las cuatro operaciones tú mismo, y confirma el field_id de cada columna al final. Con kiosko.dim_store en el estado que dejó la lección 3, corre los tres pasos de esta lección. Confirma que el esquema final tiene exactamente cuatro columnas (store_id, store_name, city, country), con los field_id 1, 2, 3, 4 — sin ningún rastro de temp_notes ni de scratch_check.
Ver solución
Si partiste del estado exacto de la lección 3, tu esquema final debería coincidir con el de esta lección: cuatro columnas, field_id 1 a 4 en orden, store_name de vuelta a su nombre original. El field_id de la próxima columna que agregues (si lo intentas) va a ser 7, no 5 ni 6 — porque tanto temp_notes (5) como scratch_check (6) ya "quemaron" esos números en esta lección.
Ejercicio 2 — Predicción: ¿qué pasaría si intentaras renombrar store_id a city, cuando city ya existe? Sin correrlo todavía, predice: si llamaras update.rename_column("store_id", "city") sobre el esquema actual de dim_store —que ya tiene una columna llamada city—, ¿qué esperas que pase?
Ver solución
PyIceberg rechaza esa operación, porque produciría dos columnas con el mismo nombre visible dentro del mismo esquema —una ambigüedad que ningún motor de consulta podría resolver de forma confiable (¿a cuál city se refiere una consulta que la mencione?)—. La garantía de "cada columna tiene un field_id único" no elimina la necesidad de que los nombres, dentro de un mismo nivel del esquema, sigan siendo únicos; rename_column valida esto antes de aceptar el cambio, exactamente como lo haría agregar una columna con un nombre ya usado.
Ejercicio 3 — Explica, con tus propias palabras, por qué esta lección revierte el rename de store_name antes de seguir adelante. En 1-2 frases, explica por qué el paso 1 de esta lección deja dim_store con el nombre store_name restaurado, en vez de continuar el resto del módulo con outlet_name.
Ver solución
El propósito del rename en esta lección es puramente demostrativo —probar que el mecanismo es seguro y no toca datos—, no un cambio de negocio real que el resto de esta guía necesite. Revertirlo mantiene kiosko.dim_store con el nombre de columna que las lecciones 5 a 8 —y el resto del ecosistema de Kiosko, que siempre usó store_name— esperan encontrar, evitando que un experimento de esta lección introduzca una inconsistencia de nombres en el resto del módulo.
Resumen y siguiente paso
En esta lección extendiste el vocabulario de table.update_schema() con rename_column y delete_column, además del add_column de la lección anterior. Confirmaste, con la misma disciplina de comparar listas de archivos y conteos de snapshots, que renombrar una columna de ida y vuelta, y agregar-y-borrar una columna de práctica, no tocan ningún archivo de datos ni crean ningún snapshot nuevo. Y viste, con evidencia directa, que el field_id de una columna borrada —temp_notes, con field_id=5— nunca se reutiliza, ni siquiera para una columna con el mismo nombre.
Antes de avanzar deberías poder: renombrar y borrar una columna de una tabla Iceberg con table.update_schema(); explicar por qué un field_id borrado nunca se reutiliza; y distinguir "el esquema deja de proyectar una columna" de "el disco libera el espacio que esa columna ocupaba".
kiosko.dim_store está de vuelta a sus cuatro columnas limpias, con country todavía en None para las tres tiendas. La lección 5 hace el trabajo real: poblar country de forma determinística a partir de city, con los tres países exactos de Kiosko.
Recursos
- Apache Iceberg — documentación oficial, "Evolution", sección "Schema evolution" y "Correctness", la fuente formal de las garantías de rename y delete que esta lección ejecuta. iceberg.apache.org/docs/latest/evolution. En inglés.
- PyIceberg — referencia de API,
UpdateSchema.rename_column(path_from, new_name)yUpdateSchema.delete_column(path). py.iceberg.apache.org/api. En inglés. - DISEÑO de esta guía — la sección "Evolución de esquema" (M4), fuente exacta de
temp_notescomo columna agregada y borrada en la misma lección, para demostrar que unDROP COLUMNno reescribe datos.src/guides/lakehouse-and-iceberg-guide/DISENO.md. En español.