Skip to content

~/bda/m01 $ cat 05-formato-de-datos2.md

  • Big Data Aplicado
  • M01
  • lección 05/09

Formatos de datos

Queremos guardar los mismos datos en distintos formatos y comprobar cómo cambian su tamaño y su tiempo de escritura. Primero veremos ejemplos pequeños y después realizarás una práctica de hasta dos horas, inspirada en el ejercicio del curso anterior.

Los ejemplos utilizan tres ventas para presentar las instrucciones de cada formato. En la práctica aplicarás esas operaciones al archivo original de compras y comprobarás cómo se comporta el programa con todos sus registros.

En un formato por filas, cada registro guarda juntos sus campos. CSV, JSONL y Avro siguen esa organización. Resulta útil cuando queremos intercambiar o procesar registros completos.

En un formato por columnas, los valores de un campo se agrupan dentro de unidades del archivo. Parquet y ORC siguen esta organización. Un lector puede solicitar determinadas columnas para una consulta analítica.

En un archivo columnar también existen filas lógicas. Lo que cambia es cómo se almacenan sus valores. Parquet utiliza grupos de filas y ORC utiliza stripes; dentro de esas unidades, los datos se organizan por columnas.

CSV recorre registros completos; Parquet permite solicitar las columnas necesarias.

La figura utiliza unas compras de fruta. A la izquierda se recorren las líneas del CSV; a la derecha se solicitan fecha y precio del Parquet. El dibujo simplifica los grupos internos de los archivos.

Preparar el entorno y una muestra pequeña

Section titled “Preparar el entorno y una muestra pequeña”

En el anfitrión, entra en BigDataAplicadoLab_2627/modulo01/ y abre el contenedor de formatos.

Terminal window
make formatos-shell

Sin Make, utiliza esta orden desde la misma carpeta.

Terminal window
docker compose -f formatos/compose.yaml run --rm --build formatos bash

La imagen bda-m01-formatos:3.12-r3 contiene Python 3.12.8, pandas 2.2.3, PyArrow 18.1.0, fastavro 1.10.0, nano, vi, curl, wget y gzip. Dentro del contenedor trabajas como alumno. El montaje /lab corresponde a modulo01/ del anfitrión y conserva los archivos al salir.

Dentro del contenedor, crea una carpeta para esta propuesta.

Terminal window
mkdir -p /lab/trabajo/formatos2
cd /lab/trabajo/formatos2

Crea el archivo siguiente con nano ventas_ejemplo.csv o vi ventas_ejemplo.csv.

ventas_ejemplo.csv

tienda,categoria,importe
San Jose,Ropa,214.05
Fort Worth,Ropa,153.57
Fort Worth,Juguetes,213.88

Cada script de ejemplo se guarda y ejecuta en esta carpeta. Usaremos la misma fuente para observar cada formato.

pandas es una biblioteca de Python para trabajar con datos organizados en tablas. Permite leer archivos, seleccionar columnas, filtrar filas, realizar cálculos y guardar los resultados en distintos formatos. Ya está instalada en el contenedor.

Su estructura principal es el DataFrame, una tabla con filas y columnas con nombre. En nuestro ejemplo, cada fila representa una venta y las columnas son tienda, categoria e importe.

Crea este script para abrir el CSV que acabas de preparar e inspeccionar su tabla.

introduccion_pandas.py
import pandas as pd
# Leer archivo CSV y crear un DataFrame
ventas = pd.read_csv('ventas_ejemplo.csv')
print(ventas)
print('Filas y columnas:', ventas.shape)
print('Tipos de datos:')
print(ventas.dtypes)
# Seleccionar una columna
print(ventas['importe'])
# Seleccionar varias columnas y mantener una tabla
print(ventas[['tienda', 'importe']])
Terminal window
python introduccion_pandas.py

import pandas as pd permite utilizar la biblioteca con el nombre corto pd. pd.read_csv(...) lee el archivo y devuelve un DataFrame que guardamos en la variable ventas.

ventas.shape devuelve (3, 3), tres filas y tres columnas. ventas.dtypes muestra el tipo de cada columna. Una selección como ventas['importe'] devuelve una columna, denominada Series en pandas. Con una lista de nombres entre los corchetes, ventas[['tienda', 'importe']] devuelve otro DataFrame con esas dos columnas.

En los ejemplos siguientes utilizaremos métodos como ventas.to_parquet(...) para guardar la tabla. El método se llama sobre el DataFrame que contiene los datos.

CSV organiza los registros en líneas y separa sus campos mediante un delimitador. Su cabecera identifica las columnas; el lector decide sus tipos.

JSONL guarda un objeto JSON por línea. Es práctico para registros de aplicaciones, eventos y exportaciones de documentos porque permite añadir y procesar objetos uno a uno. Un documento JSON convencional puede contener una lista completa entre corchetes; JSONL utiliza el salto de línea como separación entre documentos independientes.

JSONL está orientado a registros. Conserva la sintaxis JSON de cada objeto, pero establece que el salto de línea separa un registro del siguiente. El archivo completo se lee como una secuencia de documentos JSON, uno por línea.

Esto resulta práctico para registros de aplicaciones, exportaciones de muchos documentos y eventos procesados en secuencia.

ejemplo_jsonl.py
import pandas as pd
# Leer archivo CSV de la muestra pequeña
ventas = pd.read_csv('ventas_ejemplo.csv')
# Crear archivo con un objeto JSON por línea
ventas.to_json('ventas_ejemplo.jsonl', orient='records', lines=True)
# Leer archivo JSONL
recuperadas = pd.read_json('ventas_ejemplo.jsonl', lines=True)
print(recuperadas)
Terminal window
python ejemplo_jsonl.py

pd.read_csv carga los datos en un DataFrame, una tabla con filas y columnas. to_json escribe esa tabla; orient='records' representa cada fila como un objeto y lines=True separa los objetos mediante saltos de línea. Aquí pandas carga la muestra completa, aunque el formato JSONL también permite lectura incremental con otras interfaces.

También orientado a filas, Avro guarda registros en binario. En un archivo contenedor .avro, el esquema queda incluido en la cabecera y declara los nombres y tipos de los campos. string representa texto y double representa un número de coma flotante de doble precisión.

ejemplo_avro.py
import pandas as pd
from fastavro import writer, reader
ventas = pd.read_csv('ventas_ejemplo.csv')
esquema = {
'type': 'record',
'name': 'Venta',
'fields': [
{'name': 'tienda', 'type': 'string'},
{'name': 'categoria', 'type': 'string'},
{'name': 'importe', 'type': 'double'},
],
}
# Crear archivo binario a partir de los registros de la tabla
with open('ventas_ejemplo.avro', 'wb') as salida:
writer(salida, esquema, ventas.to_dict('records'), codec='null')
# Leer archivo registro a registro
with open('ventas_ejemplo.avro', 'rb') as entrada:
for venta in reader(entrada):
print(venta)
Terminal window
python ejemplo_avro.py

to_dict('records') convierte el DataFrame en una lista de diccionarios, uno por fila. Esta conversión utiliza memoria y por eso la aplicamos a una muestra limitada. Los modos wb y rb indican escritura y lectura binarias. codec='null' escribe sin compresión.

Avro también permite que un lector utilice un esquema compatible con el de escritura. Por ejemplo, un campo nuevo puede recibir un valor por defecto al leer datos antiguos. Esa capacidad requiere diseñar versiones compatibles del esquema.

Este fragmento se puede añadir a ejemplo_avro.py después de definir esquema. Construye la nueva versión del esquema; su uso como esquema de lectura requiere pasarlo a reader(..., reader_schema=esquema_nuevo).

# Fragmento de evolución del esquema de ejemplo_avro.py
esquema_nuevo = {
**esquema,
'fields': esquema['fields'] + [
{'name': 'canal', 'type': 'string', 'default': 'desconocido'},
],
}

Parquet organiza los datos por columnas dentro de grupos de filas. Es habitual en históricos analíticos, donde una consulta necesita unas pocas columnas de muchas filas.

Por ejemplo, si solo queremos sumar importe por categoria, el lector puede solicitar esas dos columnas. Esta lectura selectiva evita recuperar producto, unidades y precio.

ejemplo_parquet.py
import pandas as pd
ventas = pd.read_csv('ventas_ejemplo.csv')
# Crear archivo Parquet sin compresión
ventas.to_parquet('ventas_ejemplo.parquet', index=False, compression=None)
# Leer archivo completo
recuperadas = pd.read_parquet('ventas_ejemplo.parquet')
print(recuperadas)
# Leer únicamente las columnas solicitadas
reducidas = pd.read_parquet('ventas_ejemplo.parquet', columns=['tienda', 'importe'])
print(reducidas)
Terminal window
python ejemplo_parquet.py

index=False evita guardar el índice de filas que pandas crea al cargar la tabla. compression=None desactiva la compresión. columns selecciona las columnas que queremos recuperar.

ORC, Optimized Row Columnar, es un formato binario orientado a columnas utilizado en el ecosistema de Hive. Para organizar un archivo grande, divide sus registros en conjuntos llamados stripes.

Imagina seis ventas. Un primer stripe puede contener las ventas 1 a 3 y un segundo las ventas 4 a 6. Dentro de cada stripe se guardan por separado los valores de cada columna, por ejemplo los tres productos juntos y los tres importes juntos. Cada stripe incluye también índices y un pie con información que ayuda a interpretar sus datos. Al final del archivo hay metadatos para localizar los stripes y conocer el esquema.

Cada stripe de ORC almacena columnas separadas; una consulta puede solicitar únicamente importe.

El dibujo muestra dos stripes uno junto al otro. Dentro de cada uno, los valores de producto, unidades e importe aparecen en columnas verticales separadas. La consulta solicita importe, resaltado en verde, en ambos stripes; puede omitir los datos de las columnas producto y unidades. Los índices, los pies y los metadatos permiten localizar e interpretar esas columnas. Los límites de tres ventas del dibujo son ilustrativos; el tamaño real de los stripes lo determina el escritor y su configuración.

ejemplo_orc.py
import pandas as pd
ventas = pd.read_csv('ventas_ejemplo.csv')
# Crear archivo ORC sin compresión
ventas.to_orc('ventas_ejemplo.orc', index=False,
engine_kwargs={'compression': 'uncompressed'})
# Leer archivo ORC
recuperadas = pd.read_orc('ventas_ejemplo.orc')
print(recuperadas)
Terminal window
python ejemplo_orc.py

engine_kwargs pasa opciones al escritor de PyArrow que utiliza pandas. Para probar Snappy, sustituye 'uncompressed' por 'snappy' y utiliza otro nombre de archivo.

Un formato determina cómo se organizan los datos. Un códec, como Snappy, comprime su representación. Para distinguir sus efectos, compararemos ORC sin compresión con ORC comprimido, y Avro con Parquet usando las mismas columnas y sin compresión.

Podemos medir una escritura con este patrón. Crea y ejecuta el ejemplo después de los anteriores.

medir_escritura.py
from pathlib import Path
from time import perf_counter
import pandas as pd
# Leer la fuente antes de iniciar el cronómetro
ventas = pd.read_csv('ventas_ejemplo.csv')
# Crear archivo y medir solo la escritura desde el DataFrame
inicio = perf_counter()
ventas.to_parquet('medida.parquet', index=False, compression=None)
segundos = perf_counter() - inicio
print('Tiempo de escritura:', segundos, 's')
print('Tamaño:', Path('medida.parquet').stat().st_size / 1024**2, 'MiB')
Terminal window
python medir_escritura.py

perf_counter mide duraciones. La resta da el tiempo transcurrido durante la escritura. stat().st_size devuelve bytes y dividimos entre 1024**2 para obtener MiB. Mantén fuera del cronómetro la descarga, la carga inicial y la preparación de la tabla que vayas a escribir.

Los metadatos pueden hacer que un formato binario ocupe más que el texto con tres filas. Con datos mayores cambian las proporciones. Los tamaños y tiempos observados describen ese archivo, esas opciones y ese equipo.

Particionar para leer solo el período necesario

Section titled “Particionar para leer solo el período necesario”

Supongamos que guardamos ventas de muchos días y por lo general, las consulta suelen ser de un determinado día. Existe la posibilidad de agrupar los archivos por fecha en vez de colocar todo el histórico en una única carpeta.

Esta organización puede utilizarse con CSV, JSONL, Avro, Parquet u ORC. El particionado organiza un conjunto de archivos en carpetas según un criterio, independientemente del formato de cada archivo. En el dibujo usamos Parquet, pero las carpetas también podrían contener archivos .csv, .jsonl, .avro o .orc.

historico/
├── fecha=2026-04-19/
│ └── part-000.parquet
├── fecha=2026-04-20/
│ └── part-000.parquet
└── fecha=2026-04-21/
└── part-000.parquet

Cada carpeta contiene las ventas de su fecha. Esta organización se llama particionado. Un motor configurado para reconocer esas particiones puede resolver el filtro de fecha leyendo la carpeta fecha=2026-04-20 y omitiendo las otras dos.

El siguiente fragmento ilustra la consulta de un conjunto ya particionado, como el del dibujo. Requiere import pyarrow.dataset as ds y que destino identifique su carpeta. El conjunto del ejemplo debe contener fecha, producto e importe.

# Leer por lotes únicamente la fecha solicitada
fecha_elegida = '2026-04-20'
historico = ds.dataset(destino, format='parquet', partitioning='hive')
def leer_seleccion():
consulta = historico.scanner(
filter=ds.field('fecha') == fecha_elegida,
columns=['fecha', 'producto', 'importe'],
batch_size=50000, use_threads=False,
)
for lote in consulta.to_batches():
for venta in lote.to_pylist():
assert venta['fecha'] == fecha_elegida
yield venta

M01-FMT-01 — Convertir y comparar el archivo de ventas Aules

Section titled “M01-FMT-01 — Convertir y comparar el archivo de ventas Aules”

Queremos comprobar qué cambia cuando pasamos de tres registros a millones de ventas. Vamos a medir cuánto ocupa cada formato, cuánto tarda en generarse y cuánto tarda en leerse. También compararemos leer todos los campos con leer únicamente la tienda y el importe.

Para cada archivo anota su tamaño y el tiempo de conversión. Inicia el cronómetro antes de abrir el original y detenlo después de cerrar el escritor. El tiempo incluye descompresión, lectura de las ventas y escritura del nuevo formato. Cada archivo se genera una vez.

Después mide tres lecturas completas de cada archivo y calcula la mediana de sus duraciones. En los tres archivos de seis columnas, mide también tres lecturas solicitando únicamente tienda e importe. Los archivos reducidos ya contienen esos dos campos y solo necesitan la prueba de lectura completa.

El siguiente fragmento muestra cómo medir una lectura Parquet por lotes. Añádelo a tu script para utilizarlo después de crear ventas.parquet. Para los demás formatos adapta la función de lectura usando los lectores de la sección Procesar el archivo completo por bloques.

# Fragmento para medir la lectura de un Parquet ya generado
from statistics import median
from time import perf_counter
import pyarrow.parquet as pq
def leer_archivo(ruta, columnas=None):
cantidad = 0
with pq.ParquetFile(ruta) as archivo:
for lote in archivo.iter_batches(batch_size=50000, columns=columnas):
cantidad += lote.num_rows
return cantidad
tiempos = []
for repeticion in range(3):
inicio = perf_counter()
cantidad = leer_archivo('ventas.parquet')
tiempos.append(perf_counter() - inicio)
print('Ventas leídas:', cantidad)
print('Tiempos:', tiempos)
print('Mediana:', median(tiempos), 'segundos')

En def leer_archivo(ruta, columnas=None), ruta recibe el nombre del archivo. columnas=None establece que se leerán todas las columnas cuando no indiquemos una selección. iter_batches entrega grupos de filas y lote.num_rows permite contarlas. return cantidad devuelve el recuento cuando termina el recorrido.

tiempos.append(...) añade cada duración a la lista. median(tiempos) obtiene su valor central una vez ordenadas; por ejemplo, para 0,8, 0,6 y 0,7 segundos, la mediana es 0,7 segundos.

La función debe recorrer el archivo hasta el final dentro de la zona cronometrada. Crear un generador sin recorrerlo no mide la lectura. Cada repetición abre de nuevo el archivo e inicia el contador desde cero. median obtiene el valor central de las tres duraciones; conserva también los valores individuales. Las lecturas pueden aprovechar datos en caché de ejecuciones anteriores.

Para la prueba de dos campos, llama al lector con columnas=['tienda', 'importe']. En ORC indica esa selección en read_stripe. El tamaño se obtiene con Path(ruta).stat().st_size / 1024**2, como en el ejemplo de medición anterior.

Dentro del contenedor, desde /lab/trabajo/formatos2, descarga el archivo si todavía no lo tienes disponible.

Terminal window
curl -fL -o purchases.txt.gz https://github.com/josepgarcia/datos/raw/refs/heads/main/purchases.txt.gz

Pandas puede leer el archivo comprimido directamente. Crea comparar_formatos.py y utiliza esta carga inicial. Ejecuta esta primera carga y observa su comportamiento. Después desarrolla las conversiones en ese mismo script siguiendo las instrucciones del apartado 2.

comparar_formatos.py
import pandas as pd
columnas = ['fecha', 'hora', 'tienda', 'categoria', 'importe', 'pago']
ventas = pd.read_csv(
'purchases.txt.gz', sep='\t', header=None, names=columnas,
keep_default_na=False,
dtype={'fecha': str, 'hora': str, 'tienda': str,
'categoria': str, 'importe': 'float64', 'pago': str},
)
print(ventas.shape)
print(ventas.head())
print(ventas.dtypes)

sep='\t' indica tabuladores. El original carece de cabecera, por eso indicamos header=None y sus nombres mediante names. shape muestra filas y columnas. El archivo original utilizado en el ensayo contiene 4.138.476 ventas y seis columnas. head() muestra las primeras filas y dtypes permite comprobar los tipos.

Cada fila representa una venta. Conserva fecha, hora, tienda, categoría y pago como texto, e importe como float64. Para esta demostración se utilizan números de coma flotante; la precisión contable requiere otra elección de tipos.

Antes de continuar, ejecuta tu programa y observa su resultado. Si no termina como esperabas, conserva los mensajes o el comportamiento observado, investiga la causa y documenta qué has cambiado para poder continuar. Puedes consultar los procedimientos de la sección Procesar el archivo completo por bloques.

La sección Procesar el archivo completo por bloques contiene las operaciones de lectura y escritura necesarias para completar este apartado.

Para las conversiones, crea un lector nuevo que entregue una venta cada vez, siguiendo leer_compras() de esa sección. La primera prueba con pandas sirve para observar y documentar el comportamiento de la carga. El lector que utilizarás en las cinco conversiones debe leer el comprimido y convertir importe a float, conservando los otros cinco campos como texto.

Conserva los campos en este orden.

Posición Campo Tipo que debes conservar
1 fecha texto
2 hora texto
3 tienda texto
4 categoria texto
5 importe número de doble precisión
6 pago texto

Debes crear los siguientes archivos:

Archivo que debes crear Datos que contiene Compresión
ventas.parquet Todas las ventas y sus seis columnas Ninguna
ventas.orc Las mismas ventas y seis columnas Ninguna
ventas_snappy.orc Las mismas ventas y seis columnas Snappy
ventas_reducidas.avro Las mismas ventas, solo tienda e importe Ninguna
ventas_reducidas.parquet Las mismas ventas, solo tienda e importe Ninguna

Para Parquet y ORC, forma bloques de hasta 50.000 registros y utiliza los escritores de esa sección. Cada archivo debe recibir todas las ventas. Inicia un recorrido nuevo del original para cada producto.

Para el Parquet reducido, usa tabla.select(['tienda', 'importe']) sobre la tabla del bloque actual y declara un esquema con esos dos campos. Para Avro, prepara el esquema de dos campos y entrega un generador al escritor, como en esa sección.

Estas son las opciones de compresión de los escritores que vas a utilizar.

Producto Opción del escritor
Parquet completo y reducido ParquetWriter(..., compression='NONE')
ORC sin compresión ORCWriter(..., compression='uncompressed')
ORC con Snappy ORCWriter(..., compression='snappy')
Avro reducido writer(..., codec='null')

Los esquemas Arrow incluyen los campos de datos que has declarado. Al crearlos desde registros, no se añade el índice de pandas que evitábamos con index=False en los primeros ejemplos.

Vuelve a abrir los archivos con sus lectores. Comprueba que cada uno contiene los 4.138.476 registros del original y las columnas que le corresponden. En Avro puedes contarlos con un bucle sobre reader, como en el ejemplo. Contrasta algunas ventas con las filas iniciales del original.

Completa esta tabla con tus medidas. En los archivos reducidos escribe «No procede» en la última columna, porque su lectura completa ya recupera únicamente tienda e importe. Haz la lectura de comprobación después de detener el cronómetro de conversión. Recorre Parquet por lotes, ORC por stripes y Avro por registros, como se describe en la sección Procesar el archivo completo por bloques.

Archivo Filas Columnas Compresión Tamaño MiB Conversión s Lectura completa, mediana s Tienda e importe, mediana s
ventas.parquet
ventas.orc
ventas_snappy.orc
ventas_reducidas.avro No procede
ventas_reducidas.parquet No procede

Responde brevemente estas preguntas.

  1. ¿Qué cambia al pasar de ORC sin compresión a ORC con Snappy? Compara tamaño y tiempos de conversión y lectura.
  2. Entre Avro y Parquet con tienda e importe, ¿qué diferencias observas? Ambos contienen los mismos datos y están sin compresión.
  3. ¿Por qué el tamaño de un archivo con dos columnas no permite demostrar por sí solo que su formato sea mejor que otro con seis columnas?
  4. En los archivos de seis columnas, ¿qué cambia al leer únicamente tienda e importe? Relaciónalo con la organización columnar.

Entrega un ZIP con tu script y un informe Markdown que incluya la tabla, los tres tiempos individuales de cada prueba de lectura, las respuestas, cualquier incidencia observada y las versiones de Python, pandas, PyArrow y fastavro utilizadas. Los archivos de datos generados se conservan en tu equipo. Consulta las versiones dentro del contenedor con estas órdenes.

Terminal window
python --version
python -c "import pandas, pyarrow, fastavro; print(pandas.__version__, pyarrow.__version__, fastavro.__version__)"

Esta sección contiene los lectores y escritores que necesitas en los apartados 2 y 3 de la práctica M01-FMT-01.

Al pasar de los ejemplos pequeños al archivo original hay que considerar la memoria que ocuparán los datos y las copias intermedias del programa, además de su tamaño en disco.

Si el conjunto supera la memoria disponible, se procesa de forma incremental. El programa lee un registro o un bloque, realiza su trabajo, guarda el resultado y continúa con el siguiente. Los acumuladores necesarios se conservan entre pasos, sin reunir todos los registros en una lista.

La unidad de lectura depende del formato.

Formato Unidad de lectura Herramienta
CSV y JSONL Un registro de texto Para CSV, el lector csv de Python, que interpreta delimitadores y campos entrecomillados
Avro Un registro binario fastavro.reader
Parquet Un lote de filas ParquetFile.iter_batches
ORC Un stripe ORCFile.read_stripe

Pandas también permite leer CSV por bloques mediante el parámetro chunksize. Cada bloque es un DataFrame que debe procesarse antes de solicitar el siguiente. El tamaño del bloque debe ajustarse a los datos y a los recursos del equipo. Procesamiento de datos mayores que la memoria con pandas

Ejemplo con el archivo completo y bloques de ventas

Section titled “Ejemplo con el archivo completo y bloques de ventas”

El archivo original de la práctica contiene 4.138.476 ventas. Su carga y las conversiones adicionales pueden necesitar mucha más memoria que los aproximadamente 211 MB del texto descomprimido.

¿Por qué ocupa más en memoria que en disco? En el archivo, cada venta es una línea de texto compacta. En memoria, Python y pandas crean un objeto para cada texto, con información adicional sobre su tipo y su longitud, y algunas operaciones generan copias intermedias de los datos. Por eso la memoria necesaria puede multiplicar varias veces el tamaño del archivo.

Vamos a convertirlo completo con una lectura incremental. Construiremos el script compras_por_bloques.py en tres pasos y lo ejecutaremos al terminar cada uno. Créalo en /lab/trabajo/formatos2, donde guardaste la descarga.

El generador entrega ventas de una en una, islice forma bloques de hasta 50.000, cada bloque se convierte en tabla Arrow y ParquetWriter lo añade a ventas.parquet; se repite hasta agotar el original.

Paso 1. Un lector que entrega una venta cada vez

Section titled “Paso 1. Un lector que entrega una venta cada vez”

Empieza el script con este código. La prueba final muestra solo las tres primeras ventas.

# compras_por_bloques.py (paso 1)
import csv
import gzip
from itertools import islice
campos = ['fecha', 'hora', 'tienda', 'categoria', 'importe', 'pago']
# Leer el comprimido y entregar una venta cada vez
def leer_compras():
with gzip.open('purchases.txt.gz', 'rt', encoding='utf-8', newline='') as entrada:
for venta in csv.DictReader(entrada, fieldnames=campos, delimiter='\t'):
venta['importe'] = float(venta['importe'])
yield venta
# Prueba: mostrar las tres primeras ventas
for venta in islice(leer_compras(), 3):
print(venta)
Terminal window
python compras_por_bloques.py

Debes ver tres diccionarios con las seis claves de campos. El importe aparece como número, sin comillas.

  • gzip.open(..., 'rt'). Abre el archivo comprimido y entrega su contenido como texto a medida que se lee. No hace falta descomprimirlo previamente en otro archivo.
  • csv.DictReader(..., fieldnames=campos, delimiter='\t'). Lee los registros separados por tabuladores. Como el original no tiene cabecera, fieldnames aporta los nombres que tendrá cada diccionario.
  • yield venta. Entrega el diccionario de una venta y deja leer_compras en pausa en ese punto. Cuando se pide otra venta, la función continúa con la siguiente vuelta del bucle. De ese modo va entregando registros sin guardarlos todos en una lista. Una función que utiliza yield se llama generador.
  • islice(leer_compras(), 3). Pide al generador solo las tres primeras ventas. El resto del archivo no llega a leerse.

Añade estos import al principio del script.

import pyarrow as pa
import pyarrow.parquet as pq

Después, sustituye la prueba del paso 1 por este código.

esquema = pa.schema([
('fecha', pa.string()), ('hora', pa.string()),
('tienda', pa.string()), ('categoria', pa.string()),
('importe', pa.float64()), ('pago', pa.string()),
])
# Crear el Parquet completo escribiendo bloques limitados
entrada = leer_compras()
escritas = 0
with pq.ParquetWriter('ventas.parquet', esquema,
compression='NONE') as escritor:
while True:
bloque = list(islice(entrada, 50000))
if not bloque:
break
tabla = pa.Table.from_pylist(bloque, schema=esquema)
escritor.write_table(tabla)
escritas += len(bloque)
print('Ventas escritas:', escritas)
  • pa.schema(...). Declara los nombres y tipos de los campos de la tabla. pa.string() representa texto y pa.float64() un número de coma flotante de doble precisión.
  • entrada = leer_compras(). Crea el generador. La lectura empieza cuando otra operación le pide registros. Cada nueva llamada a leer_compras() permite iniciar un recorrido desde el principio del archivo.
  • pq.ParquetWriter(...). Abre el archivo de salida. Al terminar el bloque with, el escritor cierra el archivo y completa sus metadatos.
  • list(islice(entrada, 50000)). Pide al generador hasta 50.000 ventas y las reúne en una lista que representa el bloque actual. Al agotarse la entrada, se obtiene una lista vacía; if not bloque detecta esa situación y break termina el bucle.
  • pa.Table.from_pylist(bloque, schema=esquema). Convierte los diccionarios del bloque actual en una tabla Arrow con esos tipos.
  • escritor.write_table(tabla). Añade la tabla del bloque al archivo. escritas acumula cuántas ventas se han escrito.

Paso 3. Releer el Parquet y comprobar el recuento

Section titled “Paso 3. Releer el Parquet y comprobar el recuento”

Añade este código al final del script y ejecútalo de nuevo.

# Leer el Parquet por lotes y comprobar el número de ventas
leidas = 0
with pq.ParquetFile('ventas.parquet') as archivo:
for lote in archivo.iter_batches(batch_size=50000):
leidas += lote.num_rows
assert leidas == escritas
print('Ventas escritas y leídas:', leidas)
  • archivo.iter_batches(batch_size=50000). Relee el Parquet entregando lotes de hasta 50.000 filas. lote.num_rows indica la cantidad de filas del lote actual.
  • assert leidas == escritas. Comprueba que ambos recuentos coinciden. Si difieren, detiene el programa con un error para que revisemos el resultado.

Con la descarga utilizada en los ensayos, la salida termina así.

Ventas escritas: 4138476
Ventas escritas y leídas: 4138476

Memoria. El programa solo conserva el bloque actual y los búferes del lector y del escritor. El último bloque puede tener menos de 50.000 ventas. Si el equipo va justo de memoria, reduce el tamaño de bloque.

Alcance de la comprobación. assert compara recuentos; no revisa los valores de cada campo.

Aplicar el patrón a los otros archivos del ejercicio

Section titled “Aplicar el patrón a los otros archivos del ejercicio”

El mismo lector leer_compras() sirve para todos los archivos de la práctica; también podría alimentar un escritor CSV o JSONL registro a registro. Genera un archivo cada vez: vuelve a llamar a leer_compras() para empezar un recorrido nuevo del original y mantén el escritor abierto hasta escribir el último bloque.

Archivo Escritor Qué cambia respecto al ejemplo
ventas.orc y ventas_snappy.orc orc.ORCWriter Cada tabla del bloque se escribe con escritor.write(tabla)
ventas_reducidas.parquet pq.ParquetWriter Esquema de dos campos; cada tabla se reduce con tabla.select(...)
ventas_reducidas.avro fastavro.writer No forma bloques: recibe un generador de diccionarios de dos campos

Reutiliza el bucle que forma bloques y convierte cada bloque en tabla. Estas son las llamadas del escritor, mostradas como fragmento para adaptar tu script.

# Fragmento de escritura ORC
import pyarrow.orc as orc
with orc.ORCWriter('ventas_snappy.orc', compression='snappy',
stripe_size=8 * 1024 * 1024) as escritor:
# Aquí se repite el bucle de bloques del ejemplo
# Cada tabla creada se escribe con esta operación
escritor.write(tabla)

escritor.write(tabla) añade al ORC la tabla que has creado con el bloque actual. El bucle de bloques debe situarse dentro del with para que todas las tablas se escriban antes de cerrar el archivo.

La versión sin compresión usa compression='uncompressed' y otro nombre de archivo. stripe_size configura un tamaño objetivo de stripe; ese tamaño no es una cota exacta de memoria.

Para comprobar un ORC, recorre sus stripes y suma sus filas.

# Fragmento para contar las ventas de un ORC
import pyarrow.orc as orc
leidas = 0
with open('ventas_snappy.orc', 'rb') as entrada:
archivo = orc.ORCFile(entrada)
for numero in range(archivo.nstripes):
stripe = archivo.read_stripe(numero)
leidas += stripe.num_rows
print('Ventas leídas:', leidas)

nstripes indica cuántos stripes hay. read_stripe(numero) recupera uno como lote Arrow y num_rows indica sus filas. Para leer solo dos campos, usa read_stripe(numero, columns=['tienda', 'importe']).

Para ver algunas ventas, convierte a diccionarios solo un tramo pequeño. Un stripe puede contener muchas ventas.

# Fragmento para mostrar las tres primeras ventas de un ORC
with open('ventas_snappy.orc', 'rb') as entrada:
archivo = orc.ORCFile(entrada)
primeras = archivo.read_stripe(0).slice(0, 3)
for venta in primeras.to_pylist():
print(venta)

slice(0, 3) selecciona las filas desde la posición 0 y en cantidad de 3. to_pylist() convierte ese tramo en una lista de diccionarios.

Declara un esquema con los dos campos y reduce cada tabla del bloque antes de escribirla.

# Fragmento de escritura del Parquet reducido
esquema_dos_columnas = pa.schema([
('tienda', pa.string()), ('importe', pa.float64()),
])
with pq.ParquetWriter('ventas_reducidas.parquet', esquema_dos_columnas,
compression='NONE') as escritor:
# Aquí se repite el bucle de bloques del ejemplo
# Cada tabla creada se reduce a dos columnas antes de escribirla
escritor.write_table(tabla.select(['tienda', 'importe']))

La tabla del bloque se sigue creando con el esquema completo de seis campos. tabla.select(['tienda', 'importe']) devuelve una tabla nueva con esas dos columnas, que coincide con esquema_dos_columnas. Para comprobarlo, usa la lectura por lotes del paso 3.

Podemos entregar directamente un generador al escritor. Este fragmento utiliza el lector del ejemplo y el esquema de dos campos que debes preparar para el ejercicio.

# Fragmento de escritura Avro sin lista global
from fastavro import writer
def leer_reducidas():
for venta in leer_compras():
yield {'tienda': venta['tienda'], 'importe': venta['importe']}
with open('ventas_reducidas.avro', 'wb') as salida:
writer(salida, esquema_reducido, leer_reducidas(), codec='null')

En leer_reducidas, cada vuelta recibe una venta del generador original. La expresión {'tienda': venta['tienda'], 'importe': venta['importe']} crea un diccionario con solo esos dos campos. yield entrega ese diccionario y deja la función en pausa.

writer(..., leer_reducidas(), ...) recibe el generador y solicita sus registros a medida que los escribe. Esa llamada utiliza el esquema Avro de dos campos preparado para el ejercicio.

Para comprobarlo, recorre reader(entrada) con un bucle y cuenta los registros, sin reunirlos en una lista.

# Fragmento para contar las ventas de un Avro
from fastavro import reader
leidas = 0
with open('ventas_reducidas.avro', 'rb') as entrada:
for venta in reader(entrada):
leidas += 1
print('Ventas leídas:', leidas)

M01-SPL-01 — ¿Se puede repartir la lectura? (práctica guiada)

Section titled “M01-SPL-01 — ¿Se puede repartir la lectura? (práctica guiada)”

En el bloque 04 vimos que Hadoop reparte la entrada de un trabajo en splits y asigna cada uno a una tarea map. También vimos que la relación entre bloques y splits depende del formato y de la compresión. En esta práctica guiada lo comprobaremos con el mismo archivo de ventas en dos versiones: texto sin comprimir y comprimido con gzip. Es opcional y no forma parte de la entrega de M01-FMT-01.

Un archivo gzip es un flujo continuo: para llegar a la venta 2.000.000 hay que descomprimir todo lo anterior. Por eso Hadoop no puede empezar a leerlo por la mitad y lo asigna completo a un único split, aunque ocupe varios bloques. Un archivo de texto sin comprimir, en cambio, se puede empezar a leer en cualquier bloque, buscando el siguiente salto de línea.

Parquet y ORC resuelven este problema de otra forma. Se dividen en unidades independientes, grupos de filas y stripes, y sus metadatos indican dónde empieza cada una. Así, varias tareas pueden leer unidades distintas del mismo archivo a la vez, aunque estén comprimidas.

Cada paso indica dónde se ejecuta:

  • [Contenedor de formatos]: la terminal de la práctica M01-FMT-01.
  • [Anfitrión]: la terminal de tu equipo, fuera de cualquier contenedor.
  • [master]: la terminal del nodo master del clúster, como usuario hadoop.

1. Contar las unidades de los archivos columnares

Section titled “1. Contar las unidades de los archivos columnares”

[Contenedor de formatos] En /lab/trabajo/formatos2, donde están los archivos de M01-FMT-01, crea este script y ejecútalo.

contar_unidades.py
import pyarrow.parquet as pq
import pyarrow.orc as orc
for ruta in ['ventas.parquet', 'ventas_reducidas.parquet']:
print(ruta, 'grupos de filas:', pq.ParquetFile(ruta).metadata.num_row_groups)
for ruta in ['ventas.orc', 'ventas_snappy.orc']:
print(ruta, 'stripes:', orc.ORCFile(ruta).nstripes)
Terminal window
python contar_unidades.py

metadata.num_row_groups lee de los metadatos del Parquet cuántos grupos de filas contiene. nstripes hace lo mismo con los stripes de un ORC. Ninguno de los dos recorre las ventas.

Con los archivos generados en los ensayos, la salida es esta.

ventas.parquet grupos de filas: 83
ventas_reducidas.parquet grupos de filas: 83
ventas.orc stripes: 28
ventas_snappy.orc stripes: 7

Los 83 grupos de filas proceden de los bloques de 50.000 ventas: cada llamada a write_table ha creado uno. En ORC, el número de stripes depende de stripe_size y de la compresión: con Snappy los datos ocupan menos y caben en menos stripes.

Sal del contenedor.

Terminal window
exit

2. Arrancar el clúster y localizar el archivo de ventas

Section titled “2. Arrancar el clúster y localizar el archivo de ventas”

[Anfitrión] En BigDataAplicadoLab_2627/modulo01/, arranca el clúster del bloque 03 y entra en master.

Terminal window
make up
make shell

Si tu equipo tiene poca memoria, usa make up-mini. El número de splits que vamos a observar no depende de cuántos trabajadores haya.

[master] Entra en el directorio de la práctica del bloque 04, donde descargaste y descomprimiste las ventas.

Terminal window
cd /home/hadoop/ventas-por-tienda
ls -l purchases.txt purchases.txt.gz

Deben aparecer los dos archivos: unos 211 MB el texto y unos 38 MB el comprimido. Si falta alguno, descárgalo y descomprímelo de nuevo.

Terminal window
curl -fL -o purchases.txt.gz https://github.com/josepgarcia/datos/raw/refs/heads/main/purchases.txt.gz
gzip -dk purchases.txt.gz

3. Subir los dos archivos con bloques pequeños

Section titled “3. Subir los dos archivos con bloques pequeños”

El archivo comprimido ocupa unos 38 MB y cabría en un único bloque de 128 MB, el tamaño por defecto. Con un solo bloque daría un único split de todos modos y no podríamos ver la diferencia. Por eso subiremos ambos archivos con bloques de 16 MB.

[master]

Terminal window
hdfs dfs -mkdir -p /datos/m01-05/splits
hdfs dfs -D dfs.blocksize=16m -put purchases.txt /datos/m01-05/splits/
hdfs dfs -D dfs.blocksize=16m -put purchases.txt.gz /datos/m01-05/splits/
hdfs fsck /datos/m01-05/splits -files -blocks

-D dfs.blocksize=16m cambia el tamaño de bloque solo para los archivos de esa orden; el resto de HDFS conserva los 128 MB. Ya usamos -D con hadoop jar en el bloque 04 para indicar una opción de un trabajo.

En la salida de fsck, busca la línea de cada archivo. Debes ver 13 bloques para purchases.txt y 3 bloques para purchases.txt.gz.

4. Contar las tareas map con el texto sin comprimir

Section titled “4. Contar las tareas map con el texto sin comprimir”

Lanzaremos un trabajo de Streaming solo con fase map. Cada tarea ejecutará wc -l, que cuenta las líneas que recibe. Así, cada archivo de salida indicará cuántas ventas ha leído una tarea.

[master]

Terminal window
hadoop jar /opt/hadoop/share/hadoop/tools/lib/hadoop-streaming-3.4.1.jar \
-D mapreduce.job.reduces=0 \
-input /datos/m01-05/splits/purchases.txt \
-output /datos/m01-05/salidas/texto \
-mapper 'wc -l'
  • -D mapreduce.job.reduces=0. El trabajo no tiene fase reduce. Cada tarea map escribe directamente su propio archivo de salida.
  • -mapper 'wc -l'. Cada tarea recibe las líneas de su split por la entrada estándar y escribe cuántas ha recibido. No necesitamos ningún script de Python.

Durante la ejecución, Hadoop escribe muchas líneas de registro. Busca estas dos.

INFO JobSubmitter:... - number of splits:13
Launched map tasks=13

number of splits indica en cuántos splits se ha dividido la entrada y Launched map tasks, cuántas tareas map se han lanzado. Junto a esta última aparece Data-local map tasks: las tareas que se han ejecutado en un nodo que ya tenía su bloque.

Ahora mira los resultados de cada tarea.

Terminal window
hdfs dfs -ls /datos/m01-05/salidas/texto
hdfs dfs -cat /datos/m01-05/salidas/texto/part-*
hdfs dfs -cat /datos/m01-05/salidas/texto/part-* | awk '{s += $1} END {print s}'

Hay 13 archivos part-…, uno por tarea. Cada uno contiene unas 328.000 ventas, salvo el último, más pequeño porque su bloque no está completo. awk suma la primera columna de todas las líneas; el total debe ser 4138476.

[master] Lanza el mismo trabajo sobre el archivo gzip, con otra carpeta de salida.

Terminal window
hadoop jar /opt/hadoop/share/hadoop/tools/lib/hadoop-streaming-3.4.1.jar \
-D mapreduce.job.reduces=0 \
-input /datos/m01-05/splits/purchases.txt.gz \
-output /datos/m01-05/salidas/gzip \
-mapper 'wc -l'

Esta vez debes encontrar number of splits:1 y Launched map tasks=1.

Terminal window
hdfs dfs -ls /datos/m01-05/salidas/gzip
hdfs dfs -cat /datos/m01-05/salidas/gzip/part-*

Hay un único archivo part-00000 con 4138476 ventas. Hadoop reconoce la extensión .gz y descomprime el archivo antes de entregar las líneas a wc -l, así que el recuento coincide. La diferencia está en el reparto: una sola tarea ha leído los tres bloques.

Si repites un trabajo, borra antes su carpeta de salida con hdfs dfs -rm -r o elige otra, porque Hadoop no sobrescribe una salida existente.

Completa esta tabla con tus resultados.

Archivo Tamaño Bloques HDFS Splits y tareas map Ventas contadas
purchases.txt
purchases.txt.gz

Responde brevemente.

  1. El archivo comprimido ocupa tres bloques, pero se ha leído con una sola tarea. ¿Por qué no se ha repartido?
  2. ventas.parquet ocupa un tamaño parecido al de purchases.txt.gz y tiene 83 grupos de filas. ¿Qué ventaja tendría para repartir su lectura entre varias tareas?

[master] Borra los datos de esta práctica en HDFS y sal de master.

Terminal window
hdfs dfs -rm -r /datos/m01-05
exit

[Anfitrión] Si no vas a seguir trabajando con el clúster, detenlo. make down conserva el resto de los datos de HDFS.

Terminal window
make down

Con lo visto hasta ahora, Hadoop Streaming entrega texto línea a línea y no lee Parquet ni ORC directamente. En M03 trabajaremos con Spark, que reparte el procesamiento en particiones y aprovecha precisamente los grupos de filas de Parquet para leer un archivo con varias tareas a la vez. Los procedimientos de esta sección permiten adaptar las operaciones de la práctica al volumen de sus datos.

Del sistema de ficheros al almacenamiento de objetos

Section titled “Del sistema de ficheros al almacenamiento de objetos”

Hasta ahora hemos elegido cómo guardar los datos dentro de un archivo. La siguiente decisión es dónde guardar esos archivos para que otros procesos puedan utilizarlos.

En HDFS trabajamos con rutas y archivos cuyos bloques se reparten entre DataNodes. En un servicio de objetos, como Amazon S3 o MinIO, guardamos cada archivo como un objeto dentro de un bucket.

  • El bucket es el contenedor que agrupa los objetos.
  • La clave es el nombre completo que identifica un objeto dentro del bucket.
  • Los metadatos describen propiedades como su tamaño o tipo de contenido.

Por ejemplo, podemos subir ventas.parquet al bucket datos-prueba con la clave ventas/ventas.parquet. La barra forma parte de la clave. La consola puede mostrarla como una carpeta, pero no necesitamos crear un directorio ventas antes de subir el objeto.

Aspecto HDFS S3 o MinIO
Organización Archivos y directorios Buckets y claves de objetos
Acceso habitual Cliente Hadoop, como hdfs dfs API S3 o cliente compatible, como mc
Distribución Bloques repartidos entre DataNodes Gestionada por el servicio según su despliegue
Uso en este curso Comprender almacenamiento distribuido y su operación Compartir archivos entre procesos mediante almacenamiento de objetos

MinIO permite practicar una API compatible con S3 en nuestro equipo. La protección de los datos depende del despliegue; un único contenedor local permite estudiar las operaciones, pero no demuestra tolerancia a fallos del equipo físico.

M01-OBJ-01 — Subir y recuperar un objeto con MinIO

Section titled “M01-OBJ-01 — Subir y recuperar un objeto con MinIO”

Este recorrido continúa el bloque después de la práctica de formatos. Utilizaremos el Parquet pequeño de los ejemplos para reconocer bucket, clave y metadatos.

Cada paso indica dónde se ejecuta:

  • [Contenedor de formatos]: la terminal que has usado en la práctica.
  • [Anfitrión]: la terminal de tu equipo, fuera de cualquier contenedor.
  • [Cliente mc]: la terminal dentro del contenedor cliente de MinIO.

1. [Contenedor de formatos] Sal del contenedor.

Terminal window
exit

2. [Anfitrión] Sitúate en la raíz de los apuntes BDA_2627/, donde están la carpeta M01/ y el archivo M01.md. Crea la carpeta que compartiremos con el cliente MinIO.

Terminal window
mkdir -p trabajo-m01/formatos

3. [Anfitrión] Con el explorador de archivos, copia el Parquet del ejemplo a la carpeta nueva y cámbiale el nombre a ventas.parquet. El origen y el destino están en dos proyectos diferentes.

BigDataAplicadoLab_2627/ ← proyecto del laboratorio de formatos
└── modulo01/trabajo/formatos2/
└── ventas_ejemplo.parquet ← origen, creado con ejemplo_parquet.py
BDA_2627/ ← raíz de los apuntes
├── M01/laboratorio-minio/docker-compose.yml
└── trabajo-m01/formatos/
└── ventas.parquet ← destino, copia renombrada

4. [Anfitrión] Desde la raíz de los apuntes, arranca el laboratorio ya preparado.

Terminal window
docker compose -f M01/laboratorio-minio/docker-compose.yml up -d --wait minio

La API queda en http://localhost:9002 y la consola web en http://localhost:9001. Puedes entrar en la consola con minioadmin y contraseña minioadmin, credenciales del laboratorio local.

5. [Anfitrión] Abre la terminal del cliente desde la misma carpeta.

Terminal window
docker compose -f M01/laboratorio-minio/docker-compose.yml run --rm mc

mc es MinIO Client. El laboratorio utiliza la imagen fijada minio/minio:RELEASE.2025-07-18T21-56-31Z, que ya incluye ese programa. Ahora estás dentro del contenedor cliente, donde trabajo-m01/ del anfitrión aparece como /trabajo.

6. [Cliente mc] Ejecuta estas órdenes.

Terminal window
mc alias set m01local http://minio:9000 minioadmin minioadmin
mc mb --ignore-existing m01local/datos-prueba
mc cp /trabajo/formatos/ventas.parquet m01local/datos-prueba/ventas/ventas.parquet
mc ls --recursive m01local/datos-prueba/
mc stat m01local/datos-prueba/ventas/ventas.parquet
mc cp m01local/datos-prueba/ventas/ventas.parquet /trabajo/formatos/ventas-recuperadas.parquet
sha256sum /trabajo/formatos/ventas.parquet /trabajo/formatos/ventas-recuperadas.parquet
exit
Orden Qué hace Operación parecida en HDFS
mc alias set m01local ... Guarda la dirección y las credenciales del servidor con el nombre m01local El cliente Hadoop toma la dirección de su configuración
mc mb --ignore-existing ... Crea el bucket; si ya existe, continúa sin error hdfs dfs -mkdir -p crea un directorio
mc cp origen destino Sube un objeto o lo descarga, según el sentido de la copia hdfs dfs -put y hdfs dfs -get
mc ls --recursive ... Lista los objetos del bucket con sus claves hdfs dfs -ls -R
mc stat ... Muestra los metadatos del objeto, como su tamaño y tipo —
sha256sum archivo1 archivo2 Calcula una huella del contenido de cada archivo No es de MinIO ni de HDFS: es una orden de Linux

Dentro de la red Docker, el cliente encuentra el servidor como minio:9000; la dirección localhost:9002 se utiliza desde el anfitrión.

La primera copia sube el archivo y la segunda lo recupera con otro nombre. Las dos líneas de sha256sum deben mostrar la misma huella. Los archivos recuperados se conservan en el anfitrión gracias al montaje.

Anota estos datos.

Dato Tu resultado
Bucket
Clave del objeto
Tamaño mostrado por mc stat
¿Coinciden las dos huellas SHA-256?
Qué cambia respecto a subir el archivo con hdfs dfs -put (dos o tres frases)

7. [Anfitrión] Detén el laboratorio.

Terminal window
docker compose -f M01/laboratorio-minio/docker-compose.yml down

Esta parada conserva los objetos en el volumen del servidor. Si abres otra sesión del cliente, tendrás que volver a ejecutar mc alias set para definir la conexión.

Un data lake reúne datos originales y procesados para que distintos motores puedan analizarlos. Por ejemplo, un bucket puede conservar eventos JSONL y un histórico Parquet. El almacenamiento de objetos aporta el lugar donde guardarlos; los formatos indican cómo están representados.

Cuando varios procesos escriben y consultan esos archivos, necesitamos controlar qué archivos forman una tabla y qué versión deben leer las consultas. Un lakehouse incorpora esa gestión de tablas sobre el almacenamiento del data lake.

Capa Ejemplo Función
Almacenamiento S3 o MinIO Guarda los objetos
Formato de archivo Parquet Guarda los datos y sus tipos
Formato de tabla Apache Iceberg Organiza los archivos y las versiones de una tabla

Abajo, el bucket datos-prueba de MinIO guarda los objetos ventas/enero.parquet y ventas/febrero.parquet; en medio, los archivos Parquet; arriba, la tabla Iceberg ventas, cuyo snapshot 1 incluye enero y el snapshot 2, el actual, enero y febrero.

Apache Iceberg — gestionar una tabla formada por archivos

Section titled “Apache Iceberg — gestionar una tabla formada por archivos”

Imagina que una tabla de ventas está repartida entre varios Parquet. Iceberg registra cuáles pertenecen a cada estado de la tabla. Así, una consulta puede leer una versión coherente aunque otro proceso esté preparando una actualización.

  • Un snapshot identifica una versión de la tabla. Las versiones anteriores se pueden consultar mientras se conserven sus metadatos y los archivos necesarios.
  • Los metadatos describen los archivos que forman esa versión y sus características.
  • El catálogo permite localizar la referencia actual de la tabla.

Por ejemplo, una tabla de ventas recibe un archivo nuevo cada mes.

Snapshot Archivos que forman la tabla Qué ha ocurrido
1 enero.parquet Carga inicial
2 enero.parquet y febrero.parquet Carga de febrero

Una consulta que empezó con el snapshot 1 mientras se cargaba febrero sigue viendo solo enero hasta terminar. Las consultas nuevas leen el snapshot 2. Mientras se conserven sus metadatos y archivos, también se puede volver a consultar el snapshot 1.

Iceberg también facilita cambios compatibles de esquema y de particionado. En este bloque nos interesa distinguir archivo, almacenamiento y tabla. La práctica anterior ya nos ha permitido trabajar con Parquet y con objetos; estos conceptos explican qué capa adicional aporta un lakehouse.

M01-FMT-02 — Elegir las capas necesarias

Section titled “M01-FMT-02 — Elegir las capas necesarias”

Para cada caso, propón un formato de archivo, un lugar donde guardarlo y si añadirías una tabla Iceberg. Justifica la decisión con una propiedad que hemos explicado. La primera fila es un ejemplo resuelto.

Necesidad Formato de archivo Almacenamiento ¿Añadirías Iceberg? ¿Por qué?
Ejemplo: guardar durante un año los registros de acceso de un servidor para auditorías puntuales JSONL comprimido con gzip: cada acceso es un registro completo y el texto se puede revisar Bucket de MinIO o S3 No: solo se añaden archivos nuevos y nadie actualiza la tabla ni consulta versiones anteriores
Compartir 200 ventas para revisarlas manualmente
Intercambiar eventos que pueden añadir campos opcionales
Consultar un histórico por fecha y categoría, con cargas nocturnas completas
Actualizar un histórico desde varios procesos y consultar versiones anteriores

La secuencia de explicación, ejemplos y actividad recupera los materiales del curso anterior. Para ampliar conceptos y consultar otros ejemplos, utiliza Formatos de datos en Big Data, de Aitor Medrano. Las interfaces utilizadas se documentan en pandas 2.2, PyArrow 18.1 y fastavro.