Módulo 5: Hidden Partitioning And Partition Evolution
Presentación del módulo: partición oculta y evolución de partición
Por qué existe este módulo
La lección 2 del módulo 1 de esta guía nombró, una por una, cuatro veces en que Kiosko ya chocó con el techo de Parquet como simple archivo. La cuarta de esas cuatro fue spark-and-distributed-processing-guide (módulo 7 de esa guía): fact_orders_at_scale.write.partitionBy("store_id").parquet(...), diez millones de filas escritas en carpetas —store_id=S01/, store_id=S02/, store_id=S03/— el patrón Hive que cualquier motor de big data reconoce. Ese patrón funciona: filtrar por store_id evita leer las carpetas de las otras dos tiendas. Pero el precio de ese rendimiento es explícito, y esta guía lo dejó pendiente a propósito desde el módulo 2, en su lección 3: "kiosko.fact_orders no está particionada [...] El módulo 5 de esta guía introduce el primer esquema de partición real". Ese momento llegó.
El precio del particionado de Spark es este: para aprovecharlo, tienes que saber que existe. Alguien que escriba WHERE store_id = 'S01' sin saber que la tabla está particionada por store_id obtiene el resultado correcto, pero no necesariamente el plan de ejecución más barato — y si la convención de carpetas cambia (agregan una segunda columna de partición, cambian el nombre), cualquier herramienta externa que dependía de esa estructura física deja de funcionar sin ningún aviso en el código de negocio. La estructura de carpetas es el contrato, y ese contrato vive fuera de cualquier catálogo, en la memoria de quien escribió el pipeline.
Este módulo resuelve exactamente ese problema, con el mismo mecanismo que ya viste resolver problemas parecidos en los módulos 1 a 4: mover la responsabilidad del ingeniero al motor. kiosko.fact_orders_at_scale, la tabla que vas a construir aquí, va a tener una partición real —tan real como la de Spark, con los mismos beneficios de poda de archivos— pero la consulta que la aprovecha nunca va a mencionar una carpeta. Y vas a ir un paso más allá de lo que Spark permite sin fricción: vas a evolucionar el esquema de partición después de que los datos ya existen, sin reescribir ni una fila de las diez millones que ya cargaste.
El caso que nos acompaña: kiosko.fact_orders_at_scale, particionada de verdad
Este módulo hereda, sin regenerar el argumento de por qué existe, el dataset sintético a escala que spark-and-distributed-processing-guide ya justificó en su módulo 4: Kiosko real —tres tiendas, cuarenta órdenes en una semana— nunca produce un volumen que justifique pensar en partición. generate_orders_at_scale(250_000) multiplica la semana real de Kiosko una vez por cada una de 250,000 "franquicias" simuladas, sin random, sin correr el calendario hacia adelante: diez millones de filas exactas, revenue total 26,537,500.00, con el mismo desglose proporcional de siempre por tienda: S01=9,575,000.00, S02=9,700,000.00, S03=7,262,500.00.
Lo que este módulo agrega, por primera vez en esta guía, es una tabla Iceberg con un PartitionSpec real en el momento de crearla: IdentityTransform sobre store_id, el mismo criterio que Spark ya usó para sus carpetas. Vas a cargar las diez millones de filas, vas a confirmar que una consulta filtrada por store_id == 'S01' —sin folder, sin path, sin nada físico en el código— sigue dando exactamente 9,575,000.00, y vas a evolucionar ese PartitionSpec para agregar una segunda dimensión de partición (order_day, truncando order_ts a su fecha) sin tocar un solo archivo de los que ya existen.
Una analogía: el cartero que no necesita que le digas el casillero
Imagina un edificio de apartamentos con un cartero nuevo. La forma vieja de trabajar —la de Spark— es como un edificio donde cada residente tiene que memorizar en qué piso y en qué ala vive, y decírselo al cartero cada vez: "soy del piso 3, ala norte". Si el cartero es nuevo, o si el edificio reorganiza los pisos, cualquiera que no conozca la estructura actual se queda sin correspondencia, o el cartero tiene que revisar edificio completo, piso por piso.
La partición oculta de Iceberg es un cartero distinto: uno que ya sabe, por su cuenta, en qué saco fue a parar la correspondencia de cada residente, sin que nadie tenga que decirle el número de piso. Le pides "la correspondencia de agosto de Ana" y él resuelve el resto — revisa su propio índice interno, va directo al saco correcto, y te entrega exactamente lo que pediste, sin que tú hayas tenido que saber nunca en qué saco estaba. Y si un día el cartero decide reorganizar sus sacos —agregar, por ejemplo, un sub-saco por día además del sub-saco por residente—, la correspondencia vieja sigue estando exactamente donde estaba: solo la correspondencia nueva empieza a aparecer también en el sub-saco nuevo. Nadie tiene que volver a etiquetar la correspondencia vieja. Ese reordenamiento, sin tocar lo que ya existe, es exactamente lo que este módulo llama evolución de partición.
Diagrama: de la carpeta visible al saco oculto
flowchart TB
subgraph SPARK["Spark (heredado, M7 de spark-and-distributed-processing-guide)"]
A["fact_orders_at_scale.write\n.partitionBy('store_id')\n.parquet(...)"]
A --> B["kiosko_orders_at_scale.parquet/\nstore_id=S01/ store_id=S02/ store_id=S03/"]
B --> C["Quien lee TIENE que saber\nque 'store_id' organiza las carpetas"]
end
subgraph ICEBERG["Este modulo: kiosko.fact_orders_at_scale"]
D["PartitionSpec inicial\nIdentityTransform(store_id)"]
D --> E["table.append(10M filas)\nel motor decide el layout"]
E --> F["table.scan(row_filter=\n\"store_id == 'S01'\")\nnunca menciona una carpeta"]
F --> G["Leccion 6: update_spec()\nagrega DayTransform(order_ts)"]
G --> H["Leccion 7: inspect.partitions()\narchivos viejos y nuevos conviven"]
end
C -.->|"mismo store_id,\nmismo resultado,\nresponsabilidad distinta"| D
El mapa de este módulo
Leccion Que resuelve
──────── ──────────────────────────────────────────────────────────────
L1 (esta) De la carpeta visible de Spark al saco oculto de Iceberg
L2 Como particiono Spark a Kiosko por carpetas -- el punto de partida,
con una demostracion real de layout Hive en disco
L3 La misma consulta, sin saber el layout -- el contraste central
L4 Los tres transforms de particion: IdentityTransform, BucketTransform,
DayTransform, y cuando usar cada uno
L5 kiosko.fact_orders_at_scale creada, cargada con las 10M filas reales,
consulta oculta por S01 = 9,575,000.00, con evidencia de poda
L6 update_spec().add_field(DayTransform) -- evolucion sin reescribir
L7 inspect.partitions() -- los dos esquemas de particion, conviviendo
L8 Proyecto: fact_orders_at_scale particionada, oculta, evolucionada,
de punta a punta
Las lecciones 2 y 3 construyen el contraste antes de tocar ninguna tabla a escala: la 2 muestra, en disco, lo que Spark ya construyó; la 3 muestra que la misma pregunta de negocio se responde sin ese conocimiento físico. La lección 4 da vocabulario preciso a los tres transforms que vas a necesitar. Las lecciones 5 a 7 son la ejecución real, a escala completa: crear, cargar, consultar, evolucionar, y confirmar que ambos esquemas de partición conviven sin conflicto. La lección 8 integra las siete piezas en un solo script.
La frontera: qué NO entra en este módulo
Por qué existen diez millones de filas sintéticas, y por qué es correcto seguir usándolas para medir algo real, ya se justificó a fondo en spark-and-distributed-processing-guide (módulo 4 de esa guía) — este módulo hereda esa justificación, no la repite ni la regenera conceptualmente; sí reconstruye los datos, con el mismo generador determinista, porque necesita cargarlos de verdad dentro de una tabla Iceberg. Cómputo distribuido a fondo —shuffle, particiones en memoria, Catalyst— sigue siendo terreno de spark-and-distributed-processing-guide; este módulo nunca ejecuta Spark, solo PyIceberg puro. MERGE INTO y upserts nativos, la cuarta garantía operativa de esta guía, llegan recién en el módulo 6, con Spark como cliente SQL — no antes. Y catálogos de partición gestionados en la nube (particiones sobre AWS Glue Catalog o S3 Tables con miles de archivos reales) se nombran, sin implementarse, en el módulo 7.
Errores comunes
Pensar que "partición oculta" significa que Iceberg no particiona nada, o que "todo es más lento sin carpetas". Qué pasa: alguien, al escuchar "oculta", asume que Iceberg renuncia a la organización física de los datos, y que toda la poda de archivos se hace "a mano", revisando cada Parquet uno por uno. Por qué pasa: la palabra "oculta" suena, a primera escucha, como sinónimo de "ausente". Cómo detectarlo: si esperas que table.inspect.files() sobre kiosko.fact_orders_at_scale (lección 5) muestre un solo archivo gigante con todas las filas mezcladas, revisa esta lección otra vez. Cómo corregirlo: Iceberg sí organiza los datos físicamente —vas a ver, en la lección 2, que incluso escribe carpetas con el mismo patrón store_id=S01/ que Spark—; lo que cambia es quién necesita saber esa estructura para aprovecharla. Con Spark, el lector. Con Iceberg, solo el propio catálogo.
Confundir "particionar por store_id" con "ordenar por store_id". Qué pasa: alguien espera que, dentro de un mismo archivo Parquet, las filas de S01 aparezcan agrupadas y ordenadas antes que las de S02, como si particionar fuera lo mismo que un ORDER BY. Por qué pasa: ambos conceptos organizan datos según el valor de una columna, así que es fácil mezclarlos si nunca se distinguieron explícitamente. Cómo detectarlo: si tu mental model de partición incluye la palabra "orden", revisa la lección 4 de este módulo — un PartitionSpec decide en qué archivo cae cada fila, nunca en qué posición dentro de ese archivo. Cómo corregirlo: partición es una decisión de layout de archivos (cuántos archivos, y qué filas van en cada uno); el orden interno de las filas dentro de un archivo es un tema aparte (sort order), que esta guía no construye — Iceberg lo soporta, pero está fuera del alcance declarado de este módulo.
Ejercicios
Ejercicio 1 — Nombra, de memoria, la cuarta vez que Parquet solo no alcanzó. Sin mirar atrás, recuerda: ¿cuál guía anterior del ecosistema chocó con el problema que este módulo resuelve, y con qué código exacto?
Ver solución
spark-and-distributed-processing-guide, en su módulo 7: fact_orders_at_scale.write.partitionBy("store_id").parquet(...) — diez millones de filas escritas en carpetas Hive (store_id=S01/, store_id=S02/, store_id=S03/). El problema no es que ese particionado no funcione —funciona, y de hecho la propia guía de Spark muestra PushedFilters/poda de partición en su .explain()—; el problema es que aprovecharlo exige que quien consulta sepa, de antemano, que esa estructura de carpetas existe y qué columna la organiza.
Ejercicio 2 — Traza la analogía tú mismo. Con tus propias palabras, y usando la analogía del cartero de esta lección, explica qué representa el PartitionSpec de una tabla Iceberg.
Ver solución
El PartitionSpec es el índice interno que el cartero mantiene por su cuenta: la regla que dice, para cada fila nueva, en qué saco debe ir, sin que el remitente (quien escribe) ni el destinatario (quien consulta) tengan que saber esa regla de memoria. Cuando alguien escribe table.append(), el motor consulta ese índice y decide el layout; cuando alguien consulta con row_filter="store_id == 'S01'", el motor vuelve a consultar ese mismo índice para saber qué sacos puede ignorar por completo. El "saco" físico sigue siendo un archivo Parquet en disco —la lección 2 te lo muestra en carne propia, con el layout de Spark—; lo que cambia es que, del lado de Iceberg, ese índice vive en la tabla misma, no en la cabeza de quien la usa.
Ejercicio 3 — Predicción. Antes de leer la lección 6: si kiosko.fact_orders_at_scale ya tiene diez millones de filas cargadas bajo un PartitionSpec que solo usa store_id, y luego agregas un segundo campo de partición (order_day, truncando order_ts), ¿qué esperas que pase con las diez millones de filas que ya existen? ¿Van a "recibir" el nuevo campo de partición, o van a quedar como estaban?
Ver solución
No hay una única forma de expresarlo, pero la respuesta correcta —que la lección 6 confirma con evidencia ejecutada— es que las diez millones de filas existentes quedan exactamente como estaban: sus archivos de datos no se tocan, y siguen "viviendo" bajo el PartitionSpec original (solo store_id). Solo las filas que se escriban después de la evolución van a organizarse según el nuevo esquema, con ambos campos. Es la misma lógica que ya viste en el módulo 4 con la evolución de esquema: agregar algo nuevo nunca obliga a "poner al día" lo que ya existía.
Resumen y siguiente paso
En esta lección viste el punto exacto donde te dejó el módulo 4 —una promesa pendiente desde el módulo 1, sobre el particionado por carpetas de Spark— y el mapa completo de cómo este módulo la resuelve: partición oculta primero, evolución de partición después, ambas sobre kiosko.fact_orders_at_scale, la tabla de diez millones de filas que esta guía hereda, sin regenerar, de spark-and-distributed-processing-guide.
Antes de avanzar deberías poder: explicar, con tus propias palabras, qué costo tiene el particionado por carpetas de Spark que la partición oculta de Iceberg elimina; y nombrar los dos experimentos centrales de este módulo (consulta oculta, evolución sin reescritura) que vas a ejecutar de verdad en las lecciones 5 a 7.
La lección 2 empieza donde termina el módulo 4: mostrándote, en disco, exactamente lo que Spark ya construyó — el punto de partida real de todo el contraste de este módulo.
Recursos
- Apache Iceberg — documentación oficial, "Partitioning" (partición oculta, transforms, evolución de partición sin reescritura de datos) — la base formal de todo este módulo. iceberg.apache.org/docs/latest/partitioning. En inglés.
- PyIceberg — referencia de API,
PartitionSpec/PartitionField, los transforms (IdentityTransform/BucketTransform/DayTransform),table.update_spec(),table.inspect.partitions(). py.iceberg.apache.org/api. En inglés. - DISEÑO de
spark-and-distributed-processing-guide— fuente defact_orders_at_scale.write.partitionBy("store_id").parquet(...),generate_orders_at_scale()y los números exactos del dataset a escala.src/guides/spark-and-distributed-processing-guide/DISENO.md. En español. - DISEÑO de esta guía — el mapa completo de los ocho módulos, incluida la sección del módulo 5.
src/guides/lakehouse-and-iceberg-guide/DISENO.md. En español.