Aplikasi nyata hampir selalu menyimpan data persisten — daftar pengguna, transaksi, konfigurasi, dan log. Relational database (RDBMS) seperti PostgreSQL, MySQL, dan SQLite menjadi pilihan utama karena menjamin struktur data yang konsisten lewat skema tabel dan relasi antar tabel. Pada chapter ini kita akan memetakan konsep inti RDBMS — table, primary key, foreign key, normalisasi 1NF hingga 3NF, serta index — sebelum mulai menulis kode akses database di bab berikutnya. Pemahaman konsep ini menjadi fondasi untuk menulis SQL yang benar dan memilih struktur skema yang sehat.

Table, Column, dan Row #

Sebuah table dalam RDBMS menyimpan data berbentuk baris (row) dan kolom (column). Setiap kolom memiliki nama dan tipe data, misalnya TEXT, INTEGER, atau DATE. Setiap baris mewakili satu entitas data — satu produk, satu pengguna, satu pesanan. Skema tabel didefinisikan dengan pernyataan CREATE TABLE.

Contoh definisi tabel sederhana:

sql Skema tabel produk
CREATE TABLE produk (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    nama TEXT NOT NULL,
    harga INTEGER NOT NULL
);

Tabel di atas punya tiga kolom: id (primary key yang diisi otomatis oleh database), nama (teks, wajib diisi), dan harga (bilangan bulat, wajib diisi). Setiap kali kita INSERT INTO produk(nama, harga) VALUES ('Kopi', 18000), database menambahkan satu baris baru dengan id unik.

Primary Key dan Foreign Key #

Primary key adalah kolom (atau kombinasi kolom) yang nilainya unik untuk setiap baris dan tidak boleh NULL. Kolom ini menjadi pengenal resmi baris tersebut. Foreign key adalah kolom di satu tabel yang merujuk ke primary key di tabel lain, sehingga membentuk relasi antar tabel.

Misalnya, tabel pesanan memiliki foreign key pelanggan_id yang merujuk ke pelanggan.id:

sql Foreign key antar tabel
CREATE TABLE pesanan (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    pelanggan_id INTEGER NOT NULL,
    tanggal DATE NOT NULL,
    FOREIGN KEY (pelanggan_id) REFERENCES pelanggan(id)
);

Foreign key mencegah kita menyisipkan baris pesanan dengan pelanggan_id yang tidak ada di tabel pelanggan. Database menolak operasi yang melanggar referential integrity ini.

Normalisasi 1NF-3NF #

Normalisasi adalah proses menyusun ulang skema agar mengurangi duplikasi data dan menjaga konsistensi. Tiga bentuk normal pertama biasa dijumpai di aplikasi nyata.

  • 1NF (First Normal Form): setiap kolom berisi nilai atomik (tidak terbagi), tidak ada grup berulang. Kolom seperti produk1_nama, produk2_nama di satu tabel melanggar 1NF.
  • 2NF: tidak ada partial dependency — setiap kolom non-key bergantung penuh pada seluruh primary key, bukan sebagian. Berlaku terutama untuk primary key gabungan.
  • 3NF: tidak ada transitive dependency — kolom non-key tidak bergantung pada kolom non-key lain. Misalnya, nama_pelanggan seharusnya tidak disimpan di tabel pesanan karena bergantung pada pelanggan_id, bukan pada pesanan.id.

Skema yang sudah dinormalisasi 3NF menghindari anomali update: ketika email pelanggan berubah, kita cukup memperbarui satu baris di tabel pelanggan, tidak perlu menyentuh tabel pesanan.

Index untuk Akses Cepat #

Tanpa indeks, pencarian SELECT * FROM produk WHERE nama = 'Kopi' memerlukan full table scan — database membaca setiap baris. Indks adalah struktur data tambahan (biasanya B-tree) yang mempercepat pencarian berdasarkan kolom tertentu.

sql Membuat index pada kolom
CREATE INDEX idx_produk_nama ON produk(nama);
CREATE UNIQUE INDEX idx_pengguna_email ON pengguna(email);

Indeks mempercepat WHERE, JOIN, dan ORDER BY, tetapi memperlambat INSERT/UPDATE/DELETE karena struktur indeks juga harus diperbarui. Indeks sebaiknya dibuat pada kolom yang sering difilter atau diurutkan, bukan pada setiap kolom.

Demonstrasikan Konsep Skema dengan Kotlin #

Kode berikut memodelkan struktur skema relasional sebagai data class Kotlin agar konsep table, column, primary key, foreign key, dan validasi relasi terlihat eksplisit. Kode ini menjalankan validasi foreign key terhadap skema contoh aplikasi pesanan.

kotlin
// relational_database_fundamentals.kt
// Mendemonstrasikan konsep relational database: tables, columns, rows,
// primary key, foreign key, dan normalization. Kode ini memodelkan struktur
// skema sebagai data class Kotlin untuk menunjukkan pemetaan konsep tanpa
// memerlukan DB server nyata.

// Column definition: nama + tipe + constraint
data class Column(
    val name: String,
    val type: String,
    val primaryKey: Boolean = false,
    val foreignKey: String? = null,  // "table.column" reference
    val nullable: Boolean = !primaryKey && foreignKey == null,
    val unique: Boolean = false
)

// Table definition: nama + daftar kolom
data class Table(val name: String, val columns: List<Column>) {
    fun primaryKey(): List<Column> = columns.filter { it.primaryKey }
    fun foreignKeys(): List<Column> = columns.filter { it.foreignKey != null }

    fun ddl(): String {
        val cols = columns.joinToString(",\n  ") { c ->
            val parts = mutableListOf(c.name, c.type)
            if (c.primaryKey) parts.add("PRIMARY KEY")
            if (c.unique) parts.add("UNIQUE")
            if (!c.nullable && !c.primaryKey) parts.add("NOT NULL")
            if (c.foreignKey != null) parts.add("REFERENCES ${c.foreignKey}")
            parts.joinToString(" ")
        }
        return "CREATE TABLE $name (\n  $cols\n);"
    }
}

// Schema: kumpulan tabel
class Schema {
    private val tables = mutableListOf<Table>()

    fun add(table: Table) { tables.add(table) }
    fun all(): List<Table> = tables.toList()

    // Validasi: foreign key harus merujuk tabel+kolom yang ada
    fun validate(): List<String> {
        val errors = mutableListOf<String>()
        val tableNames = tables.map { it.name }.toSet()
        val pkMap = tables.associate { t ->
            t.name to t.primaryKey().map { it.name }.toSet()
        }
        for (t in tables) {
            for (fk in t.foreignKeys()) {
                val (refTable, refCol) = fk.foreignKey!!.split(".", limit = 2)
                if (refTable !in tableNames) {
                    errors.add("${t.name}.${fk.name}: foreign key -> tabel '$refTable' tidak ditemukan")
                    continue
                }
                if (refCol !in (pkMap[refTable] ?: emptySet())) {
                    errors.add("${t.name}.${fk.name}: foreign key -> '$refTable.$refCol' bukan primary key")
                }
            }
        }
        return errors
    }
}

// Skema contoh: aplikasi pesanan (denormalis -> normalized)
fun buildNormalizedSchema(): Schema {
    val s = Schema()
    // Tabel pelanggan
    s.add(Table("pelanggan", listOf(
        Column("id", "INT", primaryKey = true),
        Column("nama", "VARCHAR(100)", nullable = false),
        Column("email", "VARCHAR(100)", unique = true, nullable = false)
    )))
    // Tabel produk
    s.add(Table("produk", listOf(
        Column("id", "INT", primaryKey = true),
        Column("nama", "VARCHAR(100)", nullable = false),
        Column("harga", "INT", nullable = false)
    )))
    // Tabel pesanan: FK ke pelanggan
    s.add(Table("pesanan", listOf(
        Column("id", "INT", primaryKey = true),
        Column("pelanggan_id", "INT", foreignKey = "pelanggan.id", nullable = false),
        Column("tanggal", "DATE", nullable = false)
    )))
    // Tabel pesanan_item: junction table (many-to-many antara pesanan dan produk)
    s.add(Table("pesanan_item", listOf(
        Column("pesanan_id", "INT", foreignKey = "pesanan.id", nullable = false),
        Column("produk_id", "INT", foreignKey = "produk.id", nullable = false),
        Column("jumlah", "INT", nullable = false),
        Column("id", "INT", primaryKey = true)
    )))
    return s
}

// Skema denormalis (anti-pattern: data berulang)
fun buildDenormalizedSchema(): Table {
    return Table("pesanan_flat", listOf(
        Column("id", "INT", primaryKey = true),
        Column("pelanggan_nama", "VARCHAR(100)", nullable = false),
        Column("pelanggan_email", "VARCHAR(100)", nullable = false),
        Column("produk1_nama", "VARCHAR(100)"),
        Column("produk1_harga", "INT"),
        Column("produk2_nama", "VARCHAR(100)"),
        Column("produk2_harga", "INT")
    ))
}

fun main() {
    println("=== Skema denormalis (anti-pattern) ===")
    val denorm = buildDenormalizedSchema()
    println(denorm.ddl())
    println("\nMasalah: nama pelanggan diulang per pesanan, batas 2 produk per baris.")

    println("\n=== Skema normalized (1NF, 2NF, 3NF) ===")
    val schema = buildNormalizedSchema()
    schema.all().forEach { t ->
        println("--- Tabel ${t.name} ---")
        println(t.ddl())
        println()
    }

    println("=== Foreign key validation ===")
    val errors = schema.validate()
    if (errors.isEmpty()) {
        println("OK: semua foreign key valid")
    } else {
        errors.forEach { println("  ERROR: $it") }
    }

    println("\n=== Normal forms ===")
    println("1NF: kolom atomic, tidak ada grup berulang (produk1, produk2 -> tabel terpisah)")
    println("2NF: tidak ada partial dependency (harga produk tidak tergantung sebagian pesanan)")
    println("3NF: tidak ada transitive dependency (nama pelanggan tidak tergantung tanggal pesanan)")
}

Output yang Dihasilkan:

text
=== Skema denormalis (anti-pattern) ===
CREATE TABLE pesanan_flat (
  id INT PRIMARY KEY,
  pelanggan_nama VARCHAR(100) NOT NULL,
  pelanggan_email VARCHAR(100) NOT NULL,
  produk1_nama VARCHAR(100),
  produk1_harga INT,
  produk2_nama VARCHAR(100),
  produk2_harga INT
);

Masalah: nama pelanggan diulang per pesanan, batas 2 produk per baris.

=== Skema normalized (1NF, 2NF, 3NF) ===
--- Tabel pelanggan ---
CREATE TABLE pelanggan (
  id INT PRIMARY KEY,
  nama VARCHAR(100) NOT NULL,
  email VARCHAR(100) UNIQUE NOT NULL
);

--- Tabel produk ---
CREATE TABLE produk (
  id INT PRIMARY KEY,
  nama VARCHAR(100) NOT NULL,
  harga INT NOT NULL
);

--- Tabel pesanan ---
CREATE TABLE pesanan (
  id INT PRIMARY KEY,
  pelanggan_id INT NOT NULL REFERENCES pelanggan.id,
  tanggal DATE NOT NULL
);

--- Tabel pesanan_item ---
CREATE TABLE pesanan_item (
  pesanan_id INT NOT NULL REFERENCES pesanan.id,
  produk_id INT NOT NULL REFERENCES produk.id,
  jumlah INT NOT NULL,
  id INT PRIMARY KEY
);

=== Foreign key validation ===
OK: semua foreign key valid

=== Normal forms ===
1NF: kolom atomic, tidak ada grup berulang (produk1, produk2 -> tabel terpisah)
2NF: tidak ada partial dependency (harga produk tidak tergantung sebagian pesanan)
3NF: tidak ada transitive dependency (nama pelanggan tidak tergantung tanggal pesanan)

Kode di atas memetakan column dan table menjadi data class Kotlin dengan batasan seperti primaryKey dan foreignKey. Fungsi Schema.validate() mensimulasikan pemeriksaan referential integrity yang biasanya dilakukan database sendiri: setiap foreign key harus merujuk ke primary key tabel yang ada. Table pesanan_item menjadi junction table untuk relasi many-to-many antara pesanan dan produk.

Referensi Lanjutan #

Ringkasan dan Langkah Berikutnya #

Kita sudah melihat bahwa skema relasional terdiri dari tabel berbatasan kolom, dengan primary key sebagai identitas baris dan foreign key sebagai jembatan antar tabel. Normalisasi 1NF hingga 3NF mengurangi duplikasi data dan anomali update, sementara indeks mempercepat akses baca dengan biaya tulis yang lebih mahal. Pada chapter berikutnya, JDBC Fundamentals, kita akan mulai mengakses database langsung dari kode Kotlin melalui API JDBC standar Java.