Apache Parquet: Formato Columnar para Analítica de Big Data
Apache Parquet es un formato de almacenamiento columnar de código abierto diseñado para su uso con frameworks de procesamiento de big data en el ecosistema Hadoop y los modernos data lakes en la nube. Desarrollado conjuntamente por Twitter y Cloudera en 2013 y donado a la Apache Software Foundation, Parquet se ha convertido en el estándar de facto para cargas de trabajo analíticas — utilizado por Apache Spark, Apache Hive, Presto, AWS Athena, Google BigQuery, Snowflake, DuckDB y prácticamente todas las plataformas de datos modernas.
La idea central detrás de Parquet — y todos los formatos columnares — es que las consultas analíticas típicamente acceden solo a unas pocas columnas de muchas, y el almacenamiento columnar hace que estas lecturas selectivas por columna sean dramáticamente más rápidas que el almacenamiento orientado a filas.
Almacenamiento por Filas vs Columnar: Por Qué Importa
Considera una tabla con 1.000 millones de filas y 50 columnas. Una consulta como SELECT AVG(ingresos) FROM pedidos WHERE pais = 'ES' necesita solo 2 columnas de 50.
Almacenamiento por filas (CSV, formatos anteriores a Parquet):
Para leer pais e ingresos, el motor debe escanear cada byte de cada fila — leyendo las 50 columnas aunque solo se necesitan 2. Para 1.000 millones de filas a ~200 bytes/fila, eso son 200 GB de I/O para una consulta de 2 columnas.
Almacenamiento columnar (Parquet):
El motor lee solo pais y ingresos — una reducción de ~17× en datos leídos. Con compresión (pais tiene muy alta repetición), el I/O real puede ser inferior a 10 GB en lugar de 200 GB.
Estructura del Archivo Parquet
Un archivo Parquet se organiza en una jerarquía de estructuras físicas y lógicas:
Archivo Parquet
├── Magic bytes: PAR1 (4 bytes)
├── Row Group 0 (ej., 128 MB de filas)
│ ├── Column Chunk: 'id' (int64)
│ │ ├── Data Page 0
│ │ └── Dictionary Page (si usa dict encoding)
│ ├── Column Chunk: 'pais' (string, dict-encoded)
│ └── Column Chunk: 'ingresos' (double)
├── Row Group 1
│ └── ...
├── Footer del Archivo (metadatos)
│ ├── Esquema (nombres de columnas, tipos, tipos lógicos)
│ ├── Metadatos de row group (offsets, estadísticas min/max por column chunk)
│ └── Metadatos clave-valor
└── Magic bytes: PAR1 (4 bytes)
Row Groups
Un row group es una partición horizontal de los datos — un conjunto de filas consecutivas almacenadas juntas. El tamaño predeterminado de row group en PyArrow y Spark es 128 MB. Los row groups más grandes mejoran los ratios de compresión pero aumentan los requisitos de memoria para la escritura; los más pequeños habilitan un pushdown de predicados más granular.
Column Chunks
Dentro de cada row group, cada columna se almacena como un column chunk — todos los valores de esa columna a través de todas las filas del row group, almacenados contiguamente. Esto permite lecturas selectivas por columna.
Páginas
Los column chunks se dividen además en páginas — la unidad más pequeña de compresión y codificación en Parquet, típicamente 1 MB.
Esquemas de Codificación
Parquet usa varios esquemas de codificación para reducir el tamaño de los datos antes de la compresión:
Codificación de Diccionario
Para columnas de baja cardinalidad (como pais, estado, categoria), Parquet construye un diccionario de valores distintos y codifica cada valor como un pequeño índice entero:
Diccionario: {0: "ES", 1: "US", 2: "DE", 3: "FR"}
Valores codificados: [0, 1, 0, 0, 1, 2, 0, 3, 1, 0, ...]
En lugar de almacenar strings repetidas como "España" (6 bytes cada una) miles de veces, cada ocurrencia cuesta 1-2 bytes como índice de diccionario.
Delta Encoding
Para columnas de enteros/timestamps ordenadas o casi ordenadas, delta encoding almacena solo las diferencias entre valores consecutivos:
Valores: [1000, 1001, 1003, 1007, 1015]
Deltas: [1000, +1, +2, +4, +8]
Los timestamps con intervalos regulares hacen que delta encoding sea extremadamente efectivo.
Codecs de Compresión
| Codec | Velocidad | Ratio | Caso de uso |
|---|---|---|---|
| UNCOMPRESSED | Más rápido | 1× | Cuando CPU es el cuello de botella |
| SNAPPY | Muy rápido | ~2-3× | Predeterminado; equilibrio velocidad/ratio |
| GZIP | Lento | ~3-5× | Máxima compatibilidad |
| LZ4 | Más rápido | ~2-3× | Pipelines de alto rendimiento |
| ZSTD | Rápido | ~3-6× | Mejor trade-off ratio+velocidad |
Zstandard (ZSTD) en nivel 3 es cada vez más el predeterminado en herramientas modernas (DuckDB, versiones más recientes de Spark).
Python: PyArrow
import pyarrow as pa
import pyarrow.parquet as pq
import pandas as pd
import numpy as np
# Crear una tabla PyArrow
tabla = pa.table({
'id': pa.array(range(1000000), type=pa.int64()),
'pais': pa.array(['ES', 'US', 'DE'] * 333334, type=pa.string()),
'ingresos': pa.array(np.random.uniform(10, 1000, 1000000), type=pa.float64()),
'timestamp': pa.array(pd.date_range('2024-01-01', periods=1000000, freq='s')),
})
# Escribir Parquet con opciones
pq.write_table(
tabla,
'pedidos.parquet',
compression='zstd',
compression_level=3,
row_group_size=100000,
use_dictionary=['pais'],
write_statistics=True,
)
# Lectura selectiva de columnas (solo lee 2 columnas)
ingresos_pais = pq.read_table(
'pedidos.parquet',
columns=['pais', 'ingresos'],
)
# Predicate pushdown (solo lee row groups relevantes)
filtrado = pq.read_table(
'pedidos.parquet',
columns=['id', 'ingresos'],
filters=[('pais', '=', 'ES')],
)
# Dataset particionado
pq.write_to_dataset(
tabla,
root_path='pedidos_particionados/',
partition_cols=['pais'],
compression='zstd',
)
# Crea:
# pedidos_particionados/pais=ES/part-0.parquet
# pedidos_particionados/pais=US/part-0.parquet
Estadísticas y Predicate Pushdown
Parquet almacena estadísticas min/max para cada columna en cada row group. Los motores de consulta usan esto para saltarse row groups enteros que no pueden contener filas coincidentes:
Row group 0: pais min='DE', max='US' → podría contener 'ES'
Row group 1: pais min='DE', max='DE' → saltar (ningún 'ES' posible)
Row group 2: pais min='ES', max='US' → podría contener 'ES'
En conjuntos de datos bien ordenados, solo una fracción de los row groups necesita leerse — reducción de I/O del 90%+ para consultas selectivas.
Parquet con DuckDB
DuckDB puede consultar archivos Parquet directamente con SQL:
-- Consultar Parquet directamente
SELECT pais, AVG(ingresos) as ingreso_medio, COUNT(*) as pedidos
FROM read_parquet('pedidos.parquet')
WHERE timestamp >= '2024-06-01'
GROUP BY pais
ORDER BY ingreso_medio DESC;
-- Convertir CSV a Parquet
COPY (SELECT * FROM read_csv('pedidos.csv')) TO 'pedidos.parquet'
(FORMAT parquet, COMPRESSION zstd);
Conclusión
Apache Parquet ha ganado su posición dominante en la ingeniería de datos analíticos porque resuelve el problema correcto con ingeniería elegante: organizando los datos por columna en lugar de por fila, permite que las consultas analíticas lean solo los datos que necesitan, aplica compresión altamente efectiva a datos de columna homogéneos y proporciona predicate pushdown basado en estadísticas para saltarse datos irrelevantes antes de cualquier I/O. Ya sea que estés construyendo un data lake en S3, consultando datos con DuckDB en un portátil o ejecutando trabajos Spark de escala petabyte, Parquet es el formato que hace que estas cargas de trabajo sean prácticas.
Conversiones relacionadas
Conversiones frecuentes del catálogo: