Chapter sebelumnya menutup dengan sebuah kebutuhan yang belum terjawab: menampilkan nama pelanggan berdampingan dengan pesanannya dalam satu query tunggal. INNER JOIN menjawab kebutuhan itu dengan cara yang paling lugas — menggabungkan dua tabel, lalu hanya menampilkan pasangan baris yang kuncinya benar-benar cocok. Pada chapter ini kita menulis INNER JOIN pertama untuk pasangan tabel pelanggan dan pesanan, membiasakan diri dengan alias tabel pendek, membuktikan bahwa pelanggan tanpa pesanan tidak akan muncul di hasil, lalu mengenal varian USING dan kombinasi JOIN dengan WHERE serta ORDER BY.

Menyiapkan Pasangan Tabel Latihan #

Kita pakai pasangan tabel yang sama dengan chapter sebelumnya di database belajar_join. Tujuh pelanggan diisi data: empat orang memiliki pesanan (Budi bahkan tiga pesanan), sedangkan Dewi, Tono, dan Sari sengaja tidak diberi satu pun pesanan — ketiganya akan menjadi pembeda penting antara INNER JOIN dan LEFT JOIN di chapter berikutnya:

sql
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)
);

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);

Output yang Dihasilkan:

text
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, 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

Total tujuh baris pesanan menaut ke hanya empat pelanggan. Foreign key pelanggan_id tetap dipasang agar relasi tetap dijaga server, persis seperti penjelasan chapter sebelumnya. Seluruh tanggal ditulis sebagai literal seperti '2026-01-05' sehingga setiap contoh di chapter ini memberikan hasil yang sama setiap kali dijalankan.

Join Pertama: Klausa ON Menyamakan Kunci #

Bentuk dasar INNER JOIN menyebut dua tabel di klausa FROM, lalu mendeklarasikan aturan pencocokan di klausa ON. Query berikut menampilkan nama pelanggan di samping tanggal dan total pesanannya:

sql
SELECT p.nama, pn.tanggal, pn.total
FROM pelanggan p
INNER JOIN pesanan pn ON p.id = pn.pelanggan_id
ORDER BY pn.id;

Output yang Dihasilkan:

text
+--------------+------------+-----------+
| nama         | tanggal    | total     |
+--------------+------------+-----------+
| Budi Santoso | 2026-01-05 | 170000.00 |
| Siti Rahayu  | 2026-01-07 |  50000.00 |
| Budi Santoso | 2026-01-10 |  62000.00 |
| Budi Santoso | 2026-01-12 |  15000.00 |
| Andi Wijaya  | 2026-01-15 | 137000.00 |
| Siti Rahayu  | 2026-01-18 |  85000.00 |
| Rina Melati  | 2026-01-08 | 125000.00 |
+--------------+------------+-----------+
7 rows in set (0.00 sec)

Mari kita bedah query ini baris per baris:

  1. Baris SELECT p.nama, pn.tanggal, pn.total: daftar kolom kini memakai awalan nama tabel. Kolom nama milik p (pelanggan) dan kolom tanggal/total milik pn (pesanan) — tanpa awalan, nama kolom yang sama di dua tabel akan saling bertabrakan.
  2. Baris FROM pelanggan p: tabel pertama diberi alias pendek p lewat spasi sederhana setelah nama tabel; pesanan pn di baris berikutnya juga demikian. Alias menghemat penulisan dan membuat ON jauh lebih mudah dibaca.
  3. Baris INNER JOIN pesanan pn ON p.id = pn.pelanggan_id: inti penggabungan. MySQL memindai kedua tabel lalu membentuk satu baris hasil untuk setiap pasangan baris yang memenuhi p.id = pn.pelanggan_id — primary key induk disamakan dengan foreign key anak.
  4. Baris ORDER BY pn.id: mengurutkan hasil mengikuti nomor pesanan supaya baris tampil dalam urutan yang stabil dan mudah dibandingkan ulang.

Kata kunci INNER pada praktiknya boleh dihilangkan — JOIN pesanan pn ON ... tanpa INNER bermakna persis sama. Kebiasaan menulis INNER JOIN secara eksplisit tetap kita pakai di course ini karena jenisnya langsung terbaca dari teks query.

Pelanggan Tanpa Pesanan Tidak Muncul #

Hasil query di atas memuat tujuh baris, padahal tabel pelanggan berisi tujuh orang dan tabel pesanan berisi tujuh transaksi. Mari kita periksa nama apa saja yang benar-benar tampil:

sql
SELECT COUNT(DISTINCT p.id) AS pelanggan_yang_muncul
FROM pelanggan p
INNER JOIN pesanan pn ON p.id = pn.pelanggan_id;

Output yang Dihasilkan:

text
+-----------------------+
| pelanggan_yang_muncul |
+-----------------------+
|                     4 |
+-----------------------+
1 row in set (0.00 sec)

Fungsi COUNT(DISTINCT p.id) menghitung pelanggan berbeda yang lolos join, dan hasilnya hanya 4 — bukan 7. Dewi, Tono, dan Sari tidak pernah muncul karena INNER JOIN hanya menghasilkan baris ketika kedua sisi punya pasangan: baris pelanggan tanpa satu pun pesanan tidak menemukan pasangan di pesanan, sehingga baris tersebut tidak ikut ke hasil. Perilaku “hanya pasangan yang cocok” inilah makna kata inner — hasil berada di dalam perpotongan kedua tabel. Kabar baiknya, setiap baris hasilnya pasti valid: tidak ada baris dengan nama pelanggan yang digantung tanpa data pesanan.

Varian USING untuk Kolom Bernama Sama #

Klausa ON p.id = pn.pelanggan_id memakai dua kolom yang namanya berbeda. MySQL menyediakan varian USING(kolom) yang lebih ringkas, tetapi syaratnya ketat: kedua tabel harus memiliki kolom kunci dengan nama yang sama persis. Syarat itu tidak terpenuhi pada pasangan kita — pelanggan memakai id, pesanan memakai pelanggan_id — jadi untuk mendemokan USING kita perlu sepasang tabel kecil yang kolom kuncinya memang senama:

sql
CREATE TABLE produk (
  kode_produk CHAR(4) PRIMARY KEY,
  nama_produk VARCHAR(40) NOT NULL
);

CREATE TABLE stok (
  kode_produk CHAR(4) PRIMARY KEY,
  jumlah INT NOT NULL,
  FOREIGN KEY (kode_produk) REFERENCES produk(kode_produk)
);

INSERT INTO produk (kode_produk, nama_produk) VALUES
  ('P001', 'Kopi Gayo'),
  ('P002', 'Teh Melati'),
  ('P003', 'Gula Aren'),
  ('P004', 'Kopi Toraja');

INSERT INTO stok (kode_produk, jumlah) VALUES
  ('P001', 20),
  ('P003', 55),
  ('P004', 8);

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 4 rows affected (0.00 sec)
Records: 4  Duplicates: 0  Warnings: 0

Query OK, 3 rows affected (0.00 sec)
Records: 3  Duplicates: 0  Warnings: 0
sql
SELECT * FROM produk prd
INNER JOIN stok stk USING (kode_produk)
ORDER BY prd.kode_produk;

Output yang Dihasilkan:

text
+-------------+-------------+--------+
| kode_produk | nama_produk | jumlah |
+-------------+-------------+--------+
| P001        | Kopi Gayo   |     20 |
| P003        | Gula Aren   |     55 |
| P004        | Kopi Toraja |      8 |
+-------------+-------------+--------+
3 rows in set (0.00 sec)

Hasilnya tiga baris karena hanya tiga produk yang memiliki catatan stok — Teh Melati (P002) tidak punya baris di tabel stok, sehingga produk itu tidak lolos INNER JOIN, persis pola pelanggan tanpa pesanan sebelumnya. USING (kode_produk) adalah singkatan dari ON prd.kode_produk = stk.kode_produk, dan satu bonus kecilnya tampak pada SELECT *: kolom penghubung kode_produk tampil satu kali, bukan dua kali seperti yang terjadi pada ON — MySQL tahu kedua kolom itu kini identik. Ketika nama kolom kunci tidak sama, seperti pada pelanggan dan pesanan, klausa ON adalah satu-satunya pilihan.

INNER JOIN dengan WHERE dan ORDER BY #

INNER JOIN tidak mengganti klausa lain — query hasil join tetap tabel biasa yang bisa disaring dan diurutkan. Kombinasi lengkapnya berbentuk FROM + JOIN/ON untuk penggabungan, WHERE untuk penyaringan baris, dan ORDER BY untuk pengurutan hasil. Query berikut menampilkan pesanan mulai tanggal 2026-01-11 beserta nama pemiliknya, terurut menurut tanggal:

sql
SELECT p.nama, pn.tanggal, pn.total
FROM pelanggan p
INNER JOIN pesanan pn ON p.id = pn.pelanggan_id
WHERE pn.tanggal >= '2026-01-11'
ORDER BY pn.tanggal;

Output yang Dihasilkan:

text
+--------------+------------+-----------+
| nama         | tanggal    | total     |
+--------------+------------+-----------+
| Budi Santoso | 2026-01-12 |  15000.00 |
| Andi Wijaya  | 2026-01-15 | 137000.00 |
| Siti Rahayu  | 2026-01-18 |  85000.00 |
+--------------+------------+-----------+
3 rows in set (0.00 sec)

Urutan pengerjaan di balik layar membantu memahami hasilnya: MySQL menggabungkan kedua tabel lebih dulu (join mengikuti ON), lalu WHERE menyaring baris gabungan yang tanggalnya di bawah 2026-01-11, dan akhirnya ORDER BY mengurutkan sisa baris. Tiga pesanan lolos: pesanan Budi pada 2026-01-12, pesanan Andi pada 2026-01-15, dan pesanan Siti pada 2026-01-18 — pesanan Siti tanggal 2026-01-07 tersaring karena WHERE menilai tanggal setiap baris satu per satu. Pola tiga klausa ini berlaku untuk semua jenis join yang akan kita temui di sepanjang section.

Membersihkan Database Latihan #

sql
DROP DATABASE belajar_join;

Output yang Dihasilkan:

text
Query OK, 4 rows affected (0.00 sec)

Referensi #

INNER JOIN kini kita kuasai: klausa ON menyamakan kunci kedua tabel, alias pendek merapikan penulisan, dan baris tanpa pasangan terbukti hilang dari hasil. Ketuntasan itu sekaligus menyingkap keterbatasannya — Dewi, Tono, dan Sari tidak akan pernah tampil di laporan pesanan, padahal daftar “semua pelanggan dan pesanannya (jika ada)” adalah laporan yang sama sering dibutuhkan. Chapter berikutnya membahas LEFT JOIN, jenis join yang mempertahankan seluruh baris sisi kiri dan mengisi kekosongan pasangan dengan NULL.