Módulo 5: Hidden Partitioning And Partition Evolution
Cómo Spark particionó a Kiosko por carpetas
Descripción
Antes de contrastar nada, esta lección construye el punto de partida real: el layout de carpetas que produce partitionBy("store_id"), el mismo gesto que spark-and-distributed-processing-guide (módulo 7 de esa guía) ya ejecutó sobre kiosko_orders_at_scale.parquet, las diez millones de filas de Kiosko a escala. Esta guía no vuelve a instalar Spark para reproducirlo —esta guía no toca Spark hasta el módulo 6, declarado así de explícito—, así que vas a reconstruir el mismo tipo de layout con pyarrow, la biblioteca que ya conoces desde el módulo 1, y vas a verlo en disco con tus propios ojos.
Conexión con el módulo. La lección 1 prometió un contraste; esta lección construye la mitad "antes" de ese contraste, con evidencia real en disco, no con una descripción abstracta. La lección 3 va a tomar exactamente esta misma pregunta de negocio —"dame los datos de S01"— y respondérsela a Iceberg sin ninguno de los conocimientos físicos que esta lección va a exigir.
Una analogía: el archivero con gavetas rotuladas a mano
Piensa en un archivero de oficina, de esos con gavetas físicas. Alguien decidió, en algún momento, organizar los documentos por cliente: una gaveta para cada cliente, con una etiqueta escrita a mano pegada al frente. El sistema funciona bien — mientras la persona que busca un documento sepa que la organización es "por cliente" y sepa leer la etiqueta exacta. Si llega alguien nuevo a la oficina, sin que nadie le explique el sistema, puede abrir gaveta por gaveta hasta encontrar lo que busca —funciona, pero es lento y depende de adivinar—, o puede preguntarle a alguien que ya conoce el sistema. Lo que el archivero no hace es decirte, por sí solo, "el documento que buscas está en la tercera gaveta" — esa inteligencia vive en la cabeza de quien organizó las gavetas, no en el mueble.
Eso es, con precisión, lo que construye esta lección: un archivero real, en disco, con una gaveta por tienda, y vas a comprobar en carne propia qué pasa cuando alguien intenta usarlo sin conocer la convención de las etiquetas.
Ejemplo trabajado: reconstruyendo el layout de Spark con pyarrow
Paso 1 — Genera una porción de Kiosko a escala, y escríbela particionada por store_id
generate_orders_at_scale(), el generador determinista de spark-and-distributed-processing-guide (módulo 4 de esa guía), reconstruido aquí de forma idéntica para que esta lección sea autocontenida — sin random, sin datetime.now(), la misma semana real de Kiosko multiplicada por franquicia:
# kiosko_scale.py -- identico al de spark-and-distributed-processing-guide M4L4
from typing import Iterator, Dict, Any
KIOSKO_WEEK = [
{"order_id": "ORD-1001", "store_id": "S01", "product_id": "P001", "quantity": 3, "unit_price": 0.55, "order_ts": "2026-08-03T08:14:00"},
# ... las 40 filas completas de la semana real de Kiosko (3 al 9 de agosto de 2026)
]
def generate_orders_at_scale(num_franchises: int) -> Iterator[Dict[str, Any]]:
"""Repite KIOSKO_WEEK una vez por franquicia sintetica. Sin random, sin
datetime.now(): franchise_id recorre range(num_franchises) en orden fijo."""
for franchise_id in range(num_franchises):
for row in KIOSKO_WEEK:
yield {
"order_id": f"F{franchise_id:06d}-{row['order_id']}",
"franchise_id": franchise_id,
"store_id": row["store_id"],
"product_id": row["product_id"],
"quantity": row["quantity"],
"unit_price": row["unit_price"],
"order_ts": row["order_ts"],
}
# l2_hive_style_write.py
import os
from datetime import datetime
import pyarrow as pa
import pyarrow.dataset as ds
from kiosko_scale import generate_orders_at_scale
OUT_DIR = "kiosko_orders_at_scale_hive"
NUM_FRANCHISES = 25 # demo pequena -- 1,000 filas, suficiente para ver la carpeta
rows = list(generate_orders_at_scale(NUM_FRANCHISES))
for r in rows:
r["order_ts"] = datetime.fromisoformat(r["order_ts"])
pa_schema = pa.schema([
pa.field("order_id", pa.string(), nullable=False),
pa.field("franchise_id", pa.int32(), nullable=False),
pa.field("store_id", pa.string(), nullable=False),
pa.field("product_id", pa.string(), nullable=False),
pa.field("quantity", pa.int32(), nullable=False),
pa.field("unit_price", pa.float64(), nullable=False),
pa.field("order_ts", pa.timestamp("us"), nullable=False),
])
pa_table = pa.Table.from_pylist(rows, schema=pa_schema)
# el mismo gesto que fact_orders_at_scale.write.partitionBy("store_id").parquet(...)
# de spark-and-distributed-processing-guide: quien ESCRIBE decide, de forma
# explicita, que "store_id" se convierta en estructura de carpetas
ds.write_dataset(
pa_table, OUT_DIR, format="parquet",
partitioning=ds.partitioning(pa.schema([("store_id", pa.string())]), flavor="hive"),
)
print(f"filas escritas: {pa_table.num_rows}")
Qué esperar (verificado corriendo el script real, con una porción representativa de 25 franquicias — 1,000 filas — en vez de las 250,000 completas, para que este archivero quede lo bastante chico como para explorarlo a mano; la tabla real de la lección 5 sí carga las 10,000,000 completas):
filas escritas: 1000
Paso 2 — Mira el archivero en disco
for root, dirs, files in os.walk(OUT_DIR):
depth = root.replace(OUT_DIR, "").count(os.sep)
indent = " " * depth
print(f"{indent}{os.path.basename(root) or OUT_DIR}/")
for f in sorted(files):
print(f"{indent} {f}")
Qué esperar:
kiosko_orders_at_scale_hive/
store_id=S01/
part-0.parquet
store_id=S03/
part-0.parquet
store_id=S02/
part-0.parquet
Ahí está el archivero: tres gavetas, cada una con la etiqueta escrita a mano en el propio nombre de la carpeta —store_id=S01, store_id=S02, store_id=S03—. Esto es exactamente lo que kiosko_orders_at_scale.parquet de spark-and-distributed-processing-guide ya tiene en disco, a escala completa: el mismo patrón columna=valor/, la convención que el ecosistema Hadoop lleva usando más de una década.
Paso 3 — Comprueba, en carne propia, qué pasa si alguien no conoce la convención
import pyarrow.dataset as ds
print("--- Lectura 'ingenua': solo con pyarrow, sin decirle nada sobre particion ---")
plain = ds.dataset(OUT_DIR, format="parquet") # sin partitioning=... -> no reconoce store_id
plain_table = plain.to_table()
print("columnas vistas sin declarar el esquema de particion:", plain_table.schema.names)
print("\n--- Lectura 'consciente del layout': hay que DECLARAR el esquema hive ---")
aware = ds.dataset(OUT_DIR, format="parquet", partitioning="hive")
s01_only = aware.to_table(filter=(ds.field("store_id") == "S01"))
print("filas de S01 (declarando partitioning='hive'):", s01_only.num_rows)
Qué esperar:
--- Lectura 'ingenua': solo con pyarrow, sin decirle nada sobre particion ---
columnas vistas sin declarar el esquema de particion: ['order_id', 'franchise_id', 'product_id', 'quantity', 'unit_price', 'order_ts']
--- Lectura 'consciente del layout': hay que DECLARAR el esquema hive ---
filas de S01 (declarando partitioning='hive'): 400
Fíjate en algo que no es un detalle menor: en la lectura "ingenua", store_id directamente no aparece entre las columnas. No es que la columna esté vacía o incompleta — desapareció, porque su valor real vive codificado en el nombre de la carpeta, no dentro de ningún archivo Parquet. Un lector que no sepa, de antemano, que debe declarar partitioning="hive" ni siquiera puede filtrar por store_id — la columna, para ese lector, simplemente no existe. Para recuperarla, alguien tiene que decirle explícitamente al lector cuál es la convención (partitioning="hive"), y ese "decirle explícitamente" es, con precisión, el conocimiento físico que la lección 1 de este módulo prometió eliminar.
Diagrama: dónde vive el conocimiento
flowchart LR
W["Escritor:\n.write.partitionBy('store_id')\ndecide la convencion"] --> D["Disco:\nstore_id=S01/\nstore_id=S02/\nstore_id=S03/"]
D --> R1["Lector SIN saber la convencion:\nstore_id ni siquiera aparece"]
D --> R2["Lector QUE declara\npartitioning='hive':\nrecupera store_id, puede filtrar"]
R2 -.->|"el conocimiento vive\nEN CADA LECTOR,\nno en la tabla"| W
Profundización: esto no es un defecto de pyarrow
Vale la pena ser preciso sobre algo que el ejemplo de esta lección podría hacer parecer un problema de la biblioteca: pyarrow.dataset sí sabe leer layouts Hive — el paso 3 lo demuestra, con partitioning="hive". El punto no es que exista una forma de leerlo bien; el punto es que esa forma exige una declaración explícita, hecha por cada lector, cada vez. Si mañana alguien reorganiza el layout —agrega una segunda columna de partición, cambia store_id por region—, cada uno de esos lectores tiene que actualizar su propia declaración, en su propio código, por su cuenta. No hay un lugar único donde esa convención viva, versionada, consultable — vive repetida, tantas veces como lectores existan. Esta es exactamente la responsabilidad que la lección 3 va a mostrar transferida a un solo lugar: la tabla misma.
Errores comunes
Asumir que el nombre de la carpeta (store_id=S01) es solo cosmético, y que el valor real sigue estando en alguna columna del Parquet. Qué pasa: alguien abre uno de los archivos part-0.parquet directamente con pq.read_table(), sin pasar por pyarrow.dataset, y se sorprende al no encontrar store_id entre las columnas. Por qué pasa: es intuitivo pensar que una columna usada para particionar sigue existiendo, "por si acaso", dentro de cada archivo — pero el flavor hive de pyarrow.dataset la remueve del archivo físico, precisamente porque ya está codificada en la ruta. Cómo detectarlo: si pq.read_table("kiosko_orders_at_scale_hive/store_id=S01/part-0.parquet").schema.names no incluye store_id, no es un error — es el comportamiento esperado de un dataset particionado por Hive. Cómo corregirlo: para recuperar store_id como columna, siempre lee a través de pyarrow.dataset con partitioning="hive" (o el equivalente en la herramienta que uses) — nunca abriendo los archivos individuales sueltos.
Pensar que particionar por carpetas es "gratis" en términos de mantenimiento. Qué pasa: alguien asume que, una vez escrito el layout Hive, no hace falta pensar más en él — cualquier consulta futura "simplemente funciona". Por qué pasa: mientras nadie cambie el esquema de partición, el sistema efectivamente parece invisible y sin costo. Cómo detectarlo: pregúntate qué pasaría si, dentro de seis meses, Kiosko decide que también necesita particionar por mes además de por tienda — ¿cuántos lectores existentes tendrían que actualizar su código? Cómo corregirlo: cualquier cambio al esquema de partición de un layout Hive exige, en el caso general, reescribir todos los datos con la nueva estructura de carpetas, y actualizar cada lector que dependía de la vieja. La lección 6 de este módulo muestra el contraste exacto: Iceberg evoluciona su PartitionSpec sin ninguna de esas dos consecuencias.
Ejercicios
Ejercicio 1 — Reproduce el archivero tú mismo, y ábrelo con y sin partitioning="hive". Corre los tres pasos de esta lección en tu propia máquina. Confirma que store_id desaparece de la lectura ingenua, y que reaparece —con 400 filas para S01— al declarar partitioning="hive".
Ver solución
Si seguiste los tres pasos, tu salida debería coincidir exactamente con la de esta lección: filas escritas: 1000, un árbol de tres carpetas store_id=S0N/, columnas sin store_id en la lectura ingenua, y 400 filas de S01 en la lectura consciente del layout (25 franquicias × 16 líneas de S01 por franquicia = 400, el mismo patrón proporcional que ya conoces desde el módulo 1).
Ejercicio 2 — Calcula cuántos archivos existirían con 250,000 franquicias, si el layout no cambiara. Con la porción de 25 franquicias de esta lección, cada gaveta (store_id=SNN/) tiene exactamente un archivo (part-0.parquet). Si escribieras las 250,000 franquicias completas con el mismo ds.write_dataset() de esta lección, en un solo lote, ¿cuántos archivos por gaveta esperarías, como mínimo?
Ver solución
Como mínimo, uno por gaveta —tres en total—, exactamente como en esta lección: ds.write_dataset(), al recibir una única tabla de pyarrow en memoria (sin partición explícita de escritura en múltiples lotes), escribe un archivo por cada valor distinto de la columna de partición dentro de esa llamada. El número real puede ser mayor en un sistema distribuido como Spark, donde cada partición en memoria (tema del módulo 4 de spark-and-distributed-processing-guide) que contenga filas de un store_id dado puede generar su propio archivo — por eso una tabla particionada por store_id a escala real casi nunca tiene exactamente tres archivos, sino varios por tienda. La lección 5 de este módulo, sobre la tabla Iceberg completa, va a mostrar el número real que produce una sola llamada a table.append().
Ejercicio 3 — Explica, en 2-3 frases, por qué la lectura "ingenua" del paso 3 no lanza un error. En vez de fallar con un mensaje claro tipo "falta declarar la partición", pyarrow.dataset simplemente omite store_id en silencio. ¿Por qué crees que ese es un comportamiento más peligroso que un error explícito?
Ver solución
Un error explícito te detiene de inmediato y te obliga a corregir el problema antes de seguir. La omisión silenciosa de store_id es más peligrosa precisamente porque no te detiene: el script sigue corriendo, produce un resultado —solo que ese resultado ya no tiene la columna que quizás necesitabas para filtrar o agrupar más adelante—, y el error solo aparece más tarde, en forma de un KeyError confuso o de un resultado de negocio incompleto, lejos del lugar donde realmente se originó el problema. Esta es la misma clase de riesgo silencioso que ya viste en el módulo 1 sobre por qué un archivo Parquet suelto no puede avisarte si algo salió mal en su propia escritura.
Resumen y siguiente paso
En esta lección construiste, con pyarrow, el mismo tipo de layout que spark-and-distributed-processing-guide ya dejó en disco a escala completa: un archivero con una gaveta física por tienda, store_id=S01/, store_id=S02/, store_id=S03/. Confirmaste, con evidencia directa —no con una afirmación—, que un lector que no conoce esa convención pierde la columna store_id por completo, y que recuperarla exige declarar explícitamente el layout en cada punto de lectura.
Antes de avanzar deberías poder: describir, con tus propias palabras, qué información vive en el nombre de una carpeta Hive y qué información vive dentro del archivo Parquet; y explicar por qué ese conocimiento repartido entre lectores es, con precisión, el costo que este módulo va a eliminar.
La lección 3 hace el contraste real: la misma pregunta —"dame los datos de S01"— respondida contra una tabla Iceberg, sin que el código mencione una sola carpeta.
Recursos
- Apache Arrow — documentación oficial,
pyarrow.dataset.partitioning()y el flavorhive(la API exacta que esta lección usa para escribir y leer el layout de carpetas). arrow.apache.org/docs/python/dataset.html. En inglés. - Apache Iceberg — documentación oficial, "Partitioning", sección "Partitioning in Hive" (la descripción formal del patrón que esta lección reconstruye). iceberg.apache.org/docs/latest/partitioning. En inglés.
- DISEÑO de
spark-and-distributed-processing-guide— fuente defact_orders_at_scale.write.partitionBy("store_id").parquet(...)y dekiosko_orders_at_scale.parquet, el layout real a escala completa que esta lección reconstruye a menor escala.src/guides/spark-and-distributed-processing-guide/DISENO.md. En español. - DISEÑO de esta guía — la sección del módulo 5, con el contraste explícito entre carpeta visible y partición oculta.
src/guides/lakehouse-and-iceberg-guide/DISENO.md. En español.