Entendiendo la estructura de un archivo Parquet: Row Groups, Columnas y Metadata
¿Tienes un archivo .parquet? Ábrelo y explóralo gratis en tu navegador — sin instalar nada.
Abrir la appEl rendimiento de Apache Parquet no viene de la magia — viene de un diseño físico cuidadoso que permite a los motores de consulta leer la mínima cantidad de datos necesaria. Entender ese layout te ayuda a escribir consultas más rápidas, elegir mejores ajustes de compresión y depurar problemas cuando aparecen.
En este artículo desarmamos un archivo Parquet pieza por pieza — desde el nivel más alto hasta las páginas individuales y los esquemas de codificación — y te mostramos cómo inspeccionar cada capa por tu cuenta.
Vista general: el layout físico
Todo archivo Parquet sigue el mismo layout:
+-----------------------------+
| Magic Number | 4 bytes: "PAR1"
+-----------------------------+
| Row Group 0 |
| Column Chunk 0.0 |
| Column Chunk 0.1 |
| Column Chunk 0.2 |
| ... |
+-----------------------------+
| Row Group 1 |
| Column Chunk 1.0 |
| ... |
+-----------------------------+
| ... |
+-----------------------------+
| File Metadata | (el footer)
+-----------------------------+
| Footer Length (4 bytes) |
+-----------------------------+
| Magic Number | 4 bytes: "PAR1"
+-----------------------------+
El archivo empieza y termina con los bytes mágicos PAR1. Los datos reales viven en los row groups del medio. La metadata del archivo (el footer) está al final, justo antes del magic number de cierre.
Un lector siempre empieza leyendo los últimos 8 bytes del archivo para encontrar la longitud del footer, luego lee el footer para conocer el esquema, la ubicación de los row groups y las estadísticas de columna. Solo entonces lee los datos reales — y solo las partes que necesita.
Row Groups: la unidad de paralelismo
Un row group es una partición horizontal de los datos. Si tu archivo contiene 10 millones de filas y usa un tamaño de row group de 1 millón, el archivo tendrá 10 row groups.
Para qué sirven
Los row groups cumplen tres funciones críticas:
- Paralelismo: distintos hilos o núcleos pueden procesar row groups diferentes de forma simultánea.
- Gestión de memoria: el motor procesa un row group a la vez, manteniendo el uso de memoria acotado.
- Predicate pushdown: cada row group almacena estadísticas de columna (mínimo, máximo, cuenta de nulos). Si tu consulta filtra por
WHERE fecha > '2026-01-01'y el máximo de un row group es2025-12-31, el motor se salta ese row group entero sin leer un solo byte de datos.
Verlos en la práctica
No tienes que creer en los row groups por fe. En Parquet Explorer, el inspector de metadata te muestra el desglose de cualquier archivo que cargues: cuántos row groups hay, cuántas filas tiene cada uno, el codec de compresión y las estadísticas por columna (min/max, nulos, distintos) de cada column chunk. Así se vuelve tangible: puedes ver exactamente qué row groups saltaría una consulta según tus filtros.
Cómo dimensionarlos
El tamaño por defecto varía según el escritor:
- Apache Spark: 128 MB (comprimido)
- PyArrow: 64 MB o por número de filas
- DuckDB: ajustado automáticamente
Compromisos:
- Row groups más pequeños: mejor granularidad de predicate pushdown y menor memoria por grupo, pero más overhead de metadata y compresión ligeramente peor.
- Row groups más grandes: mejor ratio de compresión y menos overhead de metadata, pero mayor uso de memoria y pushdown más grueso.
Para la mayoría de cargas, el valor por defecto está bien. Si estás optimizando, apunta a row groups de entre 50 MB y 256 MB.
Column Chunks
Dentro de cada row group los datos se almacenan por columna. Un column chunk contiene todos los valores de una columna dentro de un row group.
Por ejemplo, en una tabla con columnas (id, nombre, edad, ciudad) y 2 row groups, el archivo contiene 8 column chunks:
Row Group 0: [id_chunk, nombre_chunk, edad_chunk, ciudad_chunk]
Row Group 1: [id_chunk, nombre_chunk, edad_chunk, ciudad_chunk]
Cada column chunk se guarda de forma contigua en disco. Esta es la clave del column pruning: si tu consulta solo referencia id y edad, el motor lee únicamente esos column chunks y se salta nombre y ciudad por completo.
Cada column chunk tiene su propio codec de compresión. Aunque la mayoría de escritores usan el mismo codec para todas las columnas, técnicamente es posible usar codecs distintos por columna (por ejemplo, Snappy para columnas de texto grandes y Zstd para columnas numéricas).
Páginas
Los column chunks se dividen a su vez en páginas (pages), la unidad mínima de I/O en Parquet. Suelen ser de ~1 MB. Hay tres tipos:
Data Pages
Contienen los valores reales de la columna, codificados y opcionalmente comprimidos. Cada data page incluye:
- Repetition y definition levels: usados para datos anidados (listas, maps, structs). En esquemas planos son triviales.
- Valores codificados: los datos de la columna, codificados con alguno de los esquemas (ver Encodings abajo).
- Cabecera de página: metadata con tamaños comprimido/sin comprimir, tipo de encoding y número de valores.
Existen dos versiones: Data Page V1 y Data Page V2. V2 separa los repetition/definition levels de los datos y permite que los levels queden sin comprimir mientras los datos sí se comprimen, lo que hace la lectura más eficiente.
Dictionary Pages
Cuando se usa dictionary encoding (el valor por defecto en la mayoría de escritores), la primera página de un column chunk es una página de diccionario. Contiene los valores únicos de esa columna, y las data pages siguientes referencian el diccionario por índice.
Por ejemplo, una columna pais con 1 millón de filas pero solo 50 países únicos guardaría una página de diccionario de 50 entradas, y cada data page contendría índices enteros (0-49) en lugar de las cadenas completas del nombre del país.
Index Pages
Parquet soporta páginas opcionales de column index y offset index que permiten filtrado fino dentro de un column chunk. El column index guarda min/max por página, permitiendo al motor saltar páginas individuales — no solo row groups enteros.
Encodings: cómo se codifican los datos
Parquet usa varios esquemas de codificación para representar los valores de forma eficiente. El escritor elige el encoding según el tipo de columna y las características de los datos.
Plain
Los valores se guardan tal cual, en su formato nativo. Es el fallback cuando ningún otro encoding aporta beneficio.
Dictionary Encoding (PLAIN_DICTIONARY / RLE_DICTIONARY)
El encoding más común. Se construye un diccionario de valores únicos y cada valor se reemplaza por un índice entero; luego los índices se codifican con run-length o bit-packing.
Es extremadamente efectivo para columnas de cardinalidad baja o moderada — códigos de estado, nombres de país, categorías, flags booleanos. Es menos efectivo (y puede desactivarse) para columnas de alta cardinalidad como UUIDs o campos de texto libre.
La mayoría de escritores empiezan con dictionary encoding y caen a plain encoding si el diccionario supera cierto umbral de tamaño (típicamente 1 MB, o cuando el número de valores únicos supera una fracción del tamaño de página).
Run-Length Encoding (RLE)
Los valores repetidos consecutivos se guardan como un par (valor, cuenta). [A, A, A, B, B] se convierte en [(A, 3), (B, 2)]. Efectivo para datos ordenados o columnas con muchos valores repetidos. Parquet usa un esquema híbrido RLE/bit-packing que maneja bien ambos casos.
Delta Encoding
Almacena la diferencia entre valores consecutivos en lugar de los valores en sí. Muy efectivo para columnas de enteros ordenados (timestamps, IDs autoincrementales). En lugar de [1000, 1001, 1003] guarda [1000, +1, +2]. Variantes:
- DELTA_BINARY_PACKED: para enteros.
- DELTA_LENGTH_BYTE_ARRAY: para strings de longitud variable (codifica las longitudes con delta).
- DELTA_BYTE_ARRAY: para strings con prefijos comunes.
Byte Stream Split
Un encoding más reciente para datos de punto flotante. Reordena los bytes de los valores float/double para agrupar bytes similares (todos los primeros bytes, luego todos los segundos, etc.), lo que comprime mucho mejor con codecs de propósito general.
Esquemas anidados: STRUCT, LIST y MAP
Una característica poderosa que muchos desconocen es el soporte para tipos anidados:
- STRUCT: sub-campos, como
direccion.calle,direccion.ciudad. - LIST: listas de valores, como
tags: ["urgente", "revisado"]. - MAP: pares clave-valor.
Estos tipos permiten representar estructuras tipo JSON dentro de un formato columnar eficiente. Visualizarlos en una terminal es complicado. El visor de esquema de Parquet Explorer los muestra como un árbol interactivo donde puedes expandir y colapsar cada nivel.
Compresión: la capa final
Después de la codificación, cada página puede comprimirse con un codec de propósito general:
| Codec | Velocidad | Ratio | Notas |
|---|---|---|---|
| Snappy | Muy rápida | Moderado | Por defecto en muchas herramientas. Bueno para cargas interactivas. |
| Zstd | Rápida | Alto | El mejor balance general. Cada vez más el default recomendado. |
| Gzip | Lenta | Alto | Opción heredada. Zstd es mejor en casi todos los casos. |
| LZ4 | Muy rápida | Bajo | Rápido, pero menor ratio que Snappy en la práctica. |
| Brotli | Lenta | Muy alto | Rara vez usado en Parquet. Mejor para contenido web. |
| Sin comprimir | N/A | Ninguno | Útil para depurar o datos ya comprimidos. |
La compresión se aplica por página, después del encoding. El paso de codificación (dictionary, RLE, delta) hace el trabajo pesado de reducir el tamaño, y el codec de compresión se encarga de la redundancia restante.
Cuando creas o conviertes archivos con Parquet Explorer, eliges entre Snappy, Zstd y Gzip — para ajustar el codec a tu caso de uso sin escribir código.
El footer: la clave de todo
El footer es la parte más importante de un archivo Parquet para la planificación de consultas. Es una estructura serializada con Thrift que contiene:
Metadata del archivo
- Versión: versión del formato Parquet.
- Esquema: el esquema completo de columnas, incluyendo tipos anidados, con los tipos lógicos (DATE, TIMESTAMP, DECIMAL, etc.) anotados sobre los tipos físicos.
- Número de filas: total de filas en todos los row groups.
- Metadata de row groups: para cada row group, la metadata de cada column chunk.
- Metadata clave-valor: metadata arbitraria definida por el usuario (esquema de Spark, esquema de Arrow, metadata de pandas o etiquetas específicas de la aplicación).
Metadata de column chunk (por row group, por columna)
- Offset y tamaño: dónde vive el column chunk en el archivo.
- Codec de compresión: cuál se usó.
- Encodings: qué codificaciones se aplicaron.
- Número de valores: incluyendo nulos.
- Estadísticas: valor mínimo, máximo, cuenta de nulos y de distintos (opcional). Son las que hacen posible el predicate pushdown.
- Tamaños: comprimido y sin comprimir.
Por qué importa el footer
Cuando un motor abre un archivo Parquet, lee el footer primero. Solo con el footer, sin leer ningún dato, puede:
- Determinar el esquema y validarlo contra la consulta.
- Identificar qué row groups saltar según las estadísticas de columna.
- Identificar qué column chunks leer según las columnas de la consulta.
- Planificar lecturas paralelas entre row groups.
Por eso las consultas sobre Parquet pueden empezar a devolver resultados casi al instante, incluso en archivos de varios gigabytes.
Cómo todo esto acelera tus consultas
Ejemplo concreto: archivo con 100 columnas, 500 millones de filas, 50 row groups.
SELECT AVG(precio) FROM ventas WHERE categoria = 'electrónica';
Lo que pasa internamente:
- Lee el footer (una lectura al final del archivo).
- Identifica columnas necesarias: solo
precioycategoria— 2 de 100 column chunks por row group. - Revisa estadísticas de
categoriaen cada row group. Si alguno no contiene “electrónica” en su rango min/max, lo salta. - Lee y descomprime solo los column chunks y páginas relevantes.
- Ejecuta el filtro y la agregación en memoria.
Cada capa del diseño contribuye. Por eso una consulta sobre 3 columnas de un archivo Parquet de 50 columnas y 10 GB puede completarse en menos de un segundo.
Inspeccionar la estructura tú mismo
Entender la teoría está bien, pero verlo en tus propios archivos lo hace concreto. Estas son las mejores formas de inspeccionar los internos de Parquet.
Parquet Explorer
En parquetexplorer.com carga cualquier archivo Parquet para obtener una vista completa de su estructura:
- Árbol de esquema: navega todos los tipos de columna, incluyendo jerarquías anidadas STRUCT, LIST y MAP, renderizadas como un árbol expandible.
- Inspector de metadata: número de row groups, filas por grupo, codecs de compresión y estadísticas por columna (min, max, nulos, distintos).
- Perfilador de datos: ve más allá de la metadata cruda — histogramas por columna, detección de tipo semántico (emails, URLs, UUIDs, IPs, teléfonos) y una puntuación de calidad de datos.
- Consultas SQL: consulta los datos al instante para validar lo que dice la metadata.
Todo corre en tu navegador, sin instalar nada. Tus archivos se quedan en tu equipo.
PyArrow
import pyarrow.parquet as pq
meta = pq.read_metadata("datos.parquet")
print(f"Filas: {meta.num_rows}")
print(f"Row groups: {meta.num_row_groups}")
print(f"Columnas: {meta.num_columns}")
for i in range(meta.num_row_groups):
rg = meta.row_group(i)
print(f"\nRow Group {i}: {rg.num_rows} filas")
for j in range(rg.num_columns):
col = rg.column(j)
print(f" {col.path_in_schema}: {col.compression} "
f"({col.total_compressed_size}/{col.total_uncompressed_size} bytes)")
DuckDB
SELECT * FROM parquet_metadata('datos.parquet');
SELECT * FROM parquet_schema('datos.parquet');
Implicaciones prácticas
Entender la estructura del archivo lleva a optimizaciones concretas:
-
Ordena tus datos antes de escribir. Las columnas ordenadas tienen rangos min/max más ajustados por row group, lo que mejora el predicate pushdown. Ordenar también potencia la efectividad de RLE y delta encoding.
-
Elige el orden de columnas con cuidado. Las columnas que se consultan juntas deberían estar cerca en el esquema (aunque los lectores modernos manejan bien las lecturas no contiguas).
-
Vigila el fallback de dictionary encoding. Si una columna cae de dictionary a plain encoding, su ratio de compresión baja. El perfilador de datos de Parquet Explorer te ayuda a detectar columnas de alta cardinalidad donde esto puede pasar — busca columnas con conteo de distintos cercano al total de filas.
-
Dimensiona bien tus row groups. El default suele estar bien, pero si optimizas para consultas muy selectivas (leer 0.1% de las filas), los row groups pequeños ayudan. Para escaneos completos, mejor los grandes.
-
Usa compresión Zstd. A menos que tengas una razón específica para Snappy o Gzip, Zstd ofrece el mejor balance ratio/velocidad en 2026.
Conclusión
El rendimiento de Parquet viene de su arquitectura en capas: los row groups permiten paralelismo y pruning, los column chunks permiten projection pushdown, las páginas permiten I/O fino, los encodings minimizan el tamaño a nivel lógico y la compresión lo reduce aún más a nivel físico. El footer lo une todo dándole al motor un mapa completo del archivo antes de leer un solo byte de datos.
Este diseño es la razón por la que una consulta sobre 3 columnas de un archivo Parquet de 50 columnas y 10 GB puede completarse en menos de un segundo — el motor lee solo los column chunks relevantes de los row groups relevantes y se salta todo lo demás.
Para explorar la estructura de tus propios archivos Parquet de forma práctica, prueba parquetexplorer.com. Carga un archivo y navega el árbol de esquema, inspecciona la metadata de row groups, ejecuta el perfilador de datos y consulta los datos — todo en un solo lugar, directo en tu navegador.