September 27, 2026

Relasi Tabel dan Normalisasi.

Memahami relasi 1:1, 1:N, dan N:M (pivot table), foreign key beserta aksi ON DELETE/ON UPDATE di MySQL InnoDB, serta dasar normalisasi 1NF sampai 3NF.

Kekuatan database relasional ada pada kemampuannya menghubungkan tabel. Alih-alih menyimpan semua data di satu tabel raksasa, data dipecah per entitas (user, produk, order) lalu dihubungkan lewat kunci. Catatan ini membahas jenis relasi, cara menerapkannya dengan foreign key di MySQL 8 (InnoDB), dan dasar normalisasi.


1. Istilah Dasar

IstilahArti
Primary key (PK)Kolom yang mengidentifikasi baris secara unik, tidak boleh NULL
Foreign key (FK)Kolom yang merujuk ke PK (atau kolom unik) di tabel lain
Tabel induk (parent)Tabel yang dirujuk, misalnya users
Tabel anak (child)Tabel yang memegang FK, misalnya posts.user_id
KardinalitasBerapa banyak baris di satu sisi yang berpasangan dengan sisi lain

2. One-to-One (1:1)

Satu baris di tabel A berpasangan dengan maksimal satu baris di tabel B. Contoh: users dan profiles. Biasanya dipakai untuk memisahkan kolom yang jarang diakses atau opsional.

CREATE TABLE users (
  id     BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  email  VARCHAR(150) NOT NULL UNIQUE
);

CREATE TABLE profiles (
  id       BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  user_id  BIGINT UNSIGNED NOT NULL UNIQUE,   -- UNIQUE yang membuatnya 1:1
  bio      TEXT,
  avatar   VARCHAR(255),
  CONSTRAINT fk_profiles_user
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

Kunci utamanya adalah constraint UNIQUE pada user_id. Tanpa itu, relasinya secara teknis menjadi 1:N.


3. One-to-Many (1:N)

Relasi paling umum. Satu user punya banyak post; setiap post dimiliki satu user. FK diletakkan di sisi “many”.

CREATE TABLE posts (
  id         BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  user_id    BIGINT UNSIGNED NOT NULL,
  title      VARCHAR(200) NOT NULL,
  body       TEXT,
  CONSTRAINT fk_posts_user
    FOREIGN KEY (user_id) REFERENCES users(id)
    ON DELETE CASCADE ON UPDATE CASCADE
);

Query:

-- semua post milik user tertentu
SELECT p.* FROM posts p WHERE p.user_id = 1;

-- jumlah post per user, termasuk yang belum pernah menulis
SELECT u.email, COUNT(p.id) AS jumlah_post
FROM users u
LEFT JOIN posts p ON p.user_id = u.id
GROUP BY u.id, u.email;

4. Many-to-Many (N:M) dengan Pivot Table

Satu post bisa punya banyak tag, satu tag dipakai banyak post. Relasi ini tidak bisa diwakili dengan satu FK; dibutuhkan tabel penghubung (pivot / junction table).

CREATE TABLE tags (
  id    BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  name  VARCHAR(50) NOT NULL UNIQUE
);

CREATE TABLE post_tag (
  post_id    BIGINT UNSIGNED NOT NULL,
  tag_id     BIGINT UNSIGNED NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (post_id, tag_id),        -- cegah pasangan duplikat
  KEY idx_post_tag_tag (tag_id),        -- untuk query dari arah tag
  CONSTRAINT fk_post_tag_post FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
  CONSTRAINT fk_post_tag_tag  FOREIGN KEY (tag_id)  REFERENCES tags(id)  ON DELETE CASCADE
);

Poin penting:

  • Composite primary key (post_id, tag_id) menjamin satu tag tidak ditempel dua kali ke post yang sama.
  • PK tersebut sudah menjadi index untuk pencarian berdasarkan post_id. Tambahkan index terpisah di tag_id untuk pencarian dari sisi tag.
  • Pivot table boleh punya kolom tambahan (misalnya created_at, role, quantity). Jika kolomnya banyak, tabel ini sebenarnya sudah menjadi entitas sendiri (contoh: order_items).
-- tag untuk post #10
SELECT t.name
FROM tags t
JOIN post_tag pt ON pt.tag_id = t.id
WHERE pt.post_id = 10;

-- post yang punya tag 'mysql'
SELECT p.title
FROM posts p
JOIN post_tag pt ON pt.post_id = p.id
JOIN tags t ON t.id = pt.tag_id
WHERE t.name = 'mysql';

5. Foreign Key dan Aksi Referensial

Syarat foreign key di InnoDB:

  • Kedua tabel memakai engine InnoDB (MyISAM tidak menegakkan FK).
  • Tipe data kolom FK dan kolom yang dirujuk harus sejenis: ukuran integer dan UNSIGNED/SIGNED harus sama. INT vs BIGINT UNSIGNED akan gagal dibuat.
  • Kolom yang dirujuk harus ter-index (biasanya PK). Kolom FK di tabel anak akan dibuatkan index otomatis jika belum ada.

Pilihan ON DELETE / ON UPDATE

AksiSaat baris induk dihapus/diubah
RESTRICTDitolak jika masih ada baris anak
NO ACTIONDi InnoDB sama dengan RESTRICT (dicek langsung, bukan ditunda)
CASCADEBaris anak ikut dihapus / FK anak ikut diubah
SET NULLFK anak diisi NULL (kolom FK harus nullable)
SET DEFAULTDikenali parser, tetapi ditolak oleh InnoDB

Jika tidak ditulis, default-nya adalah NO ACTION (efektif RESTRICT).

Contoh memilih aksi yang tepat:

-- komentar hilang jika post dihapus
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE

-- order tidak boleh hilang hanya karena customer dihapus
FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE RESTRICT

-- artikel tetap ada meski penulisnya dihapus
author_id BIGINT UNSIGNED NULL,
FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL

Peringatan: ON DELETE CASCADE itu nyaman tapi berbahaya pada rantai panjang. Menghapus satu user bisa menghapus post, komentar, like, dan seterusnya tanpa terlihat. Untuk data bisnis penting (transaksi , invoice), lebih aman pakai RESTRICT atau soft delete (deleted_at).

Menambah, melihat, dan menghapus FK

ALTER TABLE posts
  ADD CONSTRAINT fk_posts_user FOREIGN KEY (user_id) REFERENCES users(id);

-- daftar FK di database aktif
SELECT TABLE_NAME, CONSTRAINT_NAME, REFERENCED_TABLE_NAME
FROM information_schema.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = DATABASE() AND REFERENCED_TABLE_NAME IS NOT NULL;

ALTER TABLE posts DROP FOREIGN KEY fk_posts_user;

Saat import data massal (misalnya restore dump), pengecekan FK bisa dimatikan sementara per sesi:

SET FOREIGN_KEY_CHECKS = 0;
-- import ...
SET FOREIGN_KEY_CHECKS = 1;

MySQL tidak memvalidasi ulang data yang masuk selama pengecekan dimatikan, jadi pastikan datanya memang konsisten.


6. Normalisasi 1NF sampai 3NF

Normalisasi adalah proses merapikan struktur tabel untuk menghindari redundansi dan anomali (insert, update, delete). Misalkan kita mulai dari tabel “semua jadi satu”:

order_idcustomercustomer_cityproductstotal
1BudiBandungKopi, Teh45000
2BudiBandungGula15000

1NF: Nilai atomik, tanpa grup berulang

Setiap sel berisi satu nilai. Kolom products berisi daftar, jadi melanggar 1NF. Pecah menjadi satu baris per produk (atau tabel terpisah).

order_idcustomercustomer_cityproductqty
1BudiBandungKopi1
1BudiBandungTeh1
2BudiBandungGula1

2NF: Tidak ada ketergantungan parsial

Berlaku untuk tabel dengan composite key. Setiap kolom non-kunci harus bergantung pada seluruh kunci, bukan sebagian. Pada tabel di atas kuncinya (order_id, product), tetapi customer hanya bergantung pada order_id. Pisahkan:

  • orders(order_id, customer, customer_city)
  • order_items(order_id, product, qty)

3NF: Tidak ada ketergantungan transitif

Kolom non-kunci tidak boleh bergantung pada kolom non-kunci lain. customer_city bergantung pada customer, bukan pada order_id. Pisahkan lagi:

  • customers(id, name, city)
  • orders(id, customer_id)
  • products(id, name, price)
  • order_items(order_id, product_id, qty, price)

Sekarang, jika Budi pindah kota, cukup ubah satu baris di customers.

Ringkasan

BentukAturan singkat
1NFNilai atomik, tidak ada kolom berisi daftar atau kolom berulang (phone1, phone2)
2NF1NF + semua kolom bergantung pada seluruh PK
3NF2NF + tidak ada kolom yang bergantung pada kolom non-kunci lain

Kapan boleh denormalisasi?

Normalisasi bukan agama. Denormalisasi yang disengaja wajar dilakukan, misalnya:

  • Menyimpan price di order_items saat transaksi terjadi. Ini bukan redundansi, karena harga produk bisa berubah di kemudian hari dan order lama harus tetap mencatat harga saat itu.
  • Kolom ringkasan seperti posts.comments_count untuk menghindari COUNT(*) berulang di halaman yang sering dibuka.

Selama ada mekanisme yang menjaga konsistensinya (transaksi, trigger, atau job), denormalisasi seperti ini sah.


7. Kesalahan Umum

MasalahPenyebabSolusi
ERROR 3780 ... incompatible saat membuat FKTipe kolom berbeda (INT vs BIGINT UNSIGNED)Samakan tipe dan atribut UNSIGNED
ERROR 1451 Cannot delete or update a parent rowMasih ada baris anak dengan RESTRICTHapus anak dulu, atau ubah aksi FK
ERROR 1452 Cannot add or update a child rowNilai FK tidak ada di tabel indukPastikan induk tersedia
Data “yatim” (orphan) padahal ada relasiTabel MyISAM, atau relasi hanya di level aplikasiPakai InnoDB dan FK sungguhan
Pivot table berisi duplikatTidak ada PK/UNIQUE pada pasangan kolomTambah PRIMARY KEY (a_id, b_id)
Menyimpan ID dipisah koma ("1,4,7")Menghindari pivot tableBuat pivot table; kolom daftar melanggar 1NF dan tak bisa di-JOIN/index

Referensi

Hey! I’m Fanny, the software engineer tending to this digital garden. You can read more about me, or subscribe by email.

Comments