Schema sempit vs lebar

Satu pengukuran per baris vs banyak kolom, dengan tradeoff yang disampaikan jujur.

Daftar isi

Schema adalah bentuk fisik data time-series saat disimpan. Topik ini membandingkan dua bentuk, yaitu schema sempit (narrow) dengan satu pengukuran per baris dan schema lebar (wide) dengan banyak kolom terukur. Setelah membaca, keputusan bentuk tidak lagi diambil karena kebetulan, melainkan karena beban yang dipikulnya dapat disebutkan secara eksplisit.

Dua bentuk yang sama sekali berbeda#

Schema sempit menyimpan satu pasangan nama dan nilai pengukuran pada setiap baris, sehingga sebuah stasiun cuaca yang melaporkan suhu, kelembapan, dan tekanan pada cap waktu yang sama menghasilkan tiga baris terpisah. Kolom pada schema ini biasanya terdiri atas cap waktu, identitas seri seperti station_id, nama pengukuran, dan nilai. Schema lebar menyimpan seluruh pengukuran pada satu cap waktu sebagai satu baris, sehingga stasiun yang sama menghasilkan satu baris dengan tiga kolom nilai yang berbeda. Pilihan ini tampak sekadar rotasi tabel, tetapi keduanya menyiratkan beban penyimpanan dan pola query yang berbeda.

Schema sempit unggul ketika daftar pengukuran berubah seiring waktu. Penambahan metrik tekanan cukup berupa baris baru dengan metric='tekanan', tanpa perubahan struktur. Schema lebar unggul ketika daftar pengukuran tetap dan padat, sebab satu baris menyimpan banyak nilai sekaligus, sehingga pemindaian query rentang waktu menjadi lebih efisien. Schema sempit menambah tiga kali lipat baris pada contoh di atas, sedangkan schema lebar menambah kolom. Banyaknya baris memengaruhi ukuran indeks dan kompresi, sedangkan banyaknya kolom memengaruhi lebar chunk dan kelincahan query agregat.

Konversi antar schema dengan pandas#

pandas menyediakan operasi melt untuk membongkar schema lebar menjadi sempit dan pivot untuk sebaliknya. Skrip berikut membangkitkan empat baris schema lebar dari dua stasiun cuaca, lalu mengubahnya menjadi schema sempit dan kembali ke schema lebar untuk memverifikasi bahwa konversi bersifat reversibel. Skrip juga mencatat dimensi tiap bentuk, sehingga perbedaan jumlah baris dan kolom menjadi nyata.

python sempit_lebar.py
"""Perbandingan schema sempit vs lebar pada data time-series.

Skrip membangkitkan telemetri stasiun cuaca dalam bentuk schema lebar,
lalu mengubahnya menjadi schema sempit dengan operasi melt dan kembali
ke schema lebar dengan pivot. Tujuannya menunjukkan bahwa kedua bentuk
adalah rotasi tabel yang sama, tetapi menyiratkan beban penyimpanan dan
pola query yang berbeda.
"""
from __future__ import annotations

import pandas as pd

STASIUN = ["STA01", "STA02"]
CAP_WAKTU = pd.to_datetime([
    "2026-11-01 00:00:00",
    "2026-11-01 00:15:00",
])


def bangun_lebar() -> pd.DataFrame:
    return pd.DataFrame({
        "ts": list(CAP_WAKTU) * len(STASIUN),
        "station_id": [s for s in STASIUN for _ in CAP_WAKTU],
        "suhu_c": [24.1, 24.4, 22.8, 23.0],
        "kelembapan": [62.0, 61.5, 70.2, 69.8],
        "tekanan_hpa": [1013.2, 1013.1, 1012.8, 1012.9],
    })


def lebar_ke_sempit(df: pd.DataFrame) -> pd.DataFrame:
    return df.melt(
        id_vars=["ts", "station_id"],
        var_name="metric",
        value_name="nilai",
    )


def sempit_ke_lebar(df: pd.DataFrame) -> pd.DataFrame:
    return df.pivot_table(
        index=["ts", "station_id"],
        columns="metric",
        values="nilai",
    ).reset_index()


def main() -> None:
    lebar = bangun_lebar()
    sempit = lebar_ke_sempit(lebar)
    lebar_lagi = sempit_ke_lebar(sempit)

    print("schema lebar (1 baris per stasiun per cap waktu):")
    print(lebar.to_string(index=False))
    print()
    print(f"  dimensi: {lebar.shape[0]} baris x {lebar.shape[1]} kolom")
    print()

    print("schema sempit (1 baris per pengukuran):")
    print(sempit.to_string(index=False))
    print()
    print(f"  dimensi: {sempit.shape[0]} baris x {sempit.shape[1]} kolom")
    print()

    print("schema lebar hasil konversi ulang:")
    lebar_lagi.columns.name = None
    print(lebar_lagi.to_string(index=False))
    print()
    print(f"  dimensi: {lebar_lagi.shape[0]} baris x {lebar_lagi.shape[1]} kolom")


if __name__ == "__main__":
    main()
text Keluaran
schema lebar (1 baris per stasiun per cap waktu):
                 ts station_id  suhu_c  kelembapan  tekanan_hpa
2026-11-01 00:00:00      STA01    24.1        62.0       1013.2
2026-11-01 00:15:00      STA01    24.4        61.5       1013.1
2026-11-01 00:00:00      STA02    22.8        70.2       1012.8
2026-11-01 00:15:00      STA02    23.0        69.8       1012.9

  dimensi: 4 baris x 5 kolom

schema sempit (1 baris per pengukuran):
                 ts station_id      metric  nilai
2026-11-01 00:00:00      STA01      suhu_c   24.1
2026-11-01 00:15:00      STA01      suhu_c   24.4
2026-11-01 00:00:00      STA02      suhu_c   22.8
2026-11-01 00:15:00      STA02      suhu_c   23.0
2026-11-01 00:00:00      STA01  kelembapan   62.0
2026-11-01 00:15:00      STA01  kelembapan   61.5
2026-11-01 00:00:00      STA02  kelembapan   70.2
2026-11-01 00:15:00      STA02  kelembapan   69.8
2026-11-01 00:00:00      STA01 tekanan_hpa 1013.2
2026-11-01 00:15:00      STA01 tekanan_hpa 1013.1
2026-11-01 00:00:00      STA02 tekanan_hpa 1012.8
2026-11-01 00:15:00      STA02 tekanan_hpa 1012.9

  dimensi: 12 baris x 4 kolom

schema lebar hasil konversi ulang:
                 ts station_id  kelembapan  suhu_c  tekanan_hpa
2026-11-01 00:00:00      STA01        62.0    24.1       1013.2
2026-11-01 00:00:00      STA02        70.2    22.8       1012.8
2026-11-01 00:15:00      STA01        61.5    24.4       1013.1
2026-11-01 00:15:00      STA02        69.8    23.0       1012.9

  dimensi: 4 baris x 5 kolom

Keluaran di atas memperlihatkan tiga hal. Pertama, schema lebar memuat empat baris dengan lima kolom, sedangkan schema sempit memuat dua belas baris dengan empat kolom. Jumlah baris sempit adalah jumlah baris lebar dikalikan banyaknya metrik. Kedua, konversi ulang menghasilkan dimensi yang sama, yaitu empat baris dan lima kolom, sehingga tidak ada kehilangan informasi pada konversi bolak-balik. Ketiga, urutan kolom pada schema lebar hasil konversi berubah menjadi urutan alfabetis, sebab pivot_table mengurutkan kolom secara default. Perilaku ini wajar pada pandas, tetapi pada basis data produksi urutan kolom ditentukan oleh schema yang didefinisikan saat CREATE TABLE.

Tradeoff yang disampaikan jujur#

Pilihan antara dua schema tidak bebas biaya. Schema sempit menambah jumlah baris sebanyak banyaknya metrik, sehingga indeks pada cap waktu tumbuh lebih cepat, demikian juga ukuran chunk. Sebaliknya, schema lebar memerlukan daftar kolom yang stabil, sehingga menambah metrik baru berarti mengubah schema, yang pada tabel berisi miliaran baris bukanlah operasi murah. Beberapa basis data time-series mengakomodasi perubahan kolom dengan operasi online schema change, tetapi biayanya tetap ada, yaitu penulisan ulang tabel di latar belakang.

Pola query juga menentukan pilihan. Bila sebagian besar query meminta satu metrik pada banyak seri, misalnya “suhu semua stasiun”, schema sempit memerlukan pemindaian dengan filter metric='suhu_c', sedangkan schema lebar membaca satu kolom saja. Bila query meminta banyak metrik untuk satu seri, misalnya “suhu, kelembapan, dan tekanan STA01”, schema lebar membaca satu baris, sedangkan schema sempit membaca tiga. Pemilihan schema sebaiknya mengikuti pola query yang dominan, bukan dugaan tentang masa depan.

Sebagian besar basis data time-series modern mengadopsi pendekatan hibrida. Schema utama disusun lebar untuk metrik yang stabil dan sering dibaca bersamaan, sedangkan metrik yang jarang digunakan atau baru ditambahkan disimpan pada schema sempit terpisah. Pemisahan ini menjaga tabel utama tetap ramping sekaligus memberi ruang untuk pertumbuhan tanpa perubahan schema. Pada PostgreSQL dengan TimescaleDB, pemisahan semacam ini diterapkan dengan dua hypertable berbeda yang berbagi cap waktu dan identitas seri, sehingga query antar-tabel tetap dapat digabung dengan JOIN.

Kesalahan umum#

  • Memilih schema lebar karena mirip dengan tabel operasional biasa, lalu menambah kolom setiap kali sensor baru dipasang. Akibatnya tabel memiliki ratusan kolom yang sebagian besar kosong.
  • Menggunakan schema sempit tanpa indeks gabungan pada (ts, station_id, metric). Query rentang waktu menjadi pemindaian penuh, padahal indeks itulah yang membuat schema sempit layak.
  • Menyimpan satuan sebagai bagian dari nama metrik, misalnya suhu_c dan suhu_f sebagai dua metrik berbeda. Satuan sebaiknya menjadi metadata tetap, bukan dimensi yang menggandakan jumlah baris.

Ringkasan#

  • Schema sempit menyimpan satu pengukuran per baris dan unggul ketika daftar metrik berubah, sedangkan schema lebar menyimpan banyak kolom per baris dan unggul ketika daftar metrik tetap.
  • Konversi antara dua bentuk dengan melt dan pivot bersifat reversibel pada pandas, tetapi pada basis data produksi perubahan schema tidak gratis.
  • Pemilihan schema sebaiknya mengikuti pola query yang dominan, sebab pemindaian satu kolom berbeda dari pemindaian satu metrik pada banyak baris.

Referensi#