Laporan “pesanan siapa saja yang masuk minggu ini” tidak bisa digali dari satu tabel: nama pelanggan hidup di pelanggan, angka transaksi hidup di pesanan. INNER JOIN adalah alat penggabung yang paling sering dipakai — dan juga yang paling ketat: hanya baris yang benar-benar berpasangan di kedua tabel yang lolos ke hasil. Ketatnya ini menguntungkan untuk laporan transaksi (tidak ada angka hantu), tetapi menjadi jebakan bila kita justru butuh melihat pelanggan yang diam tanpa pesanan. Chapter ini membedah mekanisme INNER JOIN dengan skema pelanggan–pesanan yang sama di seluruh section, mulai dari bentuk dasarnya, kualifikasi kolom dengan alias, sampai sintaks ringkas USING.
Cara Kerja INNER JOIN #
Secara konseptual, PostgreSQL memeriksa setiap kombinasi baris dari kedua tabel, mengevaluasi kondisi ON pada tiap kombinasi, dan hanya menyimpan kombinasi yang menghasilkan true. Baris dari tabel manapun yang tidak menemukan pasangan dibuang dari hasil sepenuhnya. Dengan data contoh kita, nasib tiap kelompok baris adalah:
| Kondisi baris | Nasib di hasil INNER JOIN |
|---|---|
pesanan dengan pelanggan_id yang cocok (101–104) | tampil, kolom kedua tabel digabung |
pesanan walk-in 105 (pelanggan_id NULL) | dibuang — NULL tidak pernah sama dengan nilai apa pun |
pelanggan tanpa pesanan (Andi Wijaya) | dibuang — tidak ada pasangan yang memenuhi ON |
Pasangan baris dengan ON bernilai false | dibuang |
Skema contoh berikut sama dengan chapter sebelumnya sehingga hasilnya mudah dibandingkan:
-- Skema contoh section JOIN: pelanggan, pesanan, daftar_email
CREATE TABLE pelanggan (
id integer PRIMARY KEY,
nama text NOT NULL,
kota text NOT NULL,
email text NOT NULL UNIQUE
);
CREATE TABLE pesanan (
id integer PRIMARY KEY,
pelanggan_id integer REFERENCES pelanggan (id),
tanggal date NOT NULL,
total numeric(12, 2) NOT NULL
);
CREATE TABLE daftar_email (
email text PRIMARY KEY,
nama text NOT NULL,
sumber text NOT NULL
);
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]');
INSERT INTO pesanan (id, pelanggan_id, tanggal, total) VALUES
(101, 1, DATE '2026-01-05', 150000.00),
(102, 1, DATE '2026-01-18', 75000.00),
(103, 2, DATE '2026-01-09', 220000.00),
(104, 4, DATE '2026-02-02', 50000.00),
(105, NULL, DATE '2026-02-10', 30000.00);
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');
SELECT * FROM pelanggan ORDER BY id;
SELECT * FROM pesanan ORDER BY id;
SELECT * FROM daftar_email ORDER BY email;Output yang Dihasilkan:
CREATE TABLE
CREATE TABLE
CREATE TABLE
INSERT 0 4
INSERT 0 5
INSERT 0 4
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)
id | pelanggan_id | tanggal | total
-----+--------------+------------+-----------
101 | 1 | 2026-01-05 | 150000.00
102 | 1 | 2026-01-18 | 75000.00
103 | 2 | 2026-01-09 | 220000.00
104 | 4 | 2026-02-02 | 50000.00
105 | | 2026-02-10 | 30000.00
(5 rows)
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)Menulis INNER JOIN Pertama #
Bentuk dasarnya membawa pola PK-FK dari chapter Konsep Relasi Antartabel
: tabel anak pesanan digabungkan ke tabel induk pelanggan lewat ON ps.pelanggan_id = p.id:
-- INNER JOIN: hanya pasangan yang memenuhi kondisi ON yang tampil
SELECT p.nama, ps.id AS pesanan, ps.tanggal, ps.total
FROM pelanggan p
INNER JOIN pesanan ps ON ps.pelanggan_id = p.id
ORDER BY ps.id;
-- Kata INNER boleh dihilangkan: JOIN saja sudah berarti INNER JOIN
SELECT p.nama, ps.id AS pesanan, ps.tanggal, ps.total
FROM pelanggan p
JOIN pesanan ps ON ps.pelanggan_id = p.id
ORDER BY ps.id;Output yang Dihasilkan:
nama | pesanan | tanggal | total
--------------+---------+------------+-----------
Budi Santoso | 101 | 2026-01-05 | 150000.00
Budi Santoso | 102 | 2026-01-18 | 75000.00
Siti Rahayu | 103 | 2026-01-09 | 220000.00
Dewi Lestari | 104 | 2026-02-02 | 50000.00
(4 rows)
nama | pesanan | tanggal | total
--------------+---------+------------+-----------
Budi Santoso | 101 | 2026-01-05 | 150000.00
Budi Santoso | 102 | 2026-01-18 | 75000.00
Siti Rahayu | 103 | 2026-01-09 | 220000.00
Dewi Lestari | 104 | 2026-02-02 | 50000.00
(4 rows)Mari kita bedah komponen kuncinya:
FROM pelanggan p— tabel pertama diberi alias satu huruf. Alias bukan gaya; tanpa alias, setiap kolom harus ditulispelanggan.nama,pelanggan.id, dan seterusnya.INNER JOIN pesanan ps ON ps.pelanggan_id = p.id— jenis join, tabel kedua beserta aliasnya, lalu kondisi pasangan. Barispesananhanya menempel pada barispelangganbilapelanggan_id-nya sama denganp.id.ORDER BY ps.id— id pesanan unik, jadi urutan hasil deterministik; kebiasaan ini dipakai konsisten di seluruh section.
Kedua query menghasilkan empat baris yang identik. Kata kunci INNER murni penegasan — JOIN tanpa imbuhan sudah berarti INNER JOIN di PostgreSQL (dan di SQL standar). Banyak penulis menulis kata INNER tetap agar pembaca langsung tahu bahwa baris tanpa pasangan memang sengaja dibuang, bukan kelalaian.
Perhatikan juga dua baris yang hilang dari hasil: pesanan 105 dan pelanggan Andi Wijaya. INNER JOIN tidak menampilkan baris yang tidak berpasangan — itulah alasan hasilnya “hanya transaksi berpelanggan”. Bila laporan Anda justru butuh pelanggan yang sepi pesanan, chapter LEFT JOIN membahas persis pola itu.
Kualifikasi Kolom dan Alias Tabel #
Begitu dua tabel bergabung, kolom bernama sama dari dua sisi hidup berdampingan di satu namespace. pelanggan dan pesanan sama-sama punya kolom id; merujuknya tanpa kualifikasi memancing error:
-- Kedua tabel memiliki kolom bernama id: rujukan tanpa kualifikasi jadi ambigu
SELECT id, nama
FROM pelanggan p
INNER JOIN pesanan ps ON ps.pelanggan_id = p.id;Output yang Dihasilkan:
psql:kolom-ambigu.sql:4: ERROR: column reference "id" is ambiguous
LINE 1: SELECT id, nama
^PostgreSQL menolak query di atas dengan column reference "id" is ambiguous karena id bisa berarti p.id maupun ps.id. Solusinya sederhana: sebutkan asal kolomnya melalui alias tabel:
-- Perbaikannya: kualifikasi setiap kolom dengan alias tabel asalnya
SELECT p.id, p.nama, ps.id AS pesanan, ps.total
FROM pelanggan p
INNER JOIN pesanan ps ON ps.pelanggan_id = p.id
ORDER BY ps.id;Output yang Dihasilkan:
id | nama | pesanan | total
----+--------------+---------+-----------
1 | Budi Santoso | 101 | 150000.00
1 | Budi Santoso | 102 | 75000.00
2 | Siti Rahayu | 103 | 220000.00
4 | Dewi Lestari | 104 | 50000.00
(4 rows)Query yang sama kini berjalan dan menampilkan p.id berdampingan dengan ps.id AS pesanan. Perhatikan bahwa nama pada query error sebelumnya tidak kena masalah karena hanya pelanggan yang memilikinya — namun menulis p.nama tetap lebih aman karena tabel lain bisa saja menambahkan kolom bernama sama di kemudian hari.
Sintaks USING untuk Kolom Penggabung Bernama Sama #
Kondisi ON paling sering berbentuk “kolom kiri sama dengan kolom kanan”. Bila kedua kolom itu bernama persis sama, PostgreSQL menyediakan bentuk ringkas USING (nama_kolom). Relasi lembut daftar_email.email = pelanggan.email memenuhi syaratnya:
-- Bentuk ON: kolom penggabung bernama berbeda, harus ditulis eksplisit
SELECT d.nama AS nama_daftar, p.nama AS nama_pelanggan, p.kota
FROM daftar_email d
INNER JOIN pelanggan p ON d.email = p.email
ORDER BY p.id;
-- Bentuk USING: kolom penggabung bernama sama, outputnya digabung jadi satu
SELECT *
FROM daftar_email d
INNER JOIN pelanggan p USING (email)
ORDER BY p.id;
-- Kolom hasil USING boleh dirujuk tanpa kualifikasi
SELECT email, p.nama AS nama_pelanggan, d.sumber
FROM daftar_email d
INNER JOIN pelanggan p USING (email)
ORDER BY p.id;Output yang Dihasilkan:
nama_daftar | nama_pelanggan | kota
--------------+----------------+---------
Budi Santoso | Budi Santoso | Bandung
Siti Rahayu | Siti Rahayu | Jakarta
(2 rows)
email | nama | sumber | id | nama | kota
------------------+--------------+------------+----+--------------+---------
[email protected] | Budi Santoso | newsletter | 1 | Budi Santoso | Bandung
[email protected] | Siti Rahayu | pameran | 2 | Siti Rahayu | Jakarta
(2 rows)
email | nama_pelanggan | sumber
------------------+----------------+------------
[email protected] | Budi Santoso | newsletter
[email protected] | Siti Rahayu | pameran
(2 rows)Tiga query di atas memperlihatkan perilaku USING tahap demi tahap:
- Bentuk
ON d.email = p.email— hasil normal: kolomemaildari kedua tabel bisa tampil dua kali bila keduanya dipilih. USING (email)denganSELECT *— kolomemailtampil satu kali saja, di posisi pertama, karena PostgreSQL menggabungkan kedua kolom penggabung menjadi satu kolom hasil. Kolomnamatetap tampil dua kali karena hanyaemailyang digabung.SELECT email, ...— kolom hasilUSINGboleh dirujuk tanpa kualifikasi alias, sesuatu yang dilarang pada kolom penggabung bentukON.
| Aspek | ON kiri.k = kanan.k | USING (k) |
|---|---|---|
| Syarat nama kolom | boleh beda nama | harus sama di kedua tabel |
Kolom k di SELECT * | tampil dua kali | digabung menjadi satu |
| Rujukan tanpa kualifikasi | error ambigu | diizinkan untuk kolom k |
Kondisi tambahan (mis. AND total > 0) | mudah ditambahkan | tidak bisa — hanya kesamaan kolom |
USING cocok untuk relasi yang kolomnya memang sengaja dinamai sama (konvensi yang baik untuk FK bernama sama dengan PK-nya). Begitu kolom penggabung bernama berbeda — seperti pesanan.pelanggan_id terhadap pelanggan.id — ON adalah satu-satunya pilihan.
Dipadukan dengan WHERE dan ORDER BY #
Query dunia nyata jarang berhenti di join: hasil gabungan perlu difilter dan diurutkan. Klausa-klausa itu bekerja berurutan — JOIN menggabungkan, WHERE menyaring baris hasil, ORDER BY mengurutkan sisanya:
-- INNER JOIN + WHERE + ORDER BY dalam satu query
SELECT p.nama, p.kota, ps.id AS pesanan, ps.total
FROM pelanggan p
INNER JOIN pesanan ps ON ps.pelanggan_id = p.id
WHERE p.kota <> 'Jakarta'
ORDER BY ps.total DESC, ps.id;Output yang Dihasilkan:
nama | kota | pesanan | total
--------------+---------+---------+-----------
Budi Santoso | Bandung | 101 | 150000.00
Budi Santoso | Bandung | 102 | 75000.00
Dewi Lestari | Medan | 104 | 50000.00
(3 rows)Tiga baris hasil adalah pesanan dari pelanggan di luar Jakarta, terurut dari total terbesar. Bedah singkatnya:
INNER JOIN ... ON ps.pelanggan_id = p.id— lima pesanan dipasangkan dengan pelanggan; pesanan 105 gugur karena tidak berpasangan, menyisakan empat baris gabungan.WHERE p.kota <> 'Jakarta'— dari empat baris itu, pesanan Siti Rahayu (103) dibuang karena kotanya Jakarta.ORDER BY ps.total DESC, ps.id— tiga baris tersisa diurutkan menurun berdasar total;ps.idmenjadi pemutus urutan bila total sama, menjaga hasil tetap deterministik.
Filter di WHERE boleh menyentuh kolom dari kedua tabel — PostgreSQL menguji kondisinya pada baris hasil join, bukan sebelum penggabungan. Interaksi urutan ini baru terasa kritis di LEFT JOIN, ketika filter di WHERE diam-diam mengubah makna join itu sendiri.
INNER JOIN menjawab kebutuhan “yang berpasangan saja”. Laporan berikutnya biasanya bertanya sebaliknya: pelanggan mana yang belum pernah memesan? Untuk pertanyaan itu kita butuh join yang mempertahankan baris tanpa pasangan — tugas chapter LEFT JOIN .
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
buat-tabel.sql
Unduh berkas-- Skema contoh section JOIN: pelanggan, pesanan, daftar_email
CREATE TABLE pelanggan (
id integer PRIMARY KEY,
nama text NOT NULL,
kota text NOT NULL,
email text NOT NULL UNIQUE
);
CREATE TABLE pesanan (
id integer PRIMARY KEY,
pelanggan_id integer REFERENCES pelanggan (id),
tanggal date NOT NULL,
total numeric(12, 2) NOT NULL
);
CREATE TABLE daftar_email (
email text PRIMARY KEY,
nama text NOT NULL,
sumber text NOT NULL
);
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]');
INSERT INTO pesanan (id, pelanggan_id, tanggal, total) VALUES
(101, 1, DATE '2026-01-05', 150000.00),
(102, 1, DATE '2026-01-18', 75000.00),
(103, 2, DATE '2026-01-09', 220000.00),
(104, 4, DATE '2026-02-02', 50000.00),
(105, NULL, DATE '2026-02-10', 30000.00);
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');
SELECT * FROM pelanggan ORDER BY id;
SELECT * FROM pesanan ORDER BY id;
SELECT * FROM daftar_email ORDER BY email;
join-dasar.sql
Unduh berkas-- INNER JOIN: hanya pasangan yang memenuhi kondisi ON yang tampil
SELECT p.nama, ps.id AS pesanan, ps.tanggal, ps.total
FROM pelanggan p
INNER JOIN pesanan ps ON ps.pelanggan_id = p.id
ORDER BY ps.id;
-- Kata INNER boleh dihilangkan: JOIN saja sudah berarti INNER JOIN
SELECT p.nama, ps.id AS pesanan, ps.tanggal, ps.total
FROM pelanggan p
JOIN pesanan ps ON ps.pelanggan_id = p.id
ORDER BY ps.id;
join-filter.sql
Unduh berkas-- INNER JOIN + WHERE + ORDER BY dalam satu query
SELECT p.nama, p.kota, ps.id AS pesanan, ps.total
FROM pelanggan p
INNER JOIN pesanan ps ON ps.pelanggan_id = p.id
WHERE p.kota <> 'Jakarta'
ORDER BY ps.total DESC, ps.id;
kolom-ambigu.sql
Unduh berkas-- Kedua tabel memiliki kolom bernama id: rujukan tanpa kualifikasi jadi ambigu
SELECT id, nama
FROM pelanggan p
INNER JOIN pesanan ps ON ps.pelanggan_id = p.id;
kolom-kualifikasi.sql
Unduh berkas-- Perbaikannya: kualifikasi setiap kolom dengan alias tabel asalnya
SELECT p.id, p.nama, ps.id AS pesanan, ps.total
FROM pelanggan p
INNER JOIN pesanan ps ON ps.pelanggan_id = p.id
ORDER BY ps.id;
using.sql
Unduh berkas-- Bentuk ON: kolom penggabung bernama berbeda, harus ditulis eksplisit
SELECT d.nama AS nama_daftar, p.nama AS nama_pelanggan, p.kota
FROM daftar_email d
INNER JOIN pelanggan p ON d.email = p.email
ORDER BY p.id;
-- Bentuk USING: kolom penggabung bernama sama, outputnya digabung jadi satu
SELECT *
FROM daftar_email d
INNER JOIN pelanggan p USING (email)
ORDER BY p.id;
-- Kolom hasil USING boleh dirujuk tanpa kualifikasi
SELECT email, p.nama AS nama_pelanggan, d.sumber
FROM daftar_email d
INNER JOIN pelanggan p USING (email)
ORDER BY p.id;