Datalager och format: Parquet, Arrow
Kunna välja lagringsformat för stora dataset och förstå kolumnlagring.
Förkunskaper
- EDatapipelines och ETLkrävs
Intuition
Radlagring (CSV, JSON) sparar en rad i taget:
id,namn,alder,poang
1,Ada,15,82
2,Bo,16,95
Kolumnlagring (Parquet, ORC) sparar en kolumn i taget:
id: 1, 2, 3, 4, ...
namn: Ada, Bo, Cim, ...
alder: 15, 16, 15, ...
Tre konsekvenser av den skillnaden:
- Läs bara det du behöver.
SELECT avg(alder)läser en kolumn i stället för hela filen. På breda tabeller är det tio- till hundrafaldig skillnad. - Bättre komprimering. Värden i samma kolumn liknar varandra — samma typ, ofta liknande storlek, ofta upprepningar. Kolumnvis komprimering ger typiskt 5–10× mot CSV.
- Sämre för enskilda rader. Ska du läsa eller ändra rad 4 711 måste alla kolumner hämtas. Kolumnformat är byggda för analys, inte för transaktioner.
Tumregel: CSV för utbyte med människor, Parquet för allt annat som är större än några megabyte.
Formellt
Formaten och vad de är till för:
| Format | Typ | Bra på | Dåligt på |
|---|---|---|---|
| CSV | rad, text | läsbart, går överallt | ingen typning, stort, långsamt |
| JSON/JSONL | rad, text | nästlade strukturer, strömning | ännu större, långsamt |
| Parquet | kolumn, binär | analys, komprimering, schema | inte människoläsbart |
| Arrow | kolumn, i minnet | nollkopiering mellan verktyg | inte ett lagringsformat |
| Delta / Iceberg | Parquet + transaktionslogg | versionering, ACID, tidsresa | mer infrastruktur |
Arrow är inte ett filformat utan en minnesrepresentation. Poängen är att pandas, Polars, DuckDB och Spark kan dela samma minnesblock utan att serialisera — det tar bort en av de största kostnaderna i dataarbete.
Parquets tre lager av smarthet:
| Mekanism | Vad den gör |
|---|---|
| Kolumnpruning | läser bara efterfrågade kolumner |
| Radgruppsstatistik | min/max per block → hoppa över block som inte kan matcha |
| Dictionary encoding | upprepade strängar lagras som heltal + ordlista |
Den andra kallas predicate pushdown och är skälet till att en filtrerad fråga mot en stor Parquet-fil ofta går på bråkdelen av tiden.
Partitionering delar datan i kataloger efter en kolumn:
data/ar=2026/manad=09/dag=20/del-0.parquet
En fråga om september 2026 rör då bara de filerna. Men partitionera inte för fint — tusentals små filer gör allt långsammare (många öppningar, dålig komprimering). Riktmärket är filer på 128 MB–1 GB.
Komprimering:
| Kodek | Storlek | Hastighet |
|---|---|---|
| snappy | större | snabbast — standardvalet |
| zstd | mindre | nästan lika snabb; ofta bäst i dag |
| gzip | minst | långsam |
Kod
import numpy as np, pandas as pd, time
from pathlib import Path
rng = np.random.default_rng(0)
n = 500_000
df = pd.DataFrame({
"id": np.arange(n),
"stad": rng.choice(["Malmö", "Lund", "Göteborg", "Umeå"], n), # få unika → dictionary
"alder": rng.integers(15, 80, n),
"poang": rng.normal(70, 12, n).round(2),
"datum": pd.to_datetime("2026-01-01") + pd.to_timedelta(rng.integers(0, 365, n), "D"),
})
ut = Path("/tmp/format"); ut.mkdir(exist_ok=True)
df.to_csv(ut / "d.csv", index=False)
df.to_parquet(ut / "d.snappy.parquet", compression="snappy", index=False)
df.to_parquet(ut / "d.zstd.parquet", compression="zstd", index=False)
for f in sorted(ut.glob("d.*")):
print(f"{f.name:<20} {f.stat().st_size / 1024**2:>7.2f} MB")
def tid(fn):
t = time.perf_counter(); fn(); return round((time.perf_counter() - t) * 1000)
print("läs allt, csv ", tid(lambda: pd.read_csv(ut / "d.csv")), "ms")
print("läs allt, parquet", tid(lambda: pd.read_parquet(ut / "d.zstd.parquet")), "ms")
print("läs EN kolumn ",
tid(lambda: pd.read_parquet(ut / "d.zstd.parquet", columns=["alder"])), "ms")
# ↑ den sista är dramatiskt snabbare: bara en kolumn läses från disk
# Partitionering
df["ar"] = df["datum"].dt.year
df["manad"] = df["datum"].dt.month
df.to_parquet(ut / "part", partition_cols=["ar", "manad"], index=False)
# En fråga om en månad rör bara den katalogen
del = pd.read_parquet(ut / "part", filters=[("manad", "==", 9)])
print(len(del), "rader i september")
# Predicate pushdown med radgruppsstatistik
hog = pd.read_parquet(ut / "d.zstd.parquet", filters=[("poang", ">", 95)])
print(len(hog), "rader med poäng > 95 — block som inte kan matcha hoppas över")
Kör koden själv: skillnaden mellan «läs allt» och «läs en kolumn» är den enskilt tydligaste demonstrationen av varför kolumnlagring finns.
Behärskning innebär
- Väljer format efter användning
- Förklarar kolumnlagringens fördelar
- Partitionerar och komprimerar medvetet
Logga in för att göra övningarna och bygga upp din behärskning.
Källor
- Apache Parquet — dokumentation (Apache-2.0) — Apache-2.0
- Apache Arrow (Apache-2.0) — Apache-2.0
- pandas — User Guide (BSD-3) — BSD-3-Clause