TL;DR
FULL OUTER JOIN itu perintah SQL yang gabungin dua tabel dan nampilin semua baris dari keduanya, baik yang punya pasangan maupun yang nggak. Baris yang gak nemu pasangan tetap muncul, dengan kolom dari tabel sebelahnya diisi NULL. Fungsi ini paling berguna buat rekonsiliasi: nemuin data yang cuma ada di satu tabel dan hilang di tabel satunya. PostgreSQL dan SQL Server dukung langsung, sementara MySQL harus ditiru pakai kombinasi LEFT JOIN dan RIGHT JOIN yang digabung UNION.
FULL OUTER JOIN nampilin semua baris dari dua tabel sekaligus. Yang punya pasangan digabung jadi satu baris. Yang gak punya pasangan tetap muncul, tapi kolom dari tabel sebelahnya diisi NULL.
Ini JOIN yang paling jarang dipakai sehari-hari. Tapi pas lagi rekonsiliasi data (misalnya stok sistem gak cocok sama stok fisik), dia satu-satunya yang bisa nunjukin dua arah selisihnya sekaligus.
FULL OUTER JOIN itu jenis JOIN yang nampilin semua baris dari tabel kiri dan semua baris dari tabel kanan sekaligus. Baris yang cocok berdasarkan kondisi ON digabung. Baris yang gak nemu pasangan tetap ikut, dengan kolom dari tabel lawannya diisi NULL.
Sintaksnya:
SELECT kolom
FROM tabel_kiri a
FULL OUTER JOIN tabel_kanan b
ON a.kunci = b.kunci;
Kata OUTER boleh dibuang. FULL JOIN artinya sama persis. Ini juga berlaku di LEFT JOIN dan RIGHT JOIN.
Gampangnya, dia gabungan dari LEFT JOIN dan RIGHT JOIN, tapi baris yang muncul di dua-duanya gak digandain.
Ada tiga jenis baris di hasilnya:
Bandingin sama JOIN yang lain:
| Jenis JOIN | Baris yang cocok | Baris yatim kiri | Baris yatim kanan |
|---|---|---|---|
| INNER JOIN | Ikut | Dibuang | Dibuang |
| LEFT JOIN | Ikut | Ikut (NULL) | Dibuang |
| RIGHT JOIN | Ikut | Dibuang | Ikut (NULL) |
| FULL OUTER JOIN | Ikut | Ikut (NULL) | Ikut (NULL) |
Cuma FULL OUTER JOIN yang gak buang apa pun.
Ini kasus yang bikin FULL OUTER JOIN kepakai beneran.
Toko Berkah punya dua tabel. produk berisi 5 barang yang terdaftar di sistem. stok_opname berisi hasil hitung fisik gudang akhir Januari.
Bikin tabelnya dulu:
CREATE TABLE produk (
produk_id INT PRIMARY KEY,
nama_produk VARCHAR(100) NOT NULL,
kategori VARCHAR(50),
harga DECIMAL(12,2)
);
CREATE TABLE stok_opname (
opname_id INT PRIMARY KEY,
produk_id INT,
tanggal DATE,
qty_fisik INT
);
Isi datanya:
INSERT INTO produk (produk_id, nama_produk, kategori, harga) VALUES
(1, 'Beras Pandan Wangi 5kg', 'Sembako', 78000),
(2, 'Minyak Goreng 2L', 'Sembako', 36000),
(3, 'Kopi Gayo 250g', 'Minuman', 65000),
(4, 'Gula Aren 500g', 'Sembako', 28000),
(5, 'Teh Melati 100g', 'Minuman', 15000);
INSERT INTO stok_opname (opname_id, produk_id, tanggal, qty_fisik) VALUES
(101, 1, '2026-01-31', 42),
(102, 2, '2026-01-31', 18),
(103, 3, '2026-01-31', 7),
(104, 9, '2026-01-31', 25),
(105, 12, '2026-01-31', 3);
Liat baris terakhir. Ada produk_id 9 dan 12 di hasil opname, padahal di tabel produk cuma ada ID 1 sampai 5. Barang siluman.
Sekarang gabungin:
SELECT
p.produk_id AS id_di_master,
p.nama_produk,
s.produk_id AS id_di_opname,
s.qty_fisik
FROM produk p
FULL OUTER JOIN stok_opname s
ON p.produk_id = s.produk_id
ORDER BY COALESCE(p.produk_id, s.produk_id);
Hasilnya:
| id_di_master | nama_produk | id_di_opname | qty_fisik |
|---|---|---|---|
| 1 | Beras Pandan Wangi 5kg | 1 | 42 |
| 2 | Minyak Goreng 2L | 2 | 18 |
| 3 | Kopi Gayo 250g | 3 | 7 |
| 4 | Gula Aren 500g | NULL | NULL |
| 5 | Teh Melati 100g | NULL | NULL |
| NULL | NULL | 9 | 25 |
| NULL | NULL | 12 | 3 |
Dua masalah langsung kelihatan dari satu query:
Coba pakai INNER JOIN. Kamu cuma dapat 3 baris pertama, dan semua masalah tadi hilang tanpa jejak.
Buat laporan rekonsiliasi, saring yang cocok:
SELECT
COALESCE(p.produk_id, s.produk_id) AS produk_id,
COALESCE(p.nama_produk, '(tidak ada di master)') AS nama_produk,
CASE
WHEN s.produk_id IS NULL THEN 'Belum di-opname'
WHEN p.produk_id IS NULL THEN 'Barang siluman'
END AS masalah
FROM produk p
FULL OUTER JOIN stok_opname s
ON p.produk_id = s.produk_id
WHERE p.produk_id IS NULL
OR s.produk_id IS NULL;
Empat baris keluar. Itu daftar kerjaan tim gudang besok pagi.
COALESCE di sini penting. Dia ngambil nilai pertama yang bukan NULL, jadi kolom produk_id selalu keisi dari sisi mana pun datanya datang. Detailnya ada di panduan COALESCE.
MySQL gak punya sintaks FULL OUTER JOIN. Sampai versi 8.4 pun belum ada.
Tirunya pakai LEFT JOIN plus RIGHT JOIN yang disatuin UNION:
SELECT p.produk_id, p.nama_produk, s.qty_fisik
FROM produk p
LEFT JOIN stok_opname s ON p.produk_id = s.produk_id
UNION
SELECT p.produk_id, p.nama_produk, s.qty_fisik
FROM produk p
RIGHT JOIN stok_opname s ON p.produk_id = s.produk_id;
UNION otomatis buang baris duplikat, jadi baris yang cocok di dua tabel gak muncul dua kali. Kalau kamu pakai UNION ALL tanpa filter, baris yang cocok bakal kegandain.
PostgreSQL dan SQL Server sih dukung penuh. Kamu bisa cek di dokumentasi resmi PostgreSQL soal table expressions.
Ini yang paling sering.
-- SALAH: FULL OUTER JOIN-nya jadi percuma
FROM produk p
FULL OUTER JOIN stok_opname s ON p.produk_id = s.produk_id
WHERE s.qty_fisik > 0;
Baris yatim dari tabel produk punya qty_fisik = NULL. NULL gak lolos perbandingan > 0, jadi semuanya kebuang. Hasil akhirnya sama kayak INNER JOIN.
Kalau kondisinya buat nyaring pasangan, taruh di ON. Kalau buat nyaring hasil akhir, baru di WHERE.
SELECT p.produk_id bakal NULL buat baris yang cuma ada di tabel kanan. Bungkus pakai COALESCE(p.produk_id, s.produk_id) biar selalu keisi.
Baris NULL numpuk di ujung dan bikin hasilnya susah dibaca. Sort pakai COALESCE juga.
FULL OUTER JOIN harus scan dari dua sisi. Tanpa index di kolom ON, query yang tadinya 200 milidetik bisa jadi 30 detik di tabel ratusan ribu baris.
INNER JOIN cuma nampilin baris yang punya pasangan di kedua tabel. Baris yatim langsung dibuang tanpa pemberitahuan. FULL OUTER JOIN nampilin semua baris dari dua tabel, termasuk yang gak punya pasangan, dan ngisi kolom yang kosong pakai NULL. Kalau tujuan kamu nemuin data yang hilang, INNER JOIN justru nyembunyiin masalahnya.
MySQL emang gak pernah mengimplementasikan sintaks FULL OUTER JOIN, sampai versi 8.4 pun belum ada. Solusinya, gabungkan LEFT JOIN dan RIGHT JOIN pakai UNION. UNION otomatis buang baris duplikat, jadi baris yang cocok di kedua tabel gak muncul dua kali.
Sama persis. Kata OUTER itu opsional di standar SQL, jadi FULL JOIN dan FULL OUTER JOIN dieksekusi identik oleh PostgreSQL maupun SQL Server. Aku sendiri lebih suka nulis lengkap pakai OUTER, biar orang yang baca query-nya nanti langsung tau itu bukan INNER JOIN.
Tambahin WHERE yang ngecek NULL di kolom kunci kedua tabel: WHERE a.id IS NULL OR b.id IS NULL. Baris yang cocok pasti punya nilai di dua kolom itu, jadi otomatis kesaring keluar. Sisanya cuma baris yatim dari kiri dan kanan. Ini pola standar buat laporan rekonsiliasi.
Lebih berat dari INNER JOIN, soalnya database harus nyimpen baris yang gak match dari dua sisi sekaligus. Di data kecil sampai menengah selisihnya gak kerasa. Yang bikin lambat biasanya bukan JOIN-nya, tapi kolom kunci yang gak diindeks. Pastiin kolom di klausa ON punya index di kedua tabel.
Yang perlu diinget:
Query di atas bisa langsung kamu copy dan jalanin. Ganti nama tabelnya sama data kerjaan kamu, terus liat berapa baris yatim yang selama ini gak keitung.
Mau langsung praktek tanpa install database? Latihan JOIN interaktifnya ada di halaman fungsi FULL OUTER JOIN. Definisi istilah NULL dan baris yatim juga aku tulis di glosarium NULL.
Lanjut baca: CROSS JOIN SQL, satu-satunya JOIN yang gak butuh klausa ON sama sekali.
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Analisa per kuartal, hari kerja, atau musim jadi ribet kalau tiap query ngitung ulang atribut tanggal. Tabel kalender nyimpen semua atribut itu sekali, biar tinggal di-JOIN.
Laporan penjualan harian sering bolong di hari tanpa transaksi. Ini cara bikin deret tanggal lengkap di SQL biar tiap hari muncul, walau nilainya nol.
Struktur organisasi tersimpan sebagai kolom id_atasan yang saling nunjuk. Ini cara narik rantai jabatan, hitung total bawahan, dan span of control cuma pakai SQL.