Módulo 4: Schema Evolution Without Rewriting
Presentación del módulo: evolución de esquema sin reescritura
Por qué existe este módulo
El módulo 1 de esta guía, en su lección 2, visitó cuatro veces en que el ecosistema de Kiosko chocó con el mismo techo. La primera de esas cuatro fue data-engineering-foundations-guide (módulo 6): el patrón overwrite-partition —borrar la partición de una fecha, después insertar los datos nuevos de esa fecha— que resuelve, de verdad, el problema de los reruns duplicados. Esa guía fue honesta sobre el costo de su propia solución, con una cita que esta lección va a retomar palabra por palabra en la lección 2: entre el DELETE y el INSERT hay un instante real donde la partición queda vacía, y si el proceso muere justo ahí, la fecha se queda sin datos. Dos operaciones separadas, ejecutadas en secuencia, con una ventana de riesgo en el medio — eso es, con precisión, lo contrario de una operación atómica.
Este módulo cierra esa promesa pendiente. Vas a ver, con evidencia ejecutada —no con una afirmación de folleto—, por qué una escritura de Iceberg nunca dejar a la tabla "a medias" entre un estado y el siguiente, ni siquiera cuando esa escritura, por dentro, hace más de una cosa a la vez (ya lo viste en el módulo 3: table.overwrite() puede producir un snapshot delete y un snapshot append en la misma llamada). Y vas a usar esa misma garantía de atomicidad como la base sobre la que construir algo nuevo: evolucionar el esquema de una tabla —agregar una columna, renombrarla, borrarla— sin tocar ni un solo archivo Parquet que ya existe.
El caso que nos acompaña: kiosko.dim_store aprende de dónde es cada tienda
Hasta este módulo, kiosko.dim_store no existía en ningún catálogo Iceberg de esta guía — es una tabla nueva, las tres tiendas de Kiosko con las que ya trabajaste en las seis guías anteriores del ecosistema: S01 Kiosko Centro (Bogotá), S02 Kiosko Norte (Lima), S03 Kiosko Sur (Santiago). Este módulo la crea con tres columnas —store_id, store_name, city— y después le agrega una cuarta: country, derivada de forma determinística de city (Bogotá→Colombia, Lima→Peru, Santiago→Chile). También agrega, y borra, una columna scratch llamada temp_notes, solo para demostrar con evidencia que un DROP COLUMN tampoco reescribe ningún archivo de datos.
Lo que hace interesante a este caso no es el resultado final —una tabla de tres tiendas con una columna más es, en sí, poco dramático—. Lo interesante es cómo llega ahí: sin que ningún archivo Parquet ya escrito se toque, sin ninguna migración que bloquee la tabla, sin que un lector que esté consultando kiosko.dim_store en ese instante vea, ni por un microsegundo, un esquema a medio cambiar.
Una analogía: el formulario de censo, no la puerta de nadie
Imagina un censo nacional que ya recorrió media ciudad, casa por casa, con un formulario de tres preguntas. A mitad de camino, alguien decide que el formulario necesita una cuarta pregunta —el país de nacimiento, digamos—. Hay dos formas de manejar esto. La mala: volver a tocar la puerta de cada casa ya censada, para que complete la pregunta nueva desde cero — carísimo, lento, y probablemente imposible si algunas de esas casas ya nadie responde. La buena: agregar la pregunta al formulario de aquí en adelante, dejar en blanco (o completar con un valor conocido, si se puede deducir) la respuesta de quienes ya fueron censados, y seguir censando el resto de la ciudad con el formulario de cuatro preguntas. Nadie vuelve a tocar ninguna puerta ya tocada.
Eso es, con precisión, lo que este módulo hace con kiosko.dim_store. Agregar country no reescribe las tres filas que ya existen —no se "vuelve a tocar la puerta" de S01, S02 ni S03—; el esquema nuevo simplemente empieza a aplicar desde ese momento en adelante, y las filas viejas quedan disponibles para completarse, en este caso con un valor que sí se puede deducir de un dato que ya tenían (city). Y la atomicidad de la lección 2 es la otra mitad de la misma idea, con una analogía distinta: el mostrador de una oficina de censo que abre o actualiza un registro en un solo trámite, nunca a medias — nunca vas a encontrar un registro con el nombre ya cambiado pero la dirección todavía vieja, porque el mostrador no entrega ningún resultado parcial a quien pregunta mientras el trámite está en curso.
Diagrama: de dónde parte este módulo, hacia dónde llega
flowchart LR
A["foundations M6:\nDELETE + INSERT\nventana de riesgo real,\ncitada literal"] --> B["Leccion 2:\npor que una escritura\nIceberg SI es atomica\n(evidencia ejecutada)"]
B --> C["Leccion 3:\nadd_column('country')\nsolo metadata, 0 archivos tocados"]
C --> D["Leccion 4:\nrename + add/drop 'temp_notes'\nseguros, mismo mecanismo"]
D --> E["Leccion 5:\npoblar country desde city\nBogota->Colombia, Lima->Peru,\nSantiago->Chile"]
E --> F["Leccion 6:\nsnap_before_evolution\nsigue leyendo el esquema viejo"]
F --> G["Leccion 7:\nque garantiza 'ACID' aqui,\ncon precision"]
G --> H["Leccion 8:\nProyecto: dim_store\nevolucionada, de punta a punta"]
El mapa de este módulo
Leccion Que resuelve
──────── ──────────────────────────────────────────────────────────────
L1 (esta) El mapa completo: de la cita de foundations al proyecto final
L2 Por que overwrite-partition nunca fue atomico -- y por que
table.overwrite() de Iceberg si lo es, con evidencia ejecutada
L3 add_column() sobre kiosko.dim_store: una operacion de metadata,
cero archivos Parquet tocados
L4 rename_column() y delete_column(): la misma garantia, con
temp_notes agregada y borrada en la misma leccion
L5 country poblado deterministicamente desde city -- el pago real
de la columna nueva
L6 table.scan(snapshot_id=snap_before_evolution) sigue leyendo
el esquema de 3 columnas, sin country
L7 Que garantiza "ACID" en este contexto exacto -- ni mas, ni menos
L8 Proyecto: dim_store evolucionada, de punta a punta, con assert
Las lecciones 3 y 4 construyen el mecanismo —agregar, renombrar, borrar una columna— sin ningún dato de negocio todavía, sobre temp_notes como columna de práctica. La lección 5 es el pago real: country, poblada con los tres países de Kiosko. La lección 6 verifica algo que no es evidente a primera vista: un snapshot anterior a la evolución de esquema sigue leyéndose con su propio esquema, no con el que la tabla tiene ahora. La lección 7 da un paso atrás y responde, con precisión y sin exagerar, qué garantiza la palabra "ACID" en el contexto de una sola tabla Iceberg —que no es lo mismo que un motor transaccional completo con múltiples tablas—. La lección 8 junta las siete piezas en un proyecto único.
La frontera: qué NO entra en este módulo
Este módulo evoluciona el esquema de una tabla local, con un catálogo 100% en tu propia máquina. No entra aquí la publicación de contratos de esquema versionados como artefacto de gobierno —qué columnas pueden cambiar, quién aprueba el cambio, cómo se notifica a los consumidores aguas abajo—, que es exactamente el trabajo de data-reliability-and-governance-guide. Tampoco entra el particionado de kiosko.dim_store ni de ninguna otra tabla —eso llega en el módulo 5, con kiosko.fact_orders_at_scale—. Y este módulo no toca kiosko.fact_orders ni kiosko.dim_product, las dos tablas de los módulos 1 a 3: kiosko.dim_store es una tabla nueva, aislada, construida específicamente para este módulo.
Errores comunes
Esperar que "evolución de esquema" signifique lo mismo que "cambiar los datos". Qué pasa: alguien, al escuchar "agregar una columna", asume que Iceberg reescribe automáticamente cada fila con un valor calculado para la columna nueva. Por qué pasa: en muchas herramientas cotidianas —una hoja de cálculo, un formulario web— "agregar una columna" y "llenarla con datos" ocurren en el mismo gesto, así que es natural esperar lo mismo aquí. Cómo detectarlo: si después de add_column("country", ...) esperas ver los tres países ya poblados, sin haber corrido ningún paso adicional, revisa la lección 3 —el resultado real es country=None para las tres filas—. Cómo corregirlo: este módulo separa, a propósito, en dos pasos distintos lo que parece una sola acción: la lección 3 agrega la columna (metadata, instantáneo, cero filas tocadas); la lección 5 la puebla con datos reales (una escritura normal, con su propio snapshot). Son dos operaciones de naturaleza distinta, y esta guía las trata como tales.
Confundir "atómico" con "instantáneo" o con "sin ningún costo". Qué pasa: alguien interpreta que una operación atómica de Iceberg no toma tiempo, o que "atómico" significa que Iceberg es, en general, más rápido que cualquier alternativa. Por qué pasa: la palabra "atómico" en el lenguaje cotidiano a veces se usa como sinónimo vago de "rápido" o "simple". Cómo detectarlo: si tu definición de "atómico" no menciona la palabra "indivisible" ni explica qué pasa si el proceso falla a mitad de camino, todavía no tienes la definición precisa que este módulo usa. Cómo corregirlo: "atómico" significa, con precisión, que una operación ocurre como una sola unidad indivisible desde afuera — o se ve completa, o no se ve en absoluto; nunca a medias. No dice nada sobre velocidad. La lección 2 de este módulo construye esta definición con evidencia real, no con una analogía suelta.
Ejercicios
Ejercicio 1 — Antes de empezar, recupera la cita exacta de data-engineering-foundations-guide. Sin mirar atrás todavía, intenta recordar (o busca en la lección 2 del módulo 1 de esta guía) la frase exacta que esa guía usó para describir el riesgo del patrón overwrite-partition. Anótala, porque la lección 2 de este módulo la retoma palabra por palabra.
Ver solución
La cita central, de la profundización de la lección 3 del módulo 6 de data-engineering-foundations-guide, dice: "Hay un costo real en esta decisión, y vale la pena nombrarlo con honestidad: entre el DELETE y el INSERT, 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 [...]". Esa misma guía reconoce, en la frase siguiente, que envolver ambas operaciones en una transacción explícita "es una mejora real sobre este diseño" — exactamente la mejora que una escritura Iceberg entrega de fábrica, sin que nadie tenga que envolver nada a mano.
Ejercicio 2 — Predice: ¿cuántas filas de kiosko.dim_store van a tener country=None en algún momento de este módulo? Basándote solo en el mapa de esta lección (sin haber leído todavía las lecciones 3 y 5), predice cuántas de las tres filas de dim_store van a pasar, aunque sea momentáneamente, por un estado con country=None.
Ver solución
Las tres. add_column("country", ...) en la lección 3 agrega la columna a nivel de esquema sin tocar ningún archivo de datos existente — así que, hasta que la lección 5 la puebla con un overwrite() real, las tres filas (S01, S02, S03) tienen country=None simultáneamente. Esto no es un error transitorio que haya que apurarse a corregir: es, con precisión, el punto central de la lección 3 — demostrar que agregar una columna es una operación de metadata, independiente de si esa columna ya tiene valores reales o no.
Ejercicio 3 — Nombra, de memoria, la diferencia entre lo que el módulo 3 ya demostró sobre overwrite() y lo que este módulo va a demostrar sobre update_schema(). En 2-3 frases, explica qué tipo de operación es cada una, y por qué ambas terminan garantizando lo mismo (atomicidad) aunque cambien cosas distintas.
Ver solución
table.overwrite(), que el módulo 3 ya usó para el cambio de P002, es una operación de datos: reemplaza las filas de una tabla, y puede producir más de un snapshot por dentro (delete + append), como reveló la lección 3 de ese módulo. table.update_schema(), el protagonista de este módulo, es una operación de metadata: cambia la estructura de columnas de la tabla —agregar, renombrar, borrar—, sin tocar ningún archivo de datos ni crear un snapshot nuevo. Ambas terminan garantizando atomicidad por la misma razón de fondo, que la lección 7 de este módulo desarrolla a fondo: cualquier cambio al estado de una tabla Iceberg —sea de datos o de esquema— se confirma con un único movimiento atómico del puntero del catálogo, nunca en dos pasos separados que un lector externo pudiera sorprender a medio camino.
Resumen y siguiente paso
En esta lección conociste el mapa completo del módulo 4: la promesa pendiente del módulo 1 —por qué una escritura Iceberg sí es atómica, a diferencia del overwrite-partition de data-engineering-foundations-guide— y el caso nuevo que la acompaña —kiosko.dim_store, con una columna country agregada sin reescribir ningún archivo, y una columna temp_notes agregada y borrada para probarlo—. Viste la analogía del formulario de censo, y la frontera explícita de lo que este módulo no resuelve.
Antes de avanzar deberías poder: explicar, en tus propias palabras, la diferencia entre una operación de datos y una operación de esquema en Iceberg; y recordar de memoria la cita exacta de data-engineering-foundations-guide sobre el riesgo del overwrite-partition, porque la lección 2 la retoma directamente.
La lección 2 es donde ocurre la comparación real: la misma cita, el mismo riesgo, y la evidencia ejecutada de por qué una escritura Iceberg no lo tiene.
Recursos
- DISEÑO de
data-engineering-foundations-guide— fuente del patrónoverwrite-partitiony la cita exacta sobre la ventana de riesgo entreDELETEeINSERT, que la lección 2 de este módulo retoma.src/guides/data-engineering-foundations-guide/DISENO.md. En español. - Apache Iceberg — documentación oficial, "Reliability" (Serializable Isolation, Optimistic Concurrency), la base formal de la atomicidad que este módulo demuestra con código. iceberg.apache.org/docs/latest/reliability. En inglés.
- Apache Iceberg — documentación oficial, "Evolution" (Schema evolution, Correctness), la base formal de la evolución de esquema segura que este módulo ejecuta. iceberg.apache.org/docs/latest/evolution. En inglés.
- PyIceberg — referencia de API,
table.update_schema()conadd_column/rename_column/delete_column/update_column. py.iceberg.apache.org/api. En inglés. - DISEÑO de esta guía — el mapa completo de los ocho módulos, incluida la frontera exacta de este módulo.
src/guides/lakehouse-and-iceberg-guide/DISENO.md. En español.