INNER JOIN membuktikan dirinya selektif: Dewi, Tono, dan Sari lenyap dari hasil karena tidak memiliki satu pun pesanan. Banyak laporan justru meminta kebalikannya — daftar seluruh pelanggan lengkap dengan pesanannya bila ada, misalnya untuk mencari pelanggan yang perlu di-follow-up. LEFT JOIN dirancang untuk kebutuhan itu: seluruh baris tabel kiri dijamin tampil, dan ketika pasangan di tabel kanan tidak ditemukan, kolom-kolom kanan diisi NULL. Pada chapter ini kita membuktikan perilaku tersebut di atas data yang sama, lalu membangun dua pola penting di atasnya: anti-join dengan IS NULL untuk menemukan pelanggan yang belum pernah memesan, dan cara menghitung yang benar saat COUNT bertemu LEFT JOIN.
Menyiapkan Pasangan Tabel Latihan #
Data latihan identik dengan chapter INNER JOIN — sengaja demikian supaya perbandingan perilaku kedua jenis join terlihat langsung. Empat pelanggan memiliki pesanan (Budi tiga pesanan, Siti dua), dan tiga pelanggan tidak memiliki pesanan sama sekali:
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:
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: 0Kedua tabel sama-sama berisi tujuh baris, tetapi sebaran pasangannya timpang: sisi kanan (pesanan) menaut hanya ke empat dari tujuh pelanggan. Ketimpangan inilah bahan percobaan terbaik untuk melihat apa yang terjadi pada baris kiri tanpa pasangan.
Semua Baris Kiri Tetap Tampil #
Kita jalankan query yang sama persis dengan chapter sebelumnya, hanya kata INNER diganti LEFT:
SELECT p.nama, pn.id AS pesanan_id, pn.tanggal, pn.total
FROM pelanggan p
LEFT JOIN pesanan pn ON p.id = pn.pelanggan_id
ORDER BY p.id, pn.id;Output yang Dihasilkan:
+--------------+------------+------------+-----------+
| nama | pesanan_id | tanggal | total |
+--------------+------------+------------+-----------+
| Budi Santoso | 1 | 2026-01-05 | 170000.00 |
| Budi Santoso | 3 | 2026-01-10 | 62000.00 |
| Budi Santoso | 4 | 2026-01-12 | 15000.00 |
| Siti Rahayu | 2 | 2026-01-07 | 50000.00 |
| Siti Rahayu | 6 | 2026-01-18 | 85000.00 |
| Andi Wijaya | 5 | 2026-01-15 | 137000.00 |
| Rina Melati | 7 | 2026-01-08 | 125000.00 |
| Dewi Lestari | NULL | NULL | NULL |
| Tono Hartono | NULL | NULL | NULL |
| Sari Puspita | NULL | NULL | NULL |
+--------------+------------+------------+-----------+
10 rows in set (0.00 sec)Hasilnya kini sepuluh baris — bukan tujuh. Mari kita bedah mekanismenya baris demi baris:
- Klausa
LEFT JOIN pesanan pn ON p.id = pn.pelanggan_id: MySQL memproses setiap barispelanggan(tabel kiri) satu per satu dan mencari barispesanan(tabel kanan) yang kuncinya cocok. Perbedaan krusialnya denganINNER JOIN: baris kiri yang tidak menemukan satu pun pasangan tidak dibuang, melainkan tetap masuk hasil. - Baris Budi, Siti, Andi, Rina: keempat pelanggan ini menemukan pasangan, sehingga masing-masing menghasilkan satu baris per pesanan — Budi tiga baris, Siti dua baris, Andi dan Rina satu baris, total tujuh baris bergaya
INNER JOIN. - Baris Dewi, Tono, Sari: ketiga pelanggan ini tidak menemukan pasangan di
pesanan, namun tetap tampil. Kolom-kolom yang berasal dari tabel kanan —pesanan_id,tanggal,total— diisiNULLsebagai penanda “tidak ada data yang cocok”. - Klausa
ORDER BY p.id, pn.id: mengurutkan mengikuti pelanggan lebih dulu, lalu nomor pesanan; baris-barisNULLmilik satu pelanggan yang sama pasti berurutan karenapn.idmereka identik.
Perhatikan makna NULL di hasil ini dengan saksama: NULL pada kolom total bukan berarti “totalnya kosong/nol”, melainkan “pesanan yang cocok tidak ada”. Penafsiran “nol” akan menyesatkan — pelanggan tanpa pesanan memang belum berbelanja apa pun, tetapi NULL di sini mewakili ketiadaan baris, bukan nilai angka nol. Pemisahan makna ini penting ketika hasil join dipakai di perhitungan lanjutan.
Anti-Join: LEFT JOIN + IS NULL #
Kehadiran baris NULL membuka satu pola query yang sangat penting di dunia nyata: mencari baris kiri yang tidak memiliki pasangan sama sekali. Cukup saring hasil LEFT JOIN dengan kondisi kolom kunci kanan bernilai NULL:
SELECT p.nama, p.kota
FROM pelanggan p
LEFT JOIN pesanan pn ON p.id = pn.pelanggan_id
WHERE pn.id IS NULL
ORDER BY p.nama;Output yang Dihasilkan:
+--------------+---------+
| nama | kota |
+--------------+---------+
| Dewi Lestari | Bandung |
| Sari Puspita | Jakarta |
| Tono Hartono | Medan |
+--------------+---------+
3 rows in set (0.00 sec)Mari kita telusuri alurnya: LEFT JOIN menghasilkan sepuluh baris seperti sebelumnya, lalu WHERE pn.id IS NULL hanya meloloskan baris yang kolom pesanan_id-nya NULL — tepat tiga baris milik Dewi, Sari, dan Tono, yang ditampilkan terurut menurut nama. Pola ini diberi nama anti-join karena hasilnya justru lawan dari INNER JOIN: INNER JOIN menjawab “siapa yang punya pasangan”, anti-join menjawab “siapa yang tidak punya”. Kasus nyatanya sangat umum: pelanggan yang belum pernah memesan (calon target promosi), produk yang belum pernah terjual (calon clearance), atau pelamar yang belum pernah diundang interview.
Perbandingan pn.id IS NULL memakai operator IS NULL — bukan pn.id = NULL — karena pembandingan biasa dengan NULL selalu menghasilkan NULL yang dianggap tidak benar; perilaku logika tiga nilai ini sudah kita bahas tuntas di section filtering-dan-sorting. Kolom pn.id dipilih sebagai penanda karena kolom primary key tidak akan pernah bernilai NULL pada baris pesanan sungguhan, sehingga NULL di kolom itu hanya bisa berarti “baris kiri tanpa pasangan”.
COUNT pada LEFT JOIN: Memilih Kolom yang Dihitung #
LEFT JOIN sering dipasangkan GROUP BY untuk laporan “jumlah pesanan per pelanggan”. Di sinilah tersembunyi jebakan klasik: COUNT(*) menghitung baris hasil join — termasuk baris pengisi NULL — sedangkan COUNT(pn.id) hanya menghitung nilai pn.id yang tidak NULL. Kita buktikan keduanya dalam satu query:
SELECT p.nama,
COUNT(pn.id) AS jumlah_pesanan,
COUNT(*) AS jumlah_baris
FROM pelanggan p
LEFT JOIN pesanan pn ON p.id = pn.pelanggan_id
GROUP BY p.id, p.nama
ORDER BY p.nama;Output yang Dihasilkan:
+--------------+----------------+--------------+
| nama | jumlah_pesanan | jumlah_baris |
+--------------+----------------+--------------+
| Andi Wijaya | 1 | 1 |
| Budi Santoso | 3 | 3 |
| Dewi Lestari | 0 | 1 |
| Rina Melati | 1 | 1 |
| Sari Puspita | 0 | 1 |
| Siti Rahayu | 2 | 2 |
| Tono Hartono | 0 | 1 |
+--------------+----------------+--------------+
7 rows in set (0.00 sec)Hasilnya tujuh baris — setiap pelanggan pasti tampil, tujuan LEFT JOIN tercapai. Namun perhatikan dua kolom hitungannya: untuk pelanggan yang punya pesanan, COUNT(pn.id) dan COUNT(*) bernilai sama (Budi 3, Siti 2, Andi dan Rina 1). Untuk Dewi, Sari, dan Tono, COUNT(pn.id) melaporkan 0 — tidak ada pn.id yang tidak-NULL di kelompok mereka — sedangkan COUNT(*) melaporkan 1 karena kelompok mereka memang memuat satu baris: baris pengisi NULL buatan LEFT JOIN. Laporan yang benar untuk pertanyaan “berapa pesanan per pelanggan” harus memakai COUNT(pn.id); memakai COUNT(*) membuat setiap pelanggan diam-diam tercatat punya satu “pesanan” hantu.
Membersihkan Database Latihan #
DROP DATABASE belajar_join;Output yang Dihasilkan:
Query OK, 2 rows affected (0.00 sec)Referensi #
- JOIN Clause — MySQL 8.4 Reference Manual
- Aggregate Function Descriptions — MySQL 8.4 Reference Manual
- SELECT Statement — MySQL 8.4 Reference Manual
LEFT JOIN kini tampil utuh: baris kiri selalu hadir, kolom kanan diisi NULL ketika pasangan absen, dan pola IS NULL memberi kita daftar pelanggan tanpa pesanan. Logika LEFT JOIN sebenarnya simetris — MySQL menyediakan RIGHT JOIN yang mempertahankan sisi kanan. Chapter berikutnya membahas jenis join itu, sekaligus kebiasaan industri yang menyarankannya ditulis ulang sebagai LEFT JOIN.