Analitik mingguan TokoKita menjalankan satu kalimat SQL: rata-rata nilai pesanan per kota. Dari dua belas kolom yang tersimpan di Parquet, query tersebut hanya membutuhkan dua — kota dan nilai — namun pembacaan tanpa pemangkasan tetap membayar semuanya, termasuk kolom catatan berteks bebas yang menempati sepertiga berkas. Chapter Columnar Storage telah menjelaskan mengapa format kolom menyimpan nilai satu kolom bersebelahan sehingga melewati kolom lain hampir gratis. Bab ini mengukur penghematannya di waktu dan memori, membedah projection pushdown di engine, lalu menutup dengan anti-pola yang paling sering membatalkan semuanya: SELECT *.
Peta Byte per Kolom #
Berkas uji berisi dua juta pesanan, dua belas kolom, 30,0 MB. Peta bytenya dibaca murni dari metadata Parquet — setiap kolom di setiap row group mencatat ukuran terkompresinya — tanpa membuka data:
byte_per_kolom = {k: 0 for k in nama_kolom}
for rg in range(md.num_row_groups):
grp = md.row_group(rg)
for c in range(grp.num_columns):
byte_per_kolom[grp.column(c).path_in_schema] += grp.column(c).total_compressed_size
print(f"berkas: {ukuran_file / 1e6:.1f} MB | {N:,} baris | {len(nama_kolom)} kolom")
print("byte terkompresi per kolom (diurutkan):")
for k, v in sorted(byte_per_kolom.items(), key=lambda x: -x[1]):
print(f" {k:<13}{v / 1e6:>7.2f} MB")
dua_kolom = byte_per_kolom["kota"] + byte_per_kolom["nilai"]
print(f"kota + nilai : {dua_kolom / 1e6:.2f} MB ({dua_kolom / ukuran_file * 100:.1f}% "
f"dari total berkas)")Output yang Dihasilkan:
berkas: 30.0 MB | 2,000,000 baris | 12 kolom
byte terkompresi per kolom (diurutkan):
catatan 10.13 MB
id 8.01 MB
pelanggan 6.45 MB
nilai 2.87 MB
diskon 1.51 MB
harga 0.23 MB
produk 0.11 MB
tanggal 0.06 MB
kota 0.04 MB
status 0.04 MB
metode_bayar 0.02 MB
qty 0.02 MB
kota + nilai : 2.90 MB (9.7% dari total berkas)Peta ini mengubah intuisi menjadi angka: tiga kolom teratas (catatan, id, pelanggan) menyita 82% berkas, padahal query rata-rata per kota tidak menyentuh satu pun di antaranya. Dua kolom yang benar-benar dibutuhkan hanya 9,7% — sisanya 90% adalah biaya yang bisa dihindari.
Dua Belas Kolom vs Dua Kolom: Waktu dan Memori #
Dua fungsi pembacaan dibandingkan: satu membaca seluruh berkas, satu membaca hanya dua kolom lewat parameter columns:
def baca_semua():
return pq.read_table(BERKAS)
def baca_dua():
return pq.read_table(BERKAS, columns=["kota", "nilai"])Memori diukur dari delta memori pool pyarrow saat tabel masih hidup, lalu tabel tersebut dikonversi ke pandas untuk melihat biaya di lapisan berikutnya:
def baca_dengan_memori(fungsi):
"""Baca sekali; ukur delta memori pool pyarrow saat tabel masih hidup."""
sebelum = pool.bytes_allocated()
tabel = fungsi()
memori = pool.bytes_allocated() - sebelum
n_baris = tabel.num_rows
df = tabel.to_pandas()
memori_pandas = df.memory_usage(deep=True).sum()
del tabel, df
return memori, memori_pandas, n_baris
mem_semua, mem_semua_pd, n_semua = baca_dengan_memori(baca_semua)
mem_dua, mem_dua_pd, n_dua = baca_dengan_memori(baca_dua)
d_semua, d_dua = ukur(baca_semua), ukur(baca_dua)Output yang Dihasilkan:
=== pyarrow: baca 12 kolom vs 2 kolom ===
baca 12 kolom : 0.315 s | memori tabel 378 MB | pandas 1344 MB | 2,000,000 baris
baca 2 kolom : 0.065 s | memori tabel 61.5 MB | pandas 336.0 MB | 2,000,000 barisMari kita telaah bagian-bagian penting kode di atas:
- Baris 67-68 (
columns=["kota", "nilai"]): satu parameter ini adalah seluruh tekniknya — pyarrow hanya membuka column chunk kedua kolom tersebut dan melompati sepuluh sisanya. - Baris 86-88 (delta memori pool): selisih
bytes_allocated()sebelum dan sesudah pembacaan mengukur memori tabel Arrow yang sesungguhnya dialokasikan, selagi tabel masih hidup di memori. - Baris 90-91 (
memory_usage(deep=True)): konversi ke pandas diukur terpisah karena biayanya berbeda — teks menjadi objek Python yang jauh lebih boros. - Baris 96-98: kedua mode dijalankan dengan fungsi pengukur yang sama, sehingga angkanya bisa dibandingkan langsung.
Hasilnya: waktu turun 4,8 kali (0,315 menjadi 0,065 detik), memori tabel turun 6,1 kali (378 menjadi 61,5 MB), dan memori setelah konversi pandas turun 4 kali (1.344 menjadi 336 MB). Perhatikan lompatan 378 ke 1.344 MB pada mode 12 kolom — konversi ke pandas melipatgandakan biaya memori hampir empat kali — sehingga pada berkas ratusan GB, kolom yang tidak dipangkas bukan sekadar lambat, melainkan bisa membuat proses kehabisan memori.
Projection Pushdown di Engine #
Di engine analitik, pemangkasan kolom terjadi otomatis lewat projection pushdown: optimizer membaca daftar kolom yang dirujuk query, lalu mendorong pemilihan tersebut sampai ke pembacaan berkas. Dua query berikut membandingkan agregasi dua kolom dengan agregasi yang menyentuh sebelas kolom:
Q_2 = (f"SELECT kota, round(avg(nilai), 2) AS rata FROM read_parquet('{BERKAS}') "
"GROUP BY kota ORDER BY kota")
Q_BANYAK = (f"SELECT count(*), sum(qty), avg(nilai), min(id), max(id), "
f"sum(length(pelanggan)), sum(length(produk)), sum(length(status)), "
f"sum(length(metode_bayar)), sum(length(catatan)), sum(diskon) "
f"FROM read_parquet('{BERKAS}')")Output yang Dihasilkan:
=== DuckDB: agregasi 2 kolom vs 11 kolom ===
2 kolom : 0.0225 s -> [('Bandung', 76875.0), ('Denpasar', 77875.0), ('Jakarta', 77125.0), ('Medan', 77625.0), ('Surabaya', 77375.0)]
11 kolom : 0.1679 s
=== Kutipan TABLE_SCAN dari EXPLAIN ANALYZE ===
TABLE_SCAN
────────────────────
Function:
READ_PARQUET
Projections:
kota
nilai
Total Files Read: 1Kartu TABLE_SCAN menuliskan Projections: kota, nilai — bukti dari rencana eksekusi bahwa hanya dua column chunk yang dibaca, meskipun berkas menyimpan dua belas. Selisih 0,0225 versus 0,1679 detik (7,5 kali) murni berasal dari kolom yang ikut dimuat: Q_BANYAK sengaja memanggil fungsi agregasi pada sebelas kolom termasuk catatan, meniru beban query yang ditulis tanpa pertimbangan kolom.
Baris kini bisa dipangkas lewat predicate pushdown, kolom lewat column pruning — keduanya menetapkan apa yang dibaca. Pertanyaan berikutnya menyangkut berapa sekaligus: berapa baris per pernyataan tulis dan berapa besar potongan yang boleh menumpuk di memori. Chapter Batch Size dan Throughput melanjutkan.