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:
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:
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: 0Tabel 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:
SELECT u.ukuran, w.warna
FROM ukuran u
CROSS JOIN warna w
ORDER BY u.urutan, w.warna;Output yang Dihasilkan:
+--------+-------+
| 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:
- Klausa
FROM ukuran u CROSS JOIN warna w: tidak ada klausaONsama sekali — memang tidak diperbolehkan ada. MySQL mengambil baris pertamaukuranlalu menduplikasinya untuk setiap bariswarna, lalu lanjut ke barisukurankedua, dan seterusnya. - Kolom
u.ukuran, w.warna: setiap baris hasil mengambil satu nilai dari kiri dan satu nilai dari kanan, membentuk pasangan unik sepertiS/BirudanXXL/Putih. ORDER BY u.urutan, w.warna: wajib untuk hasil multi-baris — tanpaORDER BY, urutan baris tidak dijamin sama antar eksekusi.
Verifikasi jumlahnya dengan menghitung langsung:
SELECT COUNT(*) AS jumlah_kombinasi
FROM ukuran u
CROSS JOIN warna w;Output yang Dihasilkan:
+------------------+
| 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:
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:
+--------+-------+
| 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:
SELECT COUNT(*) AS jumlah_kombinasi_koma
FROM ukuran u, warna w;Output yang Dihasilkan:
+-----------------------+
| 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:
SELECT COUNT(*) AS pasangan_sah
FROM pelanggan p, pesanan pn
WHERE p.id = pn.pelanggan_id;Output yang Dihasilkan:
+--------------+
| 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:
-- @error
SELECT COUNT(*) FROM ukuran u, warna w ON u.urutan = w.id;Output yang Dihasilkan:
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
ByeServer 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:
SELECT COUNT(*) AS baris_tak_disengaja
FROM pelanggan p
INNER JOIN pesanan pn;Output yang Dihasilkan:
+---------------------+
| 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 #
DROP DATABASE belajar_join;Output yang Dihasilkan:
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.