Mengapa Skema Awal Menentukan Segalanya

Ada satu kebenaran pahit dalam pengembangan sistem berbasis data: refactor skema tabel di production itu mahal, lambat, dan berisiko. ALTER TABLE pada tabel dengan 50 juta baris di MySQL bisa memakan waktu berjam-jam dan mengunci tabel. Migrasi data yang salah bisa menyebabkan kehilangan data permanen.

Bukan berarti kita harus merancang skema sempurna sebelum satu baris kode pun ditulis — over-engineering di awal juga berbahaya. Yang diperlukan adalah kebiasaan dasar yang membuat skema mudah tumbuh tanpa perlu dibongkar. Itulah yang akan kita bahas melalui checklist ini.

ℹ️
Checklist ini dirancang untuk MySQL 8.0+ dan PostgreSQL 14+. Beberapa sintaks berbeda di antara keduanya — catatan akan diberikan di tempat yang relevan.

Penamaan yang Konsisten dan Prediktabel

Penamaan yang konsisten adalah bentuk dokumentasi paling murah. Developer baru harus bisa menebak nama kolom hanya dari konteksnya, tanpa perlu membaca dokumentasi.

Aturan yang disarankan

SQL — Perbandingan penamaan
-- ✗ Hindari: inkonsisten, ambigu, singkatan berlebihan
CREATE TABLE tbl_User (
  UserID   INT,
  Uname    VARCHAR(50),
  stat     TINYINT,
  Dt_Crt   DATETIME
);

-- ✓ Lebih baik: konsisten, deskriptif, mudah ditebak
CREATE TABLE users (
  id            BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  username      VARCHAR(80)  NOT NULL UNIQUE,
  is_active     TINYINT(1)  NOT NULL DEFAULT 1,
  created_at    TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP
);

Primary Key: Surrogate vs Natural

Natural key adalah kolom dengan makna bisnis yang unik: NIM mahasiswa, NIK penduduk, kode produk. Surrogate key adalah kolom buatan tanpa makna bisnis, biasanya AUTO_INCREMENT atau UUID.

Rekomendasi umum: selalu gunakan surrogate key sebagai PK, dan jadikan natural key sebagai UNIQUE constraint. Natural key sering berubah (NIM bisa di-reformat, kode produk bisa di-rename), dan perubahan PK yang direferensikan oleh FK bisa menjadi operasi yang sangat mahal.

BIGINT vs INT vs UUID

SQL — Surrogate + Natural key yang benar
CREATE TABLE mahasiswa (
  id            BIGINT UNSIGNED  AUTO_INCREMENT PRIMARY KEY,
  nim           VARCHAR(12)      NOT NULL UNIQUE,  -- natural key jadi UNIQUE, bukan PK
  nama          VARCHAR(255)     NOT NULL,
  program_studi VARCHAR(100)     NOT NULL,
  created_at    TIMESTAMP        NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at    TIMESTAMP        NOT NULL DEFAULT CURRENT_TIMESTAMP
                                   ON UPDATE CURRENT_TIMESTAMP
);

Pilih Tipe Data Paling Kecil yang Cukup

Tipe data yang tepat bukan hanya soal validasi — ia langsung mempengaruhi ukuran penyimpanan, kecepatan perbandingan, dan efisiensi index. Beberapa panduan kritis:

SQL — Contoh tipe data yang tepat
CREATE TABLE produk (
  id          BIGINT UNSIGNED  AUTO_INCREMENT PRIMARY KEY,
  nama        VARCHAR(255)     NOT NULL,
  sku         VARCHAR(50)      NOT NULL UNIQUE,
  harga       DECIMAL(15, 2)  NOT NULL,          -- bukan FLOAT
  stok        INT UNSIGNED     NOT NULL DEFAULT 0,
  status      VARCHAR(20)      NOT NULL DEFAULT 'aktif', -- bukan ENUM
  deskripsi   TEXT,                                  -- nullable, panjang tidak terbatas
  is_featured TINYINT(1)      NOT NULL DEFAULT 0
);
⚠️
Hindari kolom nullable berlebihan. Kolom NULL mempersulit query (harus selalu mempertimbangkan IS NULL / IS NOT NULL), dan beberapa agregasi fungsi mengabaikan NULL secara senyap. Jadikan kolom NOT NULL kecuali NULL memang semantis valid.

Constraint sebagai Dokumentasi Aktif

Constraint adalah aturan bisnis yang di-enforce langsung oleh database engine — bukan hanya di lapisan aplikasi. Ini adalah "dokumentasi yang tidak bisa basi" karena database akan menolak data yang melanggar aturan, tanpa perlu kode validasi tambahan.

SQL — Constraint lengkap pada tabel pesanan
CREATE TABLE pesanan (
  id          BIGINT UNSIGNED  AUTO_INCREMENT PRIMARY KEY,
  user_id     BIGINT UNSIGNED  NOT NULL,
  total       DECIMAL(15, 2)  NOT NULL
                CHECK (total >= 0),                -- MySQL 8.0.16+
  status      VARCHAR(20)      NOT NULL DEFAULT 'pending'
                CHECK (status IN ('pending', 'proses', 'selesai', 'dibatalkan')),
  created_at  TIMESTAMP        NOT NULL DEFAULT CURRENT_TIMESTAMP,
  CONSTRAINT fk_pesanan_user
    FOREIGN KEY (user_id) REFERENCES users(id)
    ON DELETE RESTRICT    -- cegah hapus user yang punya pesanan
    ON UPDATE CASCADE
);
💡
Beri nama eksplisit pada setiap constraint (CONSTRAINT fk_pesanan_user). Tanpa nama, MySQL akan membuat nama random yang sulit di-debug saat error muncul di log.

Normalisasi sampai 3NF — tapi Tahu Kapan Berhenti

Normal Form adalah kumpulan aturan untuk menghilangkan redundansi data. Tiga level pertama cukup untuk hampir semua OLTP (Online Transaction Processing):

⚠️
Tahu kapan berhenti. Untuk reporting table, data warehouse, atau tabel yang sangat sering di-read namun jarang di-write, denormalisasi yang disengaja (menyimpan data redundan) bisa meningkatkan performa query secara drastis. Jangan normalisasi membabi buta tanpa mempertimbangkan pola akses data.

Timestamp Standar di Setiap Tabel

Dua kolom ini wajib ada di setiap tabel produski: created_at dan updated_at. Biaya storage-nya sangat kecil dibanding manfaat auditability-nya yang besar — kapan data dibuat, kapan terakhir diubah.

SQL — Timestamp otomatis di MySQL
CREATE TABLE contoh_tabel (
  id          BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  -- ... kolom lain ...
  created_at  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
                         ON UPDATE CURRENT_TIMESTAMP
);

-- Untuk audit yang lebih lengkap, tambahkan:
  created_by  BIGINT UNSIGNED NULL,  -- user_id yang membuat
  updated_by  BIGINT UNSIGNED NULL   -- user_id yang terakhir mengubah
💡
Selalu simpan waktu dalam UTC di database. Konversi ke zona waktu user dilakukan di lapisan aplikasi atau saat query. Menyimpan waktu lokal tanpa info timezone adalah sumber bug yang sulit di-debug, terutama menjelang Daylight Saving Time.

Soft Delete dengan deleted_at

Hard delete (DELETE query betulan) membuat data hilang permanen — tidak bisa audit, tidak bisa pulihkan. Soft delete menandai baris sebagai "terhapus" dengan kolom timestamp sehingga data tetap ada di database dan bisa dipulihkan.

SQL — Implementasi soft delete
-- Tambahkan kolom ini ke tabel yang butuh soft delete
  deleted_at  TIMESTAMP NULL DEFAULT NULL,  -- NULL = aktif, timestamp = terhapus

-- Query data aktif (paling sering dipakai)
SELECT * FROM users WHERE deleted_at IS NULL;

-- Soft delete: set timestamp, bukan DELETE
UPDATE users SET deleted_at = NOW() WHERE id = 42;

-- Restore
UPDATE users SET deleted_at = NULL WHERE id = 42;

-- Index parsial untuk performa (PostgreSQL)
CREATE INDEX idx_users_active ON users (id)
  WHERE deleted_at IS NULL;  -- hanya index baris aktif
ℹ️
Laravel Eloquent sudah mendukung soft delete built-in dengan trait SoftDeletes. Cukup tambahkan kolom deleted_at dan trait tersebut, dan semua query otomatis menambahkan WHERE deleted_at IS NULL.

Strategi Indeks Sejak Awal

Indeks mempercepat baca tapi memperlambat tulis (INSERT/UPDATE/DELETE harus update index). Yang perlu di-index sejak awal:

SQL — Indeks yang disarankan untuk tabel pesanan
-- Index FK (paling sering terlupakan)
CREATE INDEX idx_pesanan_user_id    ON pesanan (user_id);
CREATE INDEX idx_pesanan_produk_id  ON pesanan (produk_id);

-- Index untuk filter umum
CREATE INDEX idx_pesanan_status     ON pesanan (status);
CREATE INDEX idx_pesanan_created_at ON pesanan (created_at);

-- Composite index untuk query yang sudah diketahui polanya
CREATE INDEX idx_pesanan_user_status ON pesanan (user_id, status, created_at);

Checklist Final

Gunakan checklist ini sebelum deploy migrasi baru ke production: