Formats columnars: Parquet i ORC
Els formats columnars com Apache Parquet i Apache ORC emmagatzemen les dades organitzades per columna en lloc de per fila. Aquest canvi fonamental en la disposició física de les dades permet llegir únicament les columnes necessàries per a una consulta, aplicar filtres sense llegir les dades reals (predicate pushdown) i comprimir molt millor gràcies a la similitud de valors dins d'una mateixa columna. El resultat: consultes analítiques de 10 a 100 vegades més ràpides que sobre CSV, amb fitxers que ocupen entre 5 i 10 vegades menys espai.
Per que el format columnar
Emmagatzematge per fila vs per columna
En un format per fila (row-oriented), com CSV, tots els camps d'un registre son contigus al disc. Quan vols calcular la suma dels imports de cinc milions de transaccions, has de llegir tots els camps de totes les files, incloent-hi nom, data, origen, destí i moneda, que no necessites.
Format per fila (CSV):
[id=1, data=2024-01, nom=Empresa SL, import=1500, moneda=EUR]
[id=2, data=2024-01, nom=Acme Inc, import=200, moneda=USD]
[id=3, data=2024-02, nom=Beta Corp, import=750, moneda=EUR]
En un format columnar, els valors de cada columna son contigus:
Format columnar (Parquet):
id: [1, 2, 3, ...]
data: [2024-01, 2024-01, 2024-02, ...]
nom: [Empresa SL, Acme Inc, Beta Corp, ...]
import: [1500, 200, 750, ...]
moneda: [EUR, USD, EUR, ...]
Per calcular la suma d'imports, el motor de consulta llegeix únicament la columna import. Tota la resta queda intacta al disc.
Avantatges analítics
Column pruning (poda de columnes): el motor llegeix només les columnes referenciades a la consulta. En una taula de 50 columnes on la consulta n'usa 3, s'evita llegir el 94% de les dades.
Predicate pushdown: les estadístiques emmagatzemades al footer del fitxer (valor mínim i màxim de cada columna per bloc) permeten descartar blocs sencers sense llegir-los. Si busques imports > 10.000 i el bloc té màxim 500, s'omet completament.
Millor compressió: dins d'una columna, els valors son del mateix tipus i sovint molt similars (dates consecutives, categories repetides). Els algorismes de compressió aprofiten aquesta repetició molt millor que amb dades barrejades per fila.
Algoritmes de compressió
| Algoritme | Velocitat | Ratio de compressió | Us recomanat |
|---|---|---|---|
| Snappy | Molt alt | Moderat (2-3x) | Producció, interactiu |
| Gzip | Mitjà | Alt (4-6x) | Arxiu, emmagatzematge fred |
| Zstd | Alt | Alt (4-7x) | Equilibri recomanat |
| LZ4 | El mes alt | Baix (1.5-2x) | Streaming, temps real |
| Brotli | Baix | Molt alt (6-9x) | Arxiu a llarg termini |
Encodings interns
Abans de comprimir, Parquet aplica encodings que redueixen la mida de les dades:
- RLE (Run-Length Encoding): substitueix repeticions consecutives del mateix valor per un comptador. La columna
[EUR, EUR, EUR, EUR, USD, USD]es converteix en[(EUR, 4), (USD, 2)]. - Dictionary encoding: substitueix els valors d'un conjunt petit (categories, codis de país) per enters petits. La columna
monedaamb 5 valors possibles s'emmagatzema com enters 0-4 amb un diccionari separat. - Bit packing: enters petits s'emmagatzemen amb el nombre mínim de bits necessaris.
- Delta encoding: per a seqüències numèriques creixents (ids, timestamps), s'emmagatzema la diferència entre valors consecutius en lloc del valor absolut.
Per que les dades categoriques es compixen tan be
Una columna de moneda amb valors EUR, USD, GBP, JPY, CHF en un dataset de 5 milions de files usa dictionary encoding: tots els 5 milions de valors queden representats com enters de 3 bits (0-4), més un diccionari de 5 entrades. El ratio de compressió pot superar 10:1 respecte al CSV original.
Apache Parquet
Origen i ecosistema
Parquet va néixer el 2013 d'una col·laboració entre Twitter i Cloudera per resoldre les limitacions de SequenceFile i RCFile en el processament Hadoop. Avui és el format estàndard de facto per a analítica Big Data, suportat per Spark, Hive, Dask, pandas, DuckDB, AWS Athena, Google BigQuery i pràcticament tota l'eina de dades moderna.
Estructura interna
Un fitxer Parquet s'organitza en tres nivells jeràrquics:
Fitxer Parquet
├── Row Group 1 (128 MB per defecte)
│ ├── Column Chunk: id
│ │ ├── Page 1 (1 MB per defecte)
│ │ ├── Page 2
│ │ └── ...
│ ├── Column Chunk: data
│ └── Column Chunk: import
├── Row Group 2
│ └── ...
└── Footer (estadistiques min/max, schema, offsets)
- Row Group: la unitat de particionament vertical. Un fitxer de 1 GB amb row groups de 128 MB en té 8.
- Column Chunk: tots els valors d'una columna dins d'un row group, contigus al disc.
- Page: la unitat mínima de lectura i compressió (1 MB per defecte).
- Footer: emmagatzema l'esquema complet, les estadístiques (min, max, null count) de cada column chunk, i els offsets per localitzar cada row group. El footer és la clau del predicate pushdown.
Predicate pushdown en detall
Suposem que executem SELECT sum(import) FROM vendes WHERE any = 2023:
- Es llegeix el footer (uns pocs KB).
- Per a cada row group, es consulta l'estadística
min/maxde la columnaany. - Els row groups on
max(any) < 2023omin(any) > 2023s'ometen completament. - Només es llegeix la columna
importdels row groups que poden contenir dades de 2023.
En un dataset de 5 anys on 4/5 dels row groups contenen anys fora del filtre, s'evita llegir el 80% del fitxer.
Llegir i escriure Parquet amb Python
Escriptura bàsica:
import pandas as pd
import pyarrow as pa
import pyarrow.parquet as pq
# Des de pandas
df = pd.read_csv('transaccions.csv', parse_dates=['data'])
df.to_parquet(
'transaccions.parquet',
engine='pyarrow',
compression='snappy', # o 'zstd', 'gzip', None
index=False,
)
# Verificar la mida
import os
mida_csv = os.path.getsize('transaccions.csv')
mida_pq = os.path.getsize('transaccions.parquet')
print(f"CSV: {mida_csv / 1e6:.1f} MB")
print(f"Parquet: {mida_pq / 1e6:.1f} MB")
print(f"Ratio: {mida_csv / mida_pq:.1f}x")
Lectura amb column pruning:
import pandas as pd
# Llegir NOMÉS les columnes necessaries (column pruning)
df = pd.read_parquet(
'transaccions.parquet',
columns=['data', 'import', 'moneda'],
engine='pyarrow',
)
# Llegir amb filtre (predicate pushdown via pyarrow)
import pyarrow.parquet as pq
import pyarrow.compute as pc
taula = pq.read_table(
'transaccions.parquet',
columns=['data', 'import'],
filters=[('moneda', '=', 'EUR'), ('import', '>', 1000)],
)
df = taula.to_pandas()
Esquema i evolució d'esquema
import pyarrow as pa
import pyarrow.parquet as pq
# Definir un esquema explicit
esquema = pa.schema([
pa.field('id', pa.int64()),
pa.field('data', pa.date32()),
pa.field('import', pa.float64()),
pa.field('moneda', pa.string()),
pa.field('estat', pa.string()),
])
# Convertir un DataFrame amb l'esquema
taula = pa.Table.from_pandas(df, schema=esquema)
pq.write_table(taula, 'transaccions_tipat.parquet')
# Inspeccionar l'esquema d'un fitxer existent
metadades = pq.read_metadata('transaccions_tipat.parquet')
print(metadades.schema)
print(f"Row groups: {metadades.num_row_groups}")
print(f"Files: {metadades.num_rows}")
Evolució d'esquema: afegir una nova columna als fitxers nous és compatible si s'usa merge_schema=True en llegir:
# Llegir multiples fitxers Parquet amb esquemes lleugerament diferents
df = pd.read_parquet(
'directori_amb_fitxers/',
engine='pyarrow',
# Si alguns fitxers no tenen la columna 'nova_columna',
# quedara com NaN en comptes de donar error
)
Particionament Parquet
El particionament divideix un dataset gran en directoris organitzats per valors d'una o més columnes. Permet que els motors de consulta ometin directoris sencers sense obrir cap fitxer.
import pandas as pd
df = pd.read_csv('transaccions.csv', parse_dates=['data'])
df['any'] = df['data'].dt.year
df['mes'] = df['data'].dt.month
# Escriure particionat per any i mes
df.to_parquet(
'transaccions_particionat/',
partition_cols=['any', 'mes'],
engine='pyarrow',
)
# Resultat al disc:
# transaccions_particionat/
# any=2023/mes=1/part-0.parquet
# any=2023/mes=2/part-0.parquet
# any=2024/mes=1/part-0.parquet
# Llegir amb filtre de particio (molt rapid, no obre fitxers d'altres particions)
df_2024 = pd.read_parquet(
'transaccions_particionat/',
filters=[('any', '=', 2024)],
)
Cardinalitat de la particio
Particioneu per columnes de baixa cardinalitat (any, mes, pais, categoria). Particioneu per columnes d'alta cardinalitat (id_client, uuid) i obtindreu milions de fitxers minúsculs, cosa que desborda els sistemes de fitxers i degrada el rendiment.
Quan usar Parquet
- Analítica interna al data lake (Spark, Dask, DuckDB, Athena).
- Dades que s'han de llegir moltes vegades pero s'escriuen poques.
- Taules grans on normalment es consulten poques columnes.
- Quan l'evolució d'esquema és necessaria.
Apache ORC
Origen i diferencies amb Parquet
ORC (Optimized Row Columnar) va néixer el 2013 al projecte Hive d'Apache Hadoop, amb l'objectiu de substituir el format RCFile. Com Parquet, és columnar, pero té algunes diferències rellevants:
Estructura interna:
- ORC usa stripes (equivalent als row groups de Parquet) de 250 MB per defecte.
- Cada stripe té un índex integrat (min, max, bloom filter) que Parquet no té nativament.
- El footer de ORC inclou estadístiques globals del fitxer i estadístiques per stripe.
Bloom filters integrats: ORC pot emmagatzemar bloom filters per a columnes seleccionades, cosa que permet descartar stripes per a cerques de valors concrets (p. ex., WHERE id_client = 'X') molt eficientment, fins i tot quan els valors no estan ordenats.
Compressió per defecte: ORC usa Zlib (gzip) per defecte, que dona millors ratios de compressió que Snappy (el defecte de Parquet), a costa de velocitat.
Llegir i escriure ORC amb Python
import pandas as pd
# Escriptura ORC (requereix pyarrow >= 3.0 o orc package)
df.to_orc('transaccions.orc', engine='pyarrow')
# Lectura
df = pd.read_orc('transaccions.orc')
# Amb pyarrow directament
import pyarrow.orc as orc
taula = orc.read_table('transaccions.orc', columns=['import', 'moneda'])
df = taula.to_pandas()
Quan preferir ORC
- Workloads Hive o entorns Hadoop clàssics: ORC és el format natiu de Hive i esta millor integrat.
- Quan es necessiten bloom filters per a cerques de valors específics (ORC els inclou nativament).
- Quan la compressió és crítica i es pot assumir una lectura més lenta (Zlib per defecte).
- En entorns amb ecosistema Cloudera tradicional.
Per a entorns Spark/Databricks, Delta Lake o ecosistemes cloud moderns, Parquet (o Delta Lake sobre Parquet) és generalment preferible.
Benchmark comparatiu: CSV vs Parquet vs ORC
Els valors següents son aproximats per a un dataset de 5 milions de files, 10 columnes (mix de numèrics i categorics), sense optimitzar la configuració:
| Metrica | CSV | CSV + gzip | Parquet (Snappy) | Parquet (Zstd) | ORC (Zlib) |
|---|---|---|---|---|---|
| Mida al disc | 1.200 MB | 280 MB | 95 MB | 70 MB | 80 MB |
| Temps d'escriptura | 8 s | 22 s | 12 s | 15 s | 14 s |
| Temps lectura completa | 6 s | 18 s | 3 s | 4 s | 3.5 s |
| Temps query 2 columnes | 6 s | 18 s | 0.4 s | 0.5 s | 0.5 s |
| Predicate pushdown | No | No | Si | Si | Si |
| Suport de tipus | No | No | Si | Si | Si |
| Schema evolution | No | No | Si | Si | Limitat |
Interpretacio dels resultats
La diferencia mes espectacular es en la "query 2 columnes": el CSV ha de llegir els 1.200 MB sencers per retornar dues columnes, mentre que Parquet llegeix només les columnes chunks rellevants (uns 20 MB). Això explica per que el column pruning pot accelerar les queries en un factor de 15x o mes.
AC5074/06/02 — Miniactivitat
Genera un CSV sintetic d'1 GB de transaccions financeres (usa Faker o numpy per generar les dades) i realitza el seguent:
- Converteix el CSV a Parquet amb compressio Snappy usant pyarrow.
- Compara la mida al disc dels dos fitxers (CSV vs Parquet).
- Mesura el temps d'una query selectiva (suma d'imports per moneda) sobre:
- El CSV amb pandas.
- El Parquet amb pandas (
read_parquet(columns=[...])). - El Parquet amb DuckDB (
duckdb.query("SELECT moneda, SUM(import) FROM 'fitxer.parquet' GROUP BY moneda")). - Documenta els resultats en una taula i explica per que hi ha diferencies de rendiment.
Lliurament: script .py executable que generi el dataset, faci les conversions, executi les queries i mostri la taula de resultats.