Semua join sejauh ini menggabungkan dua tabel yang berbeda. Ada satu keluarga pertanyaan yang tidak bisa dijawab begitu: “siapa nama manajer setiap pegawai?” padahal nama manajer tersimpan di tabel yang sama — tabel pegawai. Kolom manajer_id di setiap baris menunjuk ke baris lain di tabel yang sama, membentuk relasi yang menunjuk dirinya sendiri. Solusinya bernama self join: satu tabel di-join dengan dirinya, dan agar kedua “salinan” tidak tertukar, masing-masing wajib memakai alias berbeda. Pada chapter ini kita membangun tabel pegawai kecil berisi struktur atasan, membuktikan kenapa alias wajib, menangani pegawai puncak yang tidak punya manajer, lalu memanfaatkan self join untuk mencari rekan kerja satu atasan.

Menyiapkan Tabel Pegawai #

Kita tinggalkan sebentar pasangan pelanggan–pesanan dan memakai satu tabel pegawai berisi tujuh baris. Kolom manajer_id menyimpan nomor id pegawai lain yang menjadi atasannya — dan karena menunjuk baris di tabel yang sama, kolom ini menjadi foreign key ke tabelnya sendiri:

sql
DROP DATABASE IF EXISTS belajar_join;
CREATE DATABASE belajar_join;
USE belajar_join;

CREATE TABLE pegawai (
  id INT AUTO_INCREMENT PRIMARY KEY,
  nama VARCHAR(50) NOT NULL,
  jabatan VARCHAR(40) NOT NULL,
  manajer_id INT NULL,
  FOREIGN KEY (manajer_id) REFERENCES pegawai(id)
);

INSERT INTO pegawai (nama, jabatan, manajer_id) VALUES
  ('Achmad Fauzi',  'Direktur',          NULL),
  ('Nurul Hidayah', 'Manajer Penjualan', 1),
  ('Budi Santoso',  'Staf Penjualan',    2),
  ('Siti Rahayu',   'Staf Penjualan',    2),
  ('Rizky Pratama', 'Manajer Gudang',    1),
  ('Dewi Lestari',  'Staf Gudang',       5),
  ('Tono Hartono',  'Staf Gudang',       5);

Output yang Dihasilkan:

text
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, 7 rows affected (0.00 sec)
Records: 7  Duplicates: 0  Warnings: 0

Struktur organisasinya sederhana: Achmad sang direktur berada di puncak sehingga kolom manajer_id-nya NULL — tidak ada siapa pun di atasnya. Nurul dan Rizky adalah manajer yang mengarah langsung ke Achmad, sedangkan Budi, Siti, Dewi, dan Tono adalah staf yang masing-masing mengarah ke manajernya. Urutan penyisipan baris sengaja mengikuti hierarki dari atas ke bawah karena foreign key memeriksa setiap baris saat disisipkan: baris manajer harus sudah ada sebelum bawahannya bisa menunjuknya. Relasi ini pun tetap 1-N seperti pelanggan–pesanan, hanya saja sisi “satu” dan sisi “banyak” tinggal di satu tabel yang sama.

Alias Bukan Pilihan, Tapi Kebutuhan #

Coba kita tulis query join tanpa alias — tabel pegawai disebut dua kali apa adanya:

sql
-- @error
SELECT pegawai.nama FROM pegawai JOIN pegawai ON pegawai.manajer_id = pegawai.id;

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

ERROR 1066 (42000): Not unique table/alias: 'pegawai'
Bye

Server menolak dengan ERROR 1066: Not unique table/alias. MySQL menolak nama tabel yang sama muncul dua kali di FROM karena tidak ada cara menjawab pertanyaan “pegawai yang mana?” saat mengevaluasi pegawai.manajer_id — yang kiri atau yang kanan. Jalan keluarnya adalah memberi setiap salinan nama panggilan sendiri:

sql
SELECT p.nama AS pegawai, p.jabatan, m.nama AS manajer
FROM pegawai p
INNER JOIN pegawai m ON p.manajer_id = m.id
ORDER BY p.id;

Output yang Dihasilkan:

text
+---------------+-------------------+---------------+
| pegawai       | jabatan           | manajer       |
+---------------+-------------------+---------------+
| Nurul Hidayah | Manajer Penjualan | Achmad Fauzi  |
| Budi Santoso  | Staf Penjualan    | Nurul Hidayah |
| Siti Rahayu   | Staf Penjualan    | Nurul Hidayah |
| Rizky Pratama | Manajer Gudang    | Achmad Fauzi  |
| Dewi Lestari  | Staf Gudang       | Rizky Pratama |
| Tono Hartono  | Staf Gudang       | Rizky Pratama |
+---------------+-------------------+---------------+
6 rows in set (0.00 sec)

Mari kita bedah query ini baris per baris, karena pola p/m inilah inti self join:

  1. Baris FROM pegawai p: salinan pertama tabel diberi alias p — salinan ini berperan sebagai “pegawai yang sedang kita daftar”.
  2. Baris INNER JOIN pegawai m ON p.manajer_id = m.id: salinan kedua tabel yang sama diberi alias m — salinan ini berperan sebagai “manajer”. Kondisi p.manajer_id = m.id memasangkan setiap baris pegawai dengan tepat satu baris manajernya; secara teknis MySQL membaca tabel itu dua kali, dan dua salinan itu sungguh saling bebas.
  3. Baris SELECT p.nama AS pegawai, m.nama AS manajer: dua kolom nama yang berasal dari tabel yang sama dibedakan lewat awalan alias — tanpa awalan p. dan m., query ini ambigu dan ditolak.
  4. Baris ORDER BY p.id: mengurutkan mengikuti pegawai, sehingga hasil stabil setiap kali dijalankan.

Hasilnya enam baris: setiap pegawai tampil berdampingan dengan nama manajernya — Budi dan Siti sama-sama menunjuk Nurul, Dewi dan Tono sama-sama menunjuk Rizky, Nurul dan Rizky menunjuk Achmad. Nama yang berulang di kolom manajer adalah tanda relasi 1-N di dalam satu tabel bekerja dengan baik.

LEFT Self Join untuk Manajer NULL #

Enam baris hasil di atas tidak memuat Achmad. Sebagai INNER JOIN, self join ini hanya meloloskan pegawai yang manajer_id-nya menemukan pasangan — sedangkan manajer_id Achmad bernilai NULL, dan NULL tidak pernah cocok dengan m.id mana pun. Ganti jenis join-nya menjadi LEFT JOIN supaya pegawai puncak ikut tampil:

sql
SELECT p.nama AS pegawai, p.jabatan, m.nama AS manajer
FROM pegawai p
LEFT JOIN pegawai m ON p.manajer_id = m.id
ORDER BY p.id;

Output yang Dihasilkan:

text
+---------------+-------------------+---------------+
| pegawai       | jabatan           | manajer       |
+---------------+-------------------+---------------+
| Achmad Fauzi  | Direktur          | NULL          |
| Nurul Hidayah | Manajer Penjualan | Achmad Fauzi  |
| Budi Santoso  | Staf Penjualan    | Nurul Hidayah |
| Siti Rahayu   | Staf Penjualan    | Nurul Hidayah |
| Rizky Pratama | Manajer Gudang    | Achmad Fauzi  |
| Dewi Lestari  | Staf Gudang       | Rizky Pratama |
| Tono Hartono  | Staf Gudang       | Rizky Pratama |
+---------------+-------------------+---------------+
7 rows in set (0.00 sec)

Hasilnya kini tujuh baris. Baris Achmad tampil dengan kolom manajer bernilai NULL — persis perilaku LEFT JOIN yang sudah kita kenal di chapter sebelumnya: baris kiri tanpa pasangan tetap hadir dengan kolom kanan diisi NULL. Pola LEFT JOIN pada self join ini persis pola LEFT JOIN dua tabel berbeda; satu-satunya pembeda adalah kedua sisi join berasal dari tabel yang sama. Bila kebutuhan laporan meminta manajer puncak tampil dengan label khusus, fungsi IFNULL(m.nama, 'puncak organisasi') dapat membungkus kolom manajer — teknik IFNULL sudah kita kuasai di section fungsi SQL.

Membandingkan Baris dalam Satu Tabel #

Self join punya satu pola lanjutan yang sering terlupakan: memasangkan baris dengan baris lain di tabel yang sama untuk membandingkan keduanya. Contoh klasiknya mencari rekan kerja — dua pegawai berbeda yang atasan-atasannya orang sama. Kuncinya ada di kondisi ON yang memuat dua syarat:

sql
SELECT p1.nama AS pegawai, p2.nama AS rekan, p1.manajer_id AS atasan
FROM pegawai p1
INNER JOIN pegawai p2
  ON p1.manajer_id = p2.manajer_id AND p1.id < p2.id
ORDER BY p1.id;

Output yang Dihasilkan:

text
+---------------+---------------+--------+
| pegawai       | rekan         | atasan |
+---------------+---------------+--------+
| Nurul Hidayah | Rizky Pratama |      1 |
| Budi Santoso  | Siti Rahayu   |      2 |
| Dewi Lestari  | Tono Hartono  |      5 |
+---------------+---------------+--------+
3 rows in set (0.00 sec)

Kita bedah kondisi ON-nya dengan saksama karena di sinilah kekuatan pola ini:

  1. p1.manajer_id = p2.manajer_id: dua baris dipasangkan bila atasan mereka sama. Syarat ini saja belum cukup — setiap pegawai juga cocok dengan dirinya sendiri (baris p1 dan p2 bisa menunjuk baris yang sama).
  2. p1.id < p2.id: syarat kedua memastikan p1 selalu pegawai bernomor lebih kecil, sehingga pasangan tidak pernah terdiri dari pegawai dengan dirinya sendiri, dan tiap pasangan hanya muncul sekali — cukup Budi/Siti, tidak perlu juga Siti/Budi.
  3. SELECT ... p1.manajer_id AS atasan: nomor atasan ikut ditampilkan agar terlihat bahwa kedua rekan memang bernaung di bawah manajer yang sama.

Hasilnya tiga pasang rekan: Nurul dan Rizky sama-sama manajer yang mengarah langsung ke direktur bernomor 1 (Achmad), Budi dan Siti sama-sama staf penjualan di bawah manajer bernomor 2 (Nurul), sedangkan Dewi dan Tono sama-sama staf gudang di bawah manajer bernomor 5 (Rizky). Achmad tidak menghasilkan pasangan karena manajer_id-nya NULL — dan sekali lagi, NULL tidak pernah sama dengan NULL dalam pembandingan SQL. Pola “dua syarat di ON, satu menyamakan atribut dan satu membatasi arah pasangan” ini berlaku umum untuk pertanyaan berbasis perbandingan antarbaris, misalnya produk dengan kategori sama atau transaksi di hari yang sama.

Membersihkan Database Latihan #

sql
DROP DATABASE belajar_join;

Output yang Dihasilkan:

text
Query OK, 1 row affected (0.00 sec)

Referensi #

Self join tuntas dibedah: satu tabel tampil sebagai dua salinan ber-alias, ON memasangkan manajer_id dengan id di salinan satunya, LEFT JOIN menjemput pegawai puncak ber-manajer NULL, dan dua syarat di ON menghasilkan pasangan rekan satu atasan. Sampai di sini setiap contoh kita selalu memakai tepat dua tabel (atau satu tabel dua salinan). Laporan nyata sering merangkai lebih dari itu — nama pelanggan, pesanannya, dan isi tiap pesanan sekaligus. Chapter berikutnya membahas multiple joins: cara merantai tiga tabel dalam satu query.