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
| Istilah | Arti |
|---|---|
| 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 |
| Kardinalitas | Berapa 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 ditag_iduntuk 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/SIGNEDharus sama.INTvsBIGINT UNSIGNEDakan 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
| Aksi | Saat baris induk dihapus/diubah |
|---|---|
RESTRICT | Ditolak jika masih ada baris anak |
NO ACTION | Di InnoDB sama dengan RESTRICT (dicek langsung, bukan ditunda) |
CASCADE | Baris anak ikut dihapus / FK anak ikut diubah |
SET NULL | FK anak diisi NULL (kolom FK harus nullable) |
SET DEFAULT | Dikenali 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 CASCADEitu nyaman tapi berbahaya pada rantai panjang. Menghapus satuuserbisa menghapus post, komentar, like, dan seterusnya tanpa terlihat. Untuk data bisnis penting (transaksi , invoice), lebih aman pakaiRESTRICTatau 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_id | customer | customer_city | products | total |
|---|---|---|---|---|
| 1 | Budi | Bandung | Kopi, Teh | 45000 |
| 2 | Budi | Bandung | Gula | 15000 |
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_id | customer | customer_city | product | qty |
|---|---|---|---|---|
| 1 | Budi | Bandung | Kopi | 1 |
| 1 | Budi | Bandung | Teh | 1 |
| 2 | Budi | Bandung | Gula | 1 |
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
| Bentuk | Aturan singkat |
|---|---|
| 1NF | Nilai atomik, tidak ada kolom berisi daftar atau kolom berulang (phone1, phone2) |
| 2NF | 1NF + semua kolom bergantung pada seluruh PK |
| 3NF | 2NF + tidak ada kolom yang bergantung pada kolom non-kunci lain |
Kapan boleh denormalisasi?
Normalisasi bukan agama. Denormalisasi yang disengaja wajar dilakukan, misalnya:
- Menyimpan
pricediorder_itemssaat 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_countuntuk menghindariCOUNT(*)berulang di halaman yang sering dibuka.
Selama ada mekanisme yang menjaga konsistensinya (transaksi, trigger, atau job), denormalisasi seperti ini sah.
7. Kesalahan Umum
| Masalah | Penyebab | Solusi |
|---|---|---|
ERROR 3780 ... incompatible saat membuat FK | Tipe kolom berbeda (INT vs BIGINT UNSIGNED) | Samakan tipe dan atribut UNSIGNED |
ERROR 1451 Cannot delete or update a parent row | Masih ada baris anak dengan RESTRICT | Hapus anak dulu, atau ubah aksi FK |
ERROR 1452 Cannot add or update a child row | Nilai FK tidak ada di tabel induk | Pastikan induk tersedia |
| Data “yatim” (orphan) padahal ada relasi | Tabel MyISAM, atau relasi hanya di level aplikasi | Pakai InnoDB dan FK sungguhan |
| Pivot table berisi duplikat | Tidak ada PK/UNIQUE pada pasangan kolom | Tambah PRIMARY KEY (a_id, b_id) |
Menyimpan ID dipisah koma ("1,4,7") | Menghindari pivot table | Buat 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.