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 barisNasib 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 falsedibuang

Skema contoh berikut sama dengan chapter sebelumnya sehingga hasilnya mudah dibandingkan:

sql buat-tabel.sql
-- 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:

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

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:

sql join-dasar.sql
-- 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:

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

  1. FROM pelanggan p — tabel pertama diberi alias satu huruf. Alias bukan gaya; tanpa alias, setiap kolom harus ditulis pelanggan.nama, pelanggan.id, dan seterusnya.
  2. INNER JOIN pesanan ps ON ps.pelanggan_id = p.id — jenis join, tabel kedua beserta aliasnya, lalu kondisi pasangan. Baris pesanan hanya menempel pada baris pelanggan bila pelanggan_id-nya sama dengan p.id.
  3. 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:

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

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

sql using.sql
-- 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:

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

  1. Bentuk ON d.email = p.email — hasil normal: kolom email dari kedua tabel bisa tampil dua kali bila keduanya dipilih.
  2. USING (email) dengan SELECT * — kolom email tampil satu kali saja, di posisi pertama, karena PostgreSQL menggabungkan kedua kolom penggabung menjadi satu kolom hasil. Kolom nama tetap tampil dua kali karena hanya email yang digabung.
  3. SELECT email, ... — kolom hasil USING boleh dirujuk tanpa kualifikasi alias, sesuatu yang dilarang pada kolom penggabung bentuk ON.
AspekON kiri.k = kanan.kUSING (k)
Syarat nama kolomboleh beda namaharus sama di kedua tabel
Kolom k di SELECT *tampil dua kalidigabung menjadi satu
Rujukan tanpa kualifikasierror ambigudiizinkan untuk kolom k
Kondisi tambahan (mis. AND total > 0)mudah ditambahkantidak 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:

sql join-filter.sql
-- 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:

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

  1. INNER JOIN ... ON ps.pelanggan_id = p.id — lima pesanan dipasangkan dengan pelanggan; pesanan 105 gugur karena tidak berpasangan, menyisakan empat baris gabungan.
  2. WHERE p.kota <> 'Jakarta' — dari empat baris itu, pesanan Siti Rahayu (103) dibuang karena kotanya Jakarta.
  3. ORDER BY ps.total DESC, ps.id — tiga baris tersisa diurutkan menurun berdasar total; ps.id menjadi 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
sql
-- 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
sql
-- 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
sql
-- 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
sql
-- 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
sql
-- 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
sql
-- 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;