Ketiga jenis join sebelumnya selalu menyertakan syarat pencocokan: ON p.id = pn.pelanggan_id. CROSS JOIN membuang syarat itu sama sekali — hasilnya adalah semua kombinasi baris dari kedua tabel, tanpa pencocokan apa pun. Kedengarannya aneh, tetapi kombinasi menyeluruh justru dibutuhkan pada kasus tertentu, misalnya menyusun matriks ukuran×warna untuk katalog produk. Pada chapter ini kita menghitung dan melihat sendiri hasil perkalian Cartesian product 5×4, membandingkan sintaks CROSS JOIN eksplisit dengan gaya koma yang lama, membuktikan bahaya diam-diam lupa ON, lalu menyaring hasil cross join dengan WHERE.

Menyiapkan Tabel Latihan #

Kita butuh dua perangkat data. Pertama, pasangan pelanggan dan pesanan yang sudah akrab — perannya di chapter ini adalah korban dari kecelakaan “lupa ON” yang akan kita demostrasikan. Kedua, dua tabel kecil untuk kasus nyata cross join: daftar ukuran dan warna kaos yang akan kita kombinasikan:

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

CREATE TABLE pelanggan (
  id INT AUTO_INCREMENT PRIMARY KEY,
  nama VARCHAR(50) NOT NULL,
  kota VARCHAR(30) NOT NULL
);

CREATE TABLE pesanan (
  id INT AUTO_INCREMENT PRIMARY KEY,
  pelanggan_id INT NOT NULL,
  tanggal DATE NOT NULL,
  total DECIMAL(12,2) NOT NULL,
  FOREIGN KEY (pelanggan_id) REFERENCES pelanggan(id)
);

INSERT INTO pelanggan (nama, kota) VALUES
  ('Budi Santoso', 'Jakarta'),
  ('Siti Rahayu',  'Bandung'),
  ('Andi Wijaya',  'Surabaya'),
  ('Rina Melati',  'Yogyakarta'),
  ('Dewi Lestari', 'Bandung'),
  ('Tono Hartono', 'Medan'),
  ('Sari Puspita', 'Jakarta');

INSERT INTO pesanan (pelanggan_id, tanggal, total) VALUES
  (1, '2026-01-05', 170000.00),
  (2, '2026-01-07',  50000.00),
  (1, '2026-01-10',  62000.00),
  (1, '2026-01-12',  15000.00),
  (3, '2026-01-15', 137000.00),
  (2, '2026-01-18',  85000.00),
  (4, '2026-01-08', 125000.00);

CREATE TABLE ukuran (
  urutan INT PRIMARY KEY,
  ukuran VARCHAR(8) NOT NULL
);

INSERT INTO ukuran (urutan, ukuran) VALUES
  (1, 'S'),
  (2, 'M'),
  (3, 'L'),
  (4, 'XL'),
  (5, 'XXL');

CREATE TABLE warna (
  id INT AUTO_INCREMENT PRIMARY KEY,
  warna VARCHAR(20) NOT NULL
);

INSERT INTO warna (warna) VALUES
  ('Hitam'),
  ('Putih'),
  ('Biru'),
  ('Merah');

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, 0 rows affected (0.00 sec)

Query OK, 7 rows affected (0.00 sec)
Records: 7  Duplicates: 0  Warnings: 0

Query OK, 7 rows affected (0.00 sec)
Records: 7  Duplicates: 0  Warnings: 0

Query OK, 0 rows affected (0.00 sec)

Query OK, 5 rows affected (0.00 sec)
Records: 5  Duplicates: 0  Warnings: 0

Query OK, 0 rows affected (0.00 sec)

Query OK, 4 rows affected (0.00 sec)
Records: 4  Duplicates: 0  Warnings: 0

Tabel ukuran sengaja memakai kolom urutan bernilai 1 sampai 5, karena urutan alfabetis S, M, L, XL, XXL tidak sesuai urutan logisnya — kolom ini akan jadi tulang punggung ORDER BY supaya 20 baris hasil tampil teratur dan deterministik. Tabel warna berisi empat baris, sehingga jumlah kombinasi yang diharapkan jelas: 5 × 4 = 20.

Perkalian Cartesian: 5 × 4 = 20 #

CROSS JOIN memasangkan setiap baris tabel pertama dengan setiap baris tabel kedua — hasilnya disebut Cartesian product, dan jumlah barisnya adalah perkalian jumlah baris kedua tabel:

sql
SELECT u.ukuran, w.warna
FROM ukuran u
CROSS JOIN warna w
ORDER BY u.urutan, w.warna;

Output yang Dihasilkan:

text
+--------+-------+
| ukuran | warna |
+--------+-------+
| S      | Biru  |
| S      | Hitam |
| S      | Merah |
| S      | Putih |
| M      | Biru  |
| M      | Hitam |
| M      | Merah |
| M      | Putih |
| L      | Biru  |
| L      | Hitam |
| L      | Merah |
| L      | Putih |
| XL     | Biru  |
| XL     | Hitam |
| XL     | Merah |
| XL     | Putih |
| XXL    | Biru  |
| XXL    | Hitam |
| XXL    | Merah |
| XXL    | Putih |
+--------+-------+
20 rows in set (0.00 sec)

Mari kita baca hasil 20 baris tersebut: untuk setiap ukuran muncul empat baris — satu untuk tiap warna — dan urutannya diatur ORDER BY u.urutan, w.warna sehingga ukuran S tampil lebih dulu dengan warna berurut Biru, Hitam, Merah, Putih, lalu disusul blok ukuran M, L, XL, hingga XXL. Mari kita bedah mekanisme pembentukannya:

  1. Klausa FROM ukuran u CROSS JOIN warna w: tidak ada klausa ON sama sekali — memang tidak diperbolehkan ada. MySQL mengambil baris pertama ukuran lalu menduplikasinya untuk setiap baris warna, lalu lanjut ke baris ukuran kedua, dan seterusnya.
  2. Kolom u.ukuran, w.warna: setiap baris hasil mengambil satu nilai dari kiri dan satu nilai dari kanan, membentuk pasangan unik seperti S/Biru dan XXL/Putih.
  3. ORDER BY u.urutan, w.warna: wajib untuk hasil multi-baris — tanpa ORDER BY, urutan baris tidak dijamin sama antar eksekusi.

Verifikasi jumlahnya dengan menghitung langsung:

sql
SELECT COUNT(*) AS jumlah_kombinasi
FROM ukuran u
CROSS JOIN warna w;

Output yang Dihasilkan:

text
+------------------+
| jumlah_kombinasi |
+------------------+
|               20 |
+------------------+
1 row in set (0.00 sec)

COUNT(*) mengembalikan 20 — tepat hasil 5 × 4. Rumus ini bersifat umum: tabel berisi a baris yang di-cross join dengan tabel berisi b baris menghasilkan a × b baris, dan angka itu membesar dengan sangat cepat — dua tabel masing-masing 1.000 baris langsung menghasilkan satu juta baris.

Kasus Nyata: Matriks Ukuran × Warna #

Cross join bukan sekadar kuriositas. Toko kaos kita menjual lima ukuran dalam empat warna: katalog lengkapnya perlu 20 varian, dan daftar 20 kombinasi di atas persis menjadi kerangka tabel produk_kaos — tinggal menambahkan kolom stok atau harga per varian. Pola yang sama dipakai untuk kombinasi hari×jam pada jadwal, kota×paket pada matriks harga pengiriman, atau pelanggan×bulan pada laporan perkembangan bulanan (kombinasi lalu di-LEFT JOIN dengan data aktual supaya bulan kosong tetap tampil). Ciri khas semua kasus itu: kita butuh semua kombinasi sebagai titik awal, bukan kombinasi yang sudah tersaring.

Hasil cross join juga bisa disaring setelah terbentuk. Misalnya gudang hanya menyimpan ukuran M dan L dalam warna gelap Hitam dan Biru — kombinasi yang relevan tinggal empat:

sql
SELECT u.ukuran, w.warna
FROM ukuran u
CROSS JOIN warna w
WHERE u.urutan IN (2, 3) AND w.warna IN ('Hitam', 'Biru')
ORDER BY u.urutan, w.warna;

Output yang Dihasilkan:

text
+--------+-------+
| ukuran | warna |
+--------+-------+
| M      | Biru  |
| M      | Hitam |
| L      | Biru  |
| L      | Hitam |
+--------+-------+
4 rows in set (0.00 sec)

WHERE bekerja seperti biasa: menilai setiap baris hasil 20 kombinasi satu per satu dan hanya meloloskan empat yang memenuhi kedua syarat. Menyaring setelah Cartesian product terbentuk terasa boros pada data besar; bila syararnya bisa dinyatakan sebagai pencocokan kunci, INNER JOIN dengan ON adalah pilihan yang lebih efisien.

Sintaks Lama: Koma di FROM #

Sebelum standar JOIN ... ON populer, Cartesian product ditulis dengan koma di klausa FROM:

sql
SELECT COUNT(*) AS jumlah_kombinasi_koma
FROM ukuran u, warna w;

Output yang Dihasilkan:

text
+-----------------------+
| jumlah_kombinasi_koma |
+-----------------------+
|                    20 |
+-----------------------+
1 row in set (0.00 sec)

Hasilnya tetap 20 — koma dan CROSS JOIN identik untuk kasus tanpa kondisi. Gaya koma juga bisa ditambah WHERE untuk meniru INNER JOIN:

sql
SELECT COUNT(*) AS pasangan_sah
FROM pelanggan p, pesanan pn
WHERE p.id = pn.pelanggan_id;

Output yang Dihasilkan:

text
+--------------+
| pasangan_sah |
+--------------+
|            7 |
+--------------+
1 row in set (0.00 sec)

Kondisi pencocokan di WHERE menghasilkan tujuh pasangan sah — sama dengan INNER JOIN ... ON kita di chapter kedua. Namun gaya koma punya kelemahan serius: logika penggabungan dan logika penyaringan tercampur di satu klausa WHERE, dan syntax-nya menolak klausa ON:

sql
-- @error
SELECT COUNT(*) FROM ukuran u, warna w ON u.urutan = w.id;

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'ON u.urutan = w.id' at line 1
Bye

Server menolak dengan ERROR 1064 karena klausa ON hanya boleh menyertai kata kunci JOIN. Kode baru sebaiknya memakai sintaks eksplisit: CROSS JOIN untuk Cartesian product dan INNER JOIN ... ON untuk pencocokan — niat query langsung terbaca dari teksnya.

Bahaya Diam-Diam: Lupa ON #

Inilah jebakan paling berharga dari chapter ini. Kita sedang menulis INNER JOIN, lupa menulis ON — apakah MySQL menolak query-nya? Mari kita uji pada pasangan pelanggan dan pesanan:

sql
SELECT COUNT(*) AS baris_tak_disengaja
FROM pelanggan p
INNER JOIN pesanan pn;

Output yang Dihasilkan:

text
+---------------------+
| baris_tak_disengaja |
+---------------------+
|                  49 |
+---------------------+
1 row in set (0.00 sec)

Query berjalan mulus dan mengembalikan 49 — yaitu 7 × 7, Cartesian product penuh. MySQL memperlakukan INNER JOIN tanpa ON sebagai CROSS JOIN, bukan sebagai error. Dampaknya pada query nyata sangat serius: laporan yang seharusnya berisi tujuh baris mendadak berisi 49 baris, SUM(total) membengkak tujuh kali lipat, dan server sibuk memproses jutaan baris sampah pada tabel besar. Kesalahan semacam ini jarang memicu galat — hasilnya salah secara diam-diam, itulah yang membuatnya berbahaya.

Membersihkan Database Latihan #

sql
DROP DATABASE belajar_join;

Output yang Dihasilkan:

text
Query OK, 4 rows affected (0.00 sec)

Referensi #

CROSS JOIN tuntas: perkalian a×b terbukti nyata (20 kombinasi dari 5×4), sintaks koma lama setara tapi kurang jelas, dan lupa ON terbukti menghasilkan Cartesian product tanpa error. Semua join sejauh ini memasangkan dua tabel yang berbeda; padahal ada satu pola penting yang justru memerlukan tabel yang sama muncul dua kali — mencari pasangan data di dalam satu tabel, seperti pegawai dan manajernya. Chapter berikutnya membahas self join.