Dua tabel sudah kita rangkai dengan lancar; laporan nyata jarang seberuntung itu. Pertanyaan bisnis yang umum — “berapa total belanja tiap pelanggan berdasarkan isi pesanannya?” — menuntut data dari tiga tabel sekaligus: nama dari pelanggan, pesanan dari pesanan, dan rincian item dari tabel detail. Kabar baiknya, klausa JOIN bisa dituliskan berkali-kali dalam satu query, dan setiap join tinggal merangkai dua hasil — tabel ketiga bergabung dengan hasil penggabungan dua tabel sebelumnya. Pada chapter ini kita merantai tiga tabel pelanggan → pesanan → detail_pesanan, membuktikan bahwa urutan penulisan join bebas selama rantai kunci tersambung, lalu menutup dengan laporan agregat per pelanggan yang dibangun di atas join tiga tabel.
Menyiapkan Tiga Tabel Berantai #
Pasangan pelanggan dan pesanan kembali dipakai, kini ditambah tabel ketiga: detail_pesanan, tabel anak yang menyimpan item belanja di dalam setiap pesanan. Satu pesanan bisa memuat banyak item, jadi pesanan → detail_pesanan adalah relasi 1-N kedua dalam rantai kita. Nilai total pada tabel pesanan sengaja konsisten dengan penjumlahan harga item di detailnya (kolom jumlah × harga), sehingga kita bisa saling memeriksa angka antar tabel:
DROP DATABASE IF EXISTS belajar_join;
CREATE DATABASE belajar_join;
USE belajar_join;
CREATE TABLE pelanggan (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(50) NOT NULL,
kota VARCHAR(30) NOT NULL
);
CREATE TABLE pesanan (
id INT AUTO_INCREMENT PRIMARY KEY,
pelanggan_id INT NOT NULL,
tanggal DATE NOT NULL,
total DECIMAL(12,2) NOT NULL,
FOREIGN KEY (pelanggan_id) REFERENCES pelanggan(id)
);
CREATE TABLE detail_pesanan (
id INT AUTO_INCREMENT PRIMARY KEY,
pesanan_id INT NOT NULL,
produk VARCHAR(40) NOT NULL,
jumlah INT NOT NULL,
harga DECIMAL(10,2) NOT NULL,
FOREIGN KEY (pesanan_id) REFERENCES pesanan(id)
);
INSERT INTO pelanggan (nama, kota) VALUES
('Budi Santoso', 'Jakarta'),
('Siti Rahayu', 'Bandung'),
('Andi Wijaya', 'Surabaya'),
('Rina Melati', 'Yogyakarta'),
('Dewi Lestari', 'Bandung'),
('Tono Hartono', 'Medan'),
('Sari Puspita', 'Jakarta');
INSERT INTO pesanan (pelanggan_id, tanggal, total) VALUES
(1, '2026-01-05', 170000.00),
(2, '2026-01-07', 50000.00),
(1, '2026-01-10', 62000.00),
(1, '2026-01-12', 15000.00),
(3, '2026-01-15', 137000.00),
(2, '2026-01-18', 85000.00),
(4, '2026-01-08', 125000.00);
INSERT INTO detail_pesanan (pesanan_id, produk, jumlah, harga) VALUES
(1, 'Kopi Gayo', 2, 55000.00),
(1, 'Gula Aren', 4, 15000.00),
(2, 'Teh Melati', 2, 25000.00),
(3, 'Kopi Toraja', 1, 62000.00),
(4, 'Gula Aren', 1, 15000.00),
(5, 'Kopi Toraja', 1, 62000.00),
(5, 'Teh Melati', 3, 25000.00),
(6, 'Kopi Gayo', 1, 55000.00),
(6, 'Gula Aren', 2, 15000.00),
(7, 'Kopi Gayo', 2, 55000.00),
(7, 'Gula Aren', 1, 15000.00);Output yang Dihasilkan:
Query OK, 0 rows affected, 1 warning (0.00 sec)
Query OK, 1 row affected (0.00 sec)
Database changed
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
Query OK, 7 rows affected (0.00 sec)
Records: 7 Duplicates: 0 Warnings: 0
Query OK, 7 rows affected (0.00 sec)
Records: 7 Duplicates: 0 Warnings: 0
Query OK, 11 rows affected (0.00 sec)
Records: 11 Duplicates: 0 Warnings: 0Rantai relasinya kini dua tingkat: pelanggan 1-N pesanan, lalu pesanan 1-N detail_pesanan. Sebelas baris detail terbagi ke tujuh pesanan — pesanan nomor 1, 5, 6, dan 7 masing-masing memuat dua item, sisanya satu item. Setiap foreign key (pelanggan_id, pesanan_id) menjadi mata rantai yang menyambungkan tabel berikutnya.
Merangkai Tiga Tabel dalam Satu Query #
Query pertama menampilkan isi keranjang setiap pelanggan: nama pelanggan, tanggal pesanan, produk, dan jumlahnya. Dua klausa JOIN dituliskan berurutan:
SELECT p.nama, pn.tanggal, d.produk, d.jumlah
FROM pelanggan p
INNER JOIN pesanan pn ON p.id = pn.pelanggan_id
INNER JOIN detail_pesanan d ON d.pesanan_id = pn.id
ORDER BY pn.id, d.id;Output yang Dihasilkan:
+--------------+------------+-------------+--------+
| nama | tanggal | produk | jumlah |
+--------------+------------+-------------+--------+
| Budi Santoso | 2026-01-05 | Kopi Gayo | 2 |
| Budi Santoso | 2026-01-05 | Gula Aren | 4 |
| Siti Rahayu | 2026-01-07 | Teh Melati | 2 |
| Budi Santoso | 2026-01-10 | Kopi Toraja | 1 |
| Budi Santoso | 2026-01-12 | Gula Aren | 1 |
| Andi Wijaya | 2026-01-15 | Kopi Toraja | 1 |
| Andi Wijaya | 2026-01-15 | Teh Melati | 3 |
| Siti Rahayu | 2026-01-18 | Kopi Gayo | 1 |
| Siti Rahayu | 2026-01-18 | Gula Aren | 2 |
| Rina Melati | 2026-01-08 | Kopi Gayo | 2 |
| Rina Melati | 2026-01-08 | Gula Aren | 1 |
+--------------+------------+-------------+--------+
11 rows in set (0.00 sec)Mari kita bedah proses penggabungannya tahap demi tahap:
FROM pelanggan p: titik awal adalah tabel pelanggan dengan aliasp.INNER JOIN pesanan pn ON p.id = pn.pelanggan_id: join pertama menggabungkanpelanggandanpesanan— persis join dua tabel yang kita kenal, menghasilkan baris bernama pelanggan lengkap dengan pesanannya.INNER JOIN detail_pesanan d ON d.pesanan_id = pn.id: join kedua tidak lagi bergabung dengan tabel mentah, melainkan dengan hasil join pertama — tabel sementara berisi gabungan pelanggan+pesanan. Kondisid.pesanan_id = pn.idmenyambungkan setiap item ke pesanan pemiliknya, dan karena hasil join pertama sudah membawa nama pelanggan, setiap item otomatis tahu siapa pemiliknya.ORDER BY pn.id, d.id: mengurutkan berdasarkan pesanan lebih dulu, lalu nomor detail di dalamnya.
Hasilnya sebelas baris — sama dengan jumlah baris detail_pesanan, karena setiap detail pasti dimiliki pesanan yang pasti dimiliki pelanggan (foreign key menjaga keduanya). Pesanan nomor 1 milik Budi tampil dua baris karena berisi dua item, begitu pula pesanan 5, 6, dan 7. Perhatikan pula bahwa tiga pelanggan tanpa pesanan — Dewi, Tono, dan Sari — sekali lagi absen: INNER JOIN terakhir tetap hanya meloloskan baris yang berpasangan di seluruh rantai.
Urutan Join Bebas, Hasil Sama #
Urutan penulisan join tidak harus mengikuti hierarki induk-ke-anak. Query berikut memulai dari tabel paling detail, lalu naik ke pesanan dan pelanggan:
SELECT p.nama, pn.tanggal, d.produk, d.jumlah
FROM detail_pesanan d
INNER JOIN pesanan pn ON d.pesanan_id = pn.id
INNER JOIN pelanggan p ON p.id = pn.pelanggan_id
ORDER BY pn.id, d.id;Output yang Dihasilkan:
+--------------+------------+-------------+--------+
| nama | tanggal | produk | jumlah |
+--------------+------------+-------------+--------+
| Budi Santoso | 2026-01-05 | Kopi Gayo | 2 |
| Budi Santoso | 2026-01-05 | Gula Aren | 4 |
| Siti Rahayu | 2026-01-07 | Teh Melati | 2 |
| Budi Santoso | 2026-01-10 | Kopi Toraja | 1 |
| Budi Santoso | 2026-01-12 | Gula Aren | 1 |
| Andi Wijaya | 2026-01-15 | Kopi Toraja | 1 |
| Andi Wijaya | 2026-01-15 | Teh Melati | 3 |
| Siti Rahayu | 2026-01-18 | Kopi Gayo | 1 |
| Siti Rahayu | 2026-01-18 | Gula Aren | 2 |
| Rina Melati | 2026-01-08 | Kopi Gayo | 2 |
| Rina Melati | 2026-01-08 | Gula Aren | 1 |
+--------------+------------+-------------+--------+
11 rows in set (0.00 sec)Sebelas baris yang sama persis dengan versi sebelumnya. Optimizer MySQL bekerja dari struktur logis join — himpunan kondisi ON yang sama selalu menghasilkan baris yang sama — sehingga urutan penulisan hampir selalu soal keterbacaan, bukan hasil. Kebiasaan yang baik: tulis join mengikuti alur nalar pertanyaan bisnis. Untuk laporan “belanja per pelanggan”, alur pelanggan → pesanan → detail terbaca paling natural; untuk laporan audit “item mana yang bermasalah”, alur dari detail ke pelanggan justru lebih masuk akal.
Join Tiga Tabel dengan Agregat #
Kita tutup dengan laporan yang menjadi alasan utama join bertingkat: total belanja setiap pelanggan dihitung dari jumlah dan harga tiap item. GROUP BY bekerja seperti biasa — bedanya sumber barisnya kini hasil join tiga tabel:
SELECT p.nama,
COUNT(DISTINCT pn.id) AS jumlah_pesanan,
SUM(d.jumlah * d.harga) AS total_belanja
FROM pelanggan p
INNER JOIN pesanan pn ON p.id = pn.pelanggan_id
INNER JOIN detail_pesanan d ON d.pesanan_id = pn.id
GROUP BY p.id, p.nama
ORDER BY total_belanja DESC;Output yang Dihasilkan:
+--------------+----------------+---------------+
| nama | jumlah_pesanan | total_belanja |
+--------------+----------------+---------------+
| Budi Santoso | 3 | 247000.00 |
| Andi Wijaya | 1 | 137000.00 |
| Siti Rahayu | 2 | 135000.00 |
| Rina Melati | 1 | 125000.00 |
+--------------+----------------+---------------+
4 rows in set (0.00 sec)Mari kita bedah elemen kuncinya:
SUM(d.jumlah * d.harga): nilai belanja setiap item dihitung dari kolom tabel ketiga — jumlah unit dikali harga satuan — lalu dijumlahkan per kelompok pelanggan. Budi mencapai247000.00dari110000 + 60000 + 62000 + 15000(dua item pesanan pertama, satu item pesanan ketiga, satu item pesanan keempat).COUNT(DISTINCT pn.id): penghitungan pesanan harus memakaiDISTINCTkarena join tiga tingkat menggandakan setiap pesanan sebanyak itemnya — pesanan dua item muncul dua baris. TanpaDISTINCT, jumlah pesanan Budi akan tercatat 4 padahal faktanya 3.GROUP BY p.id, p.nama: mengelompokkan berdasarkan identitas pelanggan — kolomidikut dikelompokkan supaya dua pelanggan bernama sama pun tetap terpisah.ORDER BY total_belanja DESC: laporan dibaca dari pembeli terbesar; alias hasil agregat boleh dipakai diORDER BY.
Empat baris hasil mewakili empat pelanggan yang pernah memesan — dan hanya mereka, karena INNER JOIN menyaring rantai dari hulu. Laporan seperti inilah bentuk paling umum join bertingkat di aplikasi nyata: beberapa tabel dirangkai, lalu diringkas dengan agregat. Perhatian khusus pada COUNT(DISTINCT) di poin kedua menjadi jembatan ke topik chapter terakhir section ini: join multi-tabel memang cenderung menduplikasi baris, dan duplikasi itu harus dikelola secara sadar.
Membersihkan Database Latihan #
DROP DATABASE belajar_join;Output yang Dihasilkan:
Query OK, 3 rows affected (0.00 sec)Referensi #
- JOIN Clause — MySQL 8.4 Reference Manual
- SELECT Statement — MySQL 8.4 Reference Manual
- Aggregate Function Descriptions — MySQL 8.4 Reference Manual
Tiga tabel kini bisa kita rangkai: setiap JOIN baru bergabung dengan hasil join sebelumnya, urutan penulisan bebas asal rantai kunci tersambung, dan agregat SUM/COUNT(DISTINCT) merangkum hasilnya per pelanggan. Dalam contoh terakhir, COUNT(DISTINCT) harus bekerja ekstra karena join tiga tabel menggandakan baris pesanan — dan itulah gejala yang menjadi topik penuh chapter berikutnya: duplikasi baris hasil join dan cara menghindarinya.