Dataset transaksi toko 200.000 baris yang kita pakai berulang kali di section ini berukuran 6,4 MB sebagai CSV. Kalau satu toko menghasilkan data sebanyak itu setiap hari, setahun arsipnya menembus 2 GB — dan itu baru satu toko. Kompresi mengecilkan biaya penyimpanan, tetapi juga mengubah keseimbangan lain: CPU yang dipakai untuk mengompresi dan mendekompresi, serta kecepatan pipeline yang membacanya. Chapter ini membedah dua level kompresi, membandingkan empat codec populer, lalu membuktikan angka-angkanya lewat benchmark nyata dengan pyarrow.

File Level vs Kolom Level #

Kompresi file level memperlakukan seluruh berkas sebagai satu aliran byte — transaksi.csv.gz adalah contohnya. Cara ini sederhana, tetapi pembaca harus mendekompresi semuanya dari awal demi mengambil satu baris. Kompresi kolom level, yang dipakai Parquet dan ORC, mengompresi setiap kolom pada setiap row group secara terpisah. Akibatnya codec bisa memanfaatkan kemiripan nilai dalam satu kolom, dan pembaca cukup mendekompresi kolom yang diminta query.

pyarrow bahkan mengizinkan codec berbeda untuk kolom berbeda dalam satu berkas:

python
import os
import random
import pyarrow as pa
import pyarrow.parquet as pq

random.seed(42)
KASIR = ["Budi", "Siti", "Dewi"]
PRODUK = ["Kopi Robusta", "Teh Melati", "Gula Aren 500g", "Susu UHT 1L",
          "Beras 5kg", "Telur 1kg", "Tepung 1kg", "Sabun Mandi"]
N = 200_000

tabel = pa.table({
    "id": list(range(1, N + 1)),
    "kasir": [random.choice(KASIR) for _ in range(N)],
    "produk": [random.choice(PRODUK) for _ in range(N)],
    "qty": [random.randint(1, 10) for _ in range(N)],
    "harga": [random.randint(1500, 75000) for _ in range(N)],
})

pq.write_table(
    tabel,
    "per_kolom.parquet",
    compression={"id": "NONE", "kasir": "SNAPPY", "produk": "ZSTD",
                 "qty": "NONE", "harga": "SNAPPY"},
)

grup = pq.read_metadata("per_kolom.parquet").row_group(0)
for i in range(grup.num_columns):
    kolom = grup.column(i)
    print(f"{kolom.path_in_schema:8} codec={kolom.compression:10} "
          f"terkompresi={kolom.total_compressed_size:>9,} "
          f"mentah={kolom.total_uncompressed_size:>9,}")
print("ukuran file:", f"{os.path.getsize('per_kolom.parquet'):,}", "byte")

Output yang Dihasilkan:

text
id       codec=UNCOMPRESSED terkompresi=1,871,483 mentah=1,871,483
kasir    codec=SNAPPY     terkompresi=   50,978 mentah=   50,927
produk   codec=ZSTD       terkompresi=   76,216 mentah=   76,124
qty      codec=UNCOMPRESSED terkompresi=  101,276 mentah=  101,276
harga    codec=SNAPPY     terkompresi=  704,717 mentah=  950,712
ukuran file: 2,805,838 byte

Metadata membuktikan codec berbeda bekerja berdampingan: id dan qty dibiarkan tanpa kompresi, kasir dan harga memakai snappy, produk memakai zstd. Dua kejutan menarik terlihat pada output. Kolom harga mengecil dari 950.712 byte menjadi 704.717 byte — snappy bekerja efektif pada angka acak. Sebaliknya kolom kasir justru sedikit membesar (50.978 dari 50.927 byte) karena dictionary encoding sudah menyusutkannya menjadi tiga nilai berulang; lapisan snappy tinggal menambah header blok tanpa materi baru untuk dipadatkan.

Empat Codec yang Sering Dipakai #

CodecRasio terhadap data mentahKecepatanKarakteristik
gzip (DEFLATE)tinggitulis lambat, baca cepatada di semua platform, satu aliran
snappysedangsangat cepat dua arahbawaan Parquet dan banyak sistem Hadoop
zstdtinggicepat, level 1-22 bisa diaturgenerasi baru, proyek open source Facebook
lz4sedangtercepatfavorit untuk streaming dan real-time

gzip bersifat stream: satu kesatuan aliran dari awal sampai akhir. snappy, lz4, dan zstd bekerja per blok — cocok dengan struktur kolom level yang perlu mengakses potongan tertengah berkas tanpa membuka seluruhnya.

Benchmark Nyata: Ukuran dan Waktu #

Sekarang angka sungguhan. Skrip berikut menulis 200.000 transaksi ke CSV (lalu csv.gz) dan ke Parquet dengan lima variasi codec, mengukur ukuran serta waktu tulis-baca:

python
import csv
import gzip
import io
import os
import random
import time

import pyarrow as pa
import pyarrow.parquet as pq

random.seed(42)
KASIR = ["Budi", "Siti", "Dewi"]
PRODUK = ["Kopi Robusta", "Teh Melati", "Gula Aren 500g", "Susu UHT 1L",
          "Beras 5kg", "Telur 1kg", "Tepung 1kg", "Sabun Mandi"]

data = [
    {"id": i + 1, "kasir": random.choice(KASIR), "produk": random.choice(PRODUK),
     "qty": random.randint(1, 10), "harga": random.randint(1500, 75000)}
    for i in range(200_000)
]

# CSV mentah lalu dikompresi gzip di level file
buffer = io.StringIO()
penulis = csv.DictWriter(buffer, fieldnames=["id", "kasir", "produk", "qty", "harga"])
penulis.writeheader()
penulis.writerows(data)
csv_bytes = buffer.getvalue().encode("utf-8")

mulai = time.perf_counter()
with gzip.open("transaksi.csv.gz", "wb", compresslevel=6) as f:
    f.write(csv_bytes)
waktu_tulis_gzip = time.perf_counter() - mulai

print("csv    :", f"{len(csv_bytes):,}", "byte")
print("csv.gz :", f"{os.path.getsize('transaksi.csv.gz'):,}", "byte",
      f"(tulis {waktu_tulis_gzip:.3f}s)")

# Parquet dengan berbagai codec
tabel = pa.table({k: [c[k] for c in data] for k in data[0]})

print()
print(f"{'codec':8} {'ukuran(byte)':>12} {'tulis(s)':>9} {'baca(s)':>9} {'rasio':>6}")
for codec in ["NONE", "SNAPPY", "GZIP", "ZSTD", "LZ4"]:
    jalur = f"transaksi_{codec.lower()}.parquet"
    mulai = time.perf_counter()
    pq.write_table(tabel, jalur, compression=codec)
    waktu_tulis = time.perf_counter() - mulai
    mulai = time.perf_counter()
    pq.read_table(jalur)
    waktu_baca = time.perf_counter() - mulai
    ukuran = os.path.getsize(jalur)
    rasio = len(csv_bytes) / ukuran
    print(f"{codec:8} {ukuran:12,d} {waktu_tulis:9.3f} {waktu_baca:9.3f} {rasio:6.2f}")

# gzip pyarrow: level bawaan vs level 6
mulai = time.perf_counter()
pq.write_table(tabel, "transaksi_gzip6.parquet", compression="GZIP", compression_level=6)
waktu_tulis = time.perf_counter() - mulai
print()
print("GZIP level 6 :", f"{os.path.getsize('transaksi_gzip6.parquet'):,}", "byte",
      f"(tulis {waktu_tulis:.3f}s)")

Output yang Dihasilkan:

text
csv    : 6,434,514 byte
csv.gz : 1,567,001 byte (tulis 0.229s)

codec    ukuran(byte)  tulis(s)   baca(s)  rasio
NONE        3,051,427     0.040     0.016   2.11
SNAPPY      2,006,229     0.039     0.010   3.21
GZIP        1,346,966     1.967     0.014   4.78
ZSTD        1,234,239     0.049     0.011   5.21
LZ4         1,989,175     0.040     0.009   3.23

GZIP level 6 : 1,350,086 byte (tulis 0.203s)

Mari kita telaah baris-baris kode di atas:

  1. Baris 16-20 (data = [...]): membangun 200.000 transaksi acak dengan random.seed(42) sehingga setiap eksekusi menghasilkan dataset yang sama persis — syarat perbandingan yang adil.
  2. Baris 22-27 (DictWriter): menulis CSV lengkap ke buffer memori, lalu mengubahnya menjadi byte UTF-8 sebagai tolok ukur ukuran mentah.
  3. Baris 29-32 (gzip.open): mengompresi seluruh CSV di level file dengan compresslevel=6 sambil mengukur waktu tulisnya.
  4. Baris 38 (pa.table({...})): membangun table Arrow dari daftar baris lewat dictionary comprehension — setiap kunci menjadi satu kolom.
  5. Baris 41-52 (loop codec): untuk tiap codec, skrip menulis berkas Parquet, membacanya kembali, lalu mencatat ukuran dan waktu; kolom rasio membagi ukuran CSV mentah dengan ukuran Parquet.
  6. Baris 54-60 (GZIP level 6): percobaan penutup yang menyetel compression_level=6 eksplisit untuk dibandingkan dengan level bawaan pyarrow.

Empat pengamatan dari angka di atas. Pertama, Parquet tanpa kompresi (NONE, 3.051.427 byte) sudah separuh ukuran CSV berkat encoding kolom — kompresi hanya melanjutkan pekerjaan encoding. Kedua, zstd keluar sebagai pemenang seimbang: rasio terbaik 5,21 dengan waktu tulis 0,049 detik, hampir menyamai snappy. Ketiga, gzip bawaan pyarrow tampak jauh lebih lambat menulis (1,967 detik) karena level kompresinya agresif; baris terakhir membuktikan compression_level=6 memangkasnya menjadi 0,203 detik dengan ukuran yang nyaris sama (1.350.086 versus 1.346.966 byte). Keempat, csv.gz (1.567.001 byte) kalah dari Parquet+zstd meski keduanya memadatkan data yang sama — encoding kolom memang memberi modal awal.

Angka-angka ini spesifik untuk dataset dan mesin pengujian; angka Anda pasti berbeda, tetapi pola relatifnya (zstd unggul seimbang, gzip default lambat menulis, lz4 tercepat) umumnya bertahan.

Memilih Codec untuk Pipeline #

Pilihan codec adalah tawar-menawar antara CPU dan I/O. Pipeline yang dibatasi jaringan atau biaya penyimpanan — batch malam, arsip bertahun, data antar-region — untung memakai zstd: CPU ekstra terbayar oleh transfer yang mengecil. Pipeline interaktif dan streaming yang sensitif terhadap latensi lebih cocok snappy atau lz4 karena waktu tulis-baca minim, meski berkasnya lebih besar.

Kita kini memegang semua keping: format baris dan kolom, skema dan evolusinya, serta kompresi. Chapter penutup section ini menyusun keping-keping itu menjadi kerangka keputusan: Memilih Format Data.