Tim marketing Toko Maju Jaya menyimpan daftar kontak undangan newsletter; sistem penjualan menyimpan pelanggan terdaftar. Kedua daftar itu tumpang-tindih sebagian — dan pertanyaan yang muncul selalu simetris: kontak mana yang belum menjadi pelanggan, pelanggan mana yang belum masuk daftar kontak, dan siapa yang sudah ada di keduanya? INNER JOIN hanya menghasilkan irisan, LEFT atau RIGHT JOIN melindungi satu sisi; FULL OUTER JOIN adalah satu-satunya bentuk yang menampilkan kedua sisi sekaligus, menambal NULL di sisi yang tidak berpasangan. Chapter ini memakainya untuk pekerjaan paling klasiknya — merekonsiliasi dua daftar — lalu membangun pola selisih simetris dan membedah perbedaannya dengan UNION yang sering dikira padanannya.

Kedua Sisi Dipertahankan #

FULL OUTER JOIN menggabungkan dua janji sekaligus: seluruh baris tabel kiri tampil (seperti LEFT JOIN), dan seluruh baris tabel kanan juga tampil (seperti RIGHT JOIN). Baris yang berpasangan tergabung normal; baris yang tidak punya pasangan tetap hadir dengan sisi lawannya yang kosong. Satu query, tiga kelompok hasil:

Kelompok barisNasib di hasil FULL OUTER JOIN
Berpasangan (d.email = p.email)tampil, kolom kedua tabel digabung
Hanya ada di tabel kiritampil, kolom kanan NULL
Hanya ada di tabel kanantampil, kolom kiri NULL

Dengan empat jenis join yang sudah kita kenal, peta perlindungan baris kini lengkap:

JoinBaris yang dijamin tampil
INNER JOINhanya yang berpasangan
LEFT JOINseluruh tabel kiri
RIGHT JOINseluruh tabel kanan
FULL OUTER JOINseluruh kedua tabel

Skema contoh kali ini sengaja memakai relasi lembut email dari chapter Konsep Relasi Antartabel : dua baris kontak tanpa akun pelanggan (ratna, yusuf) berhadapan dengan dua pelanggan tanpa kontak terdaftar (andi, dewi):

sql buat-tabel.sql
-- Rekonsiliasi dua daftar: kontak marketing versus pelanggan terdaftar
CREATE TABLE daftar_email (
    email  text PRIMARY KEY,
    nama   text NOT NULL,
    sumber text NOT NULL
);

CREATE TABLE pelanggan (
    id    integer PRIMARY KEY,
    nama  text NOT NULL,
    kota  text NOT NULL,
    email text NOT NULL UNIQUE
);

INSERT INTO daftar_email (email, nama, sumber) VALUES
    ('[email protected]',  'Budi Santoso', 'newsletter'),
    ('[email protected]',  'Siti Rahayu',  'pameran'),
    ('[email protected]', 'Ratna Kusuma', 'newsletter'),
    ('[email protected]', 'Yusuf Halim',  'webinar');

INSERT INTO pelanggan (id, nama, kota, email) VALUES
    (1, 'Budi Santoso', 'Bandung',  '[email protected]'),
    (2, 'Siti Rahayu',  'Jakarta',  '[email protected]'),
    (3, 'Andi Wijaya',  'Surabaya', '[email protected]'),
    (4, 'Dewi Lestari', 'Medan',    '[email protected]');

SELECT * FROM daftar_email ORDER BY email;
SELECT * FROM pelanggan ORDER BY id;

Output yang Dihasilkan:

text
CREATE TABLE
CREATE TABLE
INSERT 0 4
INSERT 0 4
       email       |     nama     |   sumber   
-------------------+--------------+------------
 [email protected]  | Budi Santoso | newsletter
 [email protected] | Ratna Kusuma | newsletter
 [email protected]  | Siti Rahayu  | pameran
 [email protected] | Yusuf Halim  | webinar
(4 rows)

 id |     nama     |   kota   |      email       
----+--------------+----------+------------------
  1 | Budi Santoso | Bandung  | [email protected]
  2 | Siti Rahayu  | Jakarta  | [email protected]
  3 | Andi Wijaya  | Surabaya | [email protected]
  4 | Dewi Lestari | Medan    | [email protected]
(4 rows)

Merekonsiliasi Dua Daftar #

Relasi lembut seperti kesamaan email tidak dijaga foreign key — kesalahan ketik, perubahan domain, atau kontak yang belum mendaftar semuanya bisa membuat kedua daftar tidak sinkron. FULL OUTER JOIN menuntaskan pemeriksaan itu dalam satu query:

sql full-dasar.sql
-- FULL OUTER JOIN: baris dari kedua sisi tampil, sisi tanpa pasangan ditambal NULL
SELECT d.email AS email_daftar, d.nama AS nama_daftar,
       p.email AS email_pelanggan, p.nama AS nama_pelanggan, p.kota
FROM daftar_email d
FULL OUTER JOIN pelanggan p ON d.email = p.email
ORDER BY COALESCE(d.email, p.email);

Output yang Dihasilkan:

text
   email_daftar    | nama_daftar  | email_pelanggan  | nama_pelanggan |   kota   
-------------------+--------------+------------------+----------------+----------
                   |              | [email protected] | Andi Wijaya    | Surabaya
 [email protected]  | Budi Santoso | [email protected] | Budi Santoso   | Bandung
                   |              | [email protected] | Dewi Lestari   | Medan
 [email protected] | Ratna Kusuma |                  |                | 
 [email protected]  | Siti Rahayu  | [email protected] | Siti Rahayu    | Jakarta
 [email protected] | Yusuf Halim  |                  |                | 
(6 rows)

Enam baris hasil terbagi tiga kelompok yang persis seperti tabel di atas:

  1. [email protected] dan [email protected] — kedua kolom email terisi dan sama; kontak ini memang pelanggan terdaftar. Baris seperti inilah hasil rekonsiliasi yang sehat.
  2. ratna dan yusuf — kolom email_pelanggan kosong; kontak marketing ini belum pernah membuat akun. Kandidat follow-up penjualan.
  3. andi dan dewi — kolom email_daftar kosong; pelanggan ini belum tercatat di daftar kontak. Kandidat penambahan daftar newsletter.

Satu trik kecil di klausa urutan: ORDER BY COALESCE(d.email, p.email). Karena baris yatim hanya mengisi salah satu kolom email, mengurutkan berdasarkan satu kolom saja akan menimbun semua baris satu sisi di posisi NULL. COALESCE mengambil nilai pertama yang tidak NULL dari daftar argumennya — d.email untuk baris berpasangan dan kiri, p.email untuk baris kanan — sehingga keenam baris terurut rapi dalam satu daftar.

Pola Selisih Simetris #

Keluaran full join memuat semua kelompok, tetapi laporan rekonsiliasi sering hanya butuh yang bermasalah: baris yang ada di tepat satu sisi. Pola lengkapnya menggabungkan FULL OUTER JOIN dengan WHERE yang menguji kekosongan kedua sisi:

-- Selisih simetris: baris yang hanya ada di salah satu sisi
SELECT COALESCE(d.email, p.email) AS email,
       CASE WHEN d.email IS NULL
            THEN 'hanya di tabel pelanggan'
            ELSE 'hanya di tabel daftar_email' END AS status
FROM daftar_email d
FULL OUTER JOIN pelanggan p ON d.email = p.email
WHERE d.email IS NULL OR p.email IS NULL
ORDER BY 1;

Output yang Dihasilkan:

text
       email       |           status            
-------------------+-----------------------------
 [email protected]  | hanya di tabel pelanggan
 [email protected]  | hanya di tabel pelanggan
 [email protected] | hanya di tabel daftar_email
 [email protected] | hanya di tabel daftar_email
(4 rows)

Mari kita bedah tiga komponennya:

  1. WHERE d.email IS NULL OR p.email IS NULL — inti polanya. Baris berpasangan memiliki kedua email terisi, jadi syarat “salah satu kosong” hanya lolos pada baris yatim dari kedua arah. Inilah selisih simetris: gabungan “kiri tanpa kanan” dan “kanan tanpa kiri” dalam satu hasil.
  2. CASE WHEN d.email IS NULL ... END — pemeriksaan sisi mana yang kosong menentukan label status tiap baris, sehingga hasil siap dibaca langsung sebagai daftar tindak lanjut.
  3. ORDER BY 1 — mengurutkan menurut kolom pertama (email); hasil menjadi deterministik karena email hasil COALESCE unik di seluruh hasil (kedua tabel sama-sama menjadikan email sebagai kunci tunggal).

Pola ini juga bekerja pada relasi PK-FK biasa — misalnya pelanggan FULL OUTER JOIN pesanan ON ps.pelanggan_id = p.id dengan syarat p.id IS NULL OR ps.id IS NULL — untuk menemukan pelanggan tanpa pesanan dan pesanan tanpa pelanggan sekaligus. Pada data produksi yang dijaga foreign key, sisi “anak tanpa induk” tentu tidak akan pernah muncul; pola ini justru bersinar pada relasi lembut seperti daftar kontak, hasil import, atau pembandingan dua sistem.

FULL OUTER JOIN Bukan UNION #

Kedua perintah sama-sama diawali kata penggabung dan sering tertukar di percakapan. Bedanya fundamental: FULL OUTER JOIN menggabungkan secara horizontal (kolom berdampingan), UNION menumpuk secara vertikal (baris di bawah baris):

-- UNION: menumpuk baris secara vertikal, duplikat antar daftar digabung
SELECT email FROM daftar_email
UNION
SELECT email FROM pelanggan
ORDER BY email;

-- FULL OUTER JOIN: menggabungkan secara horizontal, pasangan tetap berdampingan
SELECT d.email AS email_daftar, p.email AS email_pelanggan
FROM daftar_email d
FULL OUTER JOIN pelanggan p ON d.email = p.email
ORDER BY COALESCE(d.email, p.email);

Output yang Dihasilkan:

Kedua hasil memuat enam email yang sama, tetapi wujud dan maknanya berbeda. UNION menghasilkan satu kolom — daftar email unik dari gabungan kedua tabel, dengan duplikat (budi, siti) otomatis digabungkan karena UNION memang melakukan penghilangan duplikat. FULL OUTER JOIN menghasilkan dua kolom — dan justru karena pasangan tetap berdampingan, kita bisa melihat email mana yang hanya ada di satu sisi. Ringkasnya:

AspekFULL OUTER JOINUNION
Arah penggabunganhorizontal — kolom berdampinganvertikal — baris bertumpuk
Bentuk hasilkolom dari kedua tabelsatu daftar kolom yang sama dari tiap SELECT
Baris yang cocokberdampingan dalam satu barisdigabung menjadi satu entri
Syarat bentuk querysatu klausa ONjumlah dan tipe kolom tiap SELECT harus cocok
Pertanyaan yang dijawab“sisi mana yang kosong dari tiap pasangan?”“apa saja gabungan nilainya?”

UNION adalah set operation — anggota keluarga UNION ALL, INTERSECT, dan EXCEPT yang bekerja pada hasil SELECT utuh. Keduanya bukan pesaing: UNION menjawab “gabungan nilai”, FULL OUTER JOIN menjawab “pemetaan pasangan”. Bila yang dibutuhkan hanya daftar email gabungan tanpa peduli asalnya, UNION lebih sederhana; begitu pertanyaan menyebut kata “bandingkan sisi ke sisi”, jawabannya full join.

Dengan FULL OUTER JOIN, keluarga outer join lengkap: kiri, kanan, dan keduanya. Masih ada satu anggota keluarga JOIN yang belum dibahas — yang tidak mencari pasangan sama sekali, melainkan memasangkan setiap baris dengan semua baris lainnya. CROSS JOIN adalah mesin kombinasi yang bisa menjadi alat pembangkit data yang elegan sekaligus bencana kartesian bila luput dari pengawasan.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

buat-tabel.sql

Unduh berkas
sql
-- Rekonsiliasi dua daftar: kontak marketing versus pelanggan terdaftar
CREATE TABLE daftar_email (
    email  text PRIMARY KEY,
    nama   text NOT NULL,
    sumber text NOT NULL
);

CREATE TABLE pelanggan (
    id    integer PRIMARY KEY,
    nama  text NOT NULL,
    kota  text NOT NULL,
    email text NOT NULL UNIQUE
);

INSERT INTO daftar_email (email, nama, sumber) VALUES
    ('[email protected]',  'Budi Santoso', 'newsletter'),
    ('[email protected]',  'Siti Rahayu',  'pameran'),
    ('[email protected]', 'Ratna Kusuma', 'newsletter'),
    ('[email protected]', 'Yusuf Halim',  'webinar');

INSERT INTO pelanggan (id, nama, kota, email) VALUES
    (1, 'Budi Santoso', 'Bandung',  '[email protected]'),
    (2, 'Siti Rahayu',  'Jakarta',  '[email protected]'),
    (3, 'Andi Wijaya',  'Surabaya', '[email protected]'),
    (4, 'Dewi Lestari', 'Medan',    '[email protected]');

SELECT * FROM daftar_email ORDER BY email;
SELECT * FROM pelanggan ORDER BY id;

full-dasar.sql

Unduh berkas
sql
-- FULL OUTER JOIN: baris dari kedua sisi tampil, sisi tanpa pasangan ditambal NULL
SELECT d.email AS email_daftar, d.nama AS nama_daftar,
       p.email AS email_pelanggan, p.nama AS nama_pelanggan, p.kota
FROM daftar_email d
FULL OUTER JOIN pelanggan p ON d.email = p.email
ORDER BY COALESCE(d.email, p.email);

full-vs-union.sql

Unduh berkas
sql
-- UNION: menumpuk baris secara vertikal, duplikat antar daftar digabung
SELECT email FROM daftar_email
UNION
SELECT email FROM pelanggan
ORDER BY email;

-- FULL OUTER JOIN: menggabungkan secara horizontal, pasangan tetap berdampingan
SELECT d.email AS email_daftar, p.email AS email_pelanggan
FROM daftar_email d
FULL OUTER JOIN pelanggan p ON d.email = p.email
ORDER BY COALESCE(d.email, p.email);

selisih-simetris.sql

Unduh berkas
sql
-- Selisih simetris: baris yang hanya ada di salah satu sisi
SELECT COALESCE(d.email, p.email) AS email,
       CASE WHEN d.email IS NULL
            THEN 'hanya di tabel pelanggan'
            ELSE 'hanya di tabel daftar_email' END AS status
FROM daftar_email d
FULL OUTER JOIN pelanggan p ON d.email = p.email
WHERE d.email IS NULL OR p.email IS NULL
ORDER BY 1;