TL;DR
CROSS JOIN adalah perintah SQL yang memasangkan setiap baris dari tabel pertama dengan setiap baris dari tabel kedua, menghasilkan semua kombinasi yang mungkin. Jumlah barisnya adalah perkalian: tabel 30 baris di-CROSS JOIN dengan tabel 4 baris menghasilkan 120 baris. Beda dari JOIN lain, CROSS JOIN gak butuh klausa ON sama sekali. Kegunaan utamanya bikin kerangka lengkap tanggal kali cabang, supaya periode yang gak ada transaksinya tetap muncul di laporan sebagai nol, bukan hilang.
CROSS JOIN masangin setiap baris tabel A sama setiap baris tabel B. Semua kombinasi, tanpa syarat apa pun.
Tabel 3 baris ketemu tabel 4 baris? Hasilnya 12 baris. Gak ada klausa ON, gak ada yang dicocokin.
Kedengeran gak ada gunanya. Tapi ini satu-satunya cara bikin laporan harian yang tanggal kosongnya tetap muncul sebagai nol, bukan hilang begitu aja.
CROSS JOIN adalah jenis JOIN yang menghasilkan semua kombinasi pasangan antara baris tabel kiri dan baris tabel kanan. Jumlah baris hasilnya sama dengan jumlah baris tabel kiri dikali jumlah baris tabel kanan.
Sintaksnya paling pendek di antara semua JOIN:
SELECT a.kolom, b.kolom
FROM tabel_a a
CROSS JOIN tabel_b b;
Gak ada ON. Emang gak butuh.
Istilah matematikanya cartesian product. Kalau kamu ketemu warning database yang nyebut istilah itu, artinya query kamu lagi ngasilin perkalian baris.
Bikin dua tabel dulu:
CREATE TABLE cabang (
cabang_id INT PRIMARY KEY,
nama_cabang VARCHAR(50) NOT NULL
);
INSERT INTO cabang (cabang_id, nama_cabang) VALUES
(1, 'Bandung'),
(2, 'Cimahi'),
(3, 'Soreang');
Sekarang CROSS JOIN sama 3 tanggal:
SELECT c.nama_cabang, k.tanggal
FROM cabang c
CROSS JOIN (
SELECT DATE '2026-01-01' AS tanggal
UNION ALL SELECT DATE '2026-01-02'
UNION ALL SELECT DATE '2026-01-03'
) k
ORDER BY k.tanggal, c.nama_cabang;
Hasilnya 9 baris (3 cabang dikali 3 tanggal):
| nama_cabang | tanggal |
|---|---|
| Bandung | 2026-01-01 |
| Cimahi | 2026-01-01 |
| Soreang | 2026-01-01 |
| Bandung | 2026-01-02 |
| Cimahi | 2026-01-02 |
| Soreang | 2026-01-02 |
| Bandung | 2026-01-03 |
| Cimahi | 2026-01-03 |
| Soreang | 2026-01-03 |
Kerangka kosong yang lengkap. Setiap cabang punya baris di setiap tanggal, terlepas dari ada transaksinya atau gak.
Ini masalah nyata yang aku temuin waktu ngerjain dashboard toko_berkah.
Datanya:
INSERT INTO penjualan (penjualan_id, cabang_id, tanggal, total) VALUES
(1, 1, '2026-01-01', 1250000),
(2, 2, '2026-01-01', 890000),
(3, 3, '2026-01-01', 640000),
(4, 1, '2026-01-02', 1420000),
(5, 2, '2026-01-02', 760000),
(6, 1, '2026-01-03', 980000),
(7, 2, '2026-01-03', 1100000);
Liat baik-baik. Soreang cuma jualan tanggal 1. Tanggal 2 dan 3 kosong, mungkin tutup, mungkin gak ada yang beli.
Query biasa:
SELECT c.nama_cabang, p.tanggal, SUM(p.total) AS omzet
FROM penjualan p
JOIN cabang c ON c.cabang_id = p.cabang_id
GROUP BY c.nama_cabang, p.tanggal;
Hasilnya cuma 7 baris. Soreang tanggal 2 dan 3 gak muncul sama sekali.
Masukin ini ke grafik tren, dan garis Soreang bakal langsung lompat dari tanggal 1 ke tanggal 4. Keliatan kayak jualannya lancar terus, padahal dua hari kosong melompong.
SELECT
kerangka.tanggal,
kerangka.nama_cabang,
COALESCE(SUM(p.total), 0) AS omzet
FROM (
SELECT c.cabang_id, c.nama_cabang, d.tanggal
FROM cabang c
CROSS JOIN (
SELECT DATE '2026-01-01' AS tanggal
UNION ALL SELECT DATE '2026-01-02'
UNION ALL SELECT DATE '2026-01-03'
) d
) kerangka
LEFT JOIN penjualan p
ON p.cabang_id = kerangka.cabang_id
AND p.tanggal = kerangka.tanggal
GROUP BY kerangka.tanggal, kerangka.nama_cabang
ORDER BY kerangka.tanggal, kerangka.nama_cabang;
Sekarang keluar 9 baris. Soreang tanggal 2 dan 3 muncul dengan omzet 0.
| tanggal | nama_cabang | omzet |
|---|---|---|
| 2026-01-02 | Bandung | 1.420.000 |
| 2026-01-02 | Cimahi | 760.000 |
| 2026-01-02 | Soreang | 0 |
Angka nol itu informasi. Baris yang hilang bukan.
Urutan kerjanya: CROSS JOIN buat bikin rangkanya, LEFT JOIN buat nempelin data aslinya, COALESCE buat ngubah NULL jadi nol.
Ngetik UNION ALL 31 kali buat sebulan itu bikin capek. PostgreSQL punya jalan pintas:
SELECT c.nama_cabang, d.tanggal::DATE
FROM cabang c
CROSS JOIN generate_series(
DATE '2026-01-01',
DATE '2026-01-31',
INTERVAL '1 day'
) AS d(tanggal)
ORDER BY d.tanggal, c.nama_cabang;
3 cabang dikali 31 hari = 93 baris. Satu query, gak ada tabel bantu.
Detail generate_series ada di dokumentasi set returning functions PostgreSQL.
SQL Server dan MySQL 8 gak punya fungsi ini, tapi bisa pakai recursive CTE. Kalau males, bikin aja tabel kalender permanen sekali. Isi tanggal 10 tahun ke depan, terus dipakai selamanya.
Ini penyebab query lambat nomor satu yang paling sering aku liat di kerjaan orang.
-- BAHAYA: gak ada kondisi penghubung
SELECT *
FROM penjualan p, cabang c;
Sintaks koma di FROM tanpa WHERE = CROSS JOIN. Database gak protes. Dia cuma nurut.
Kalau penjualan punya 50.000 baris dan cabang punya 3 baris, hasilnya 150.000 baris. Masih ketolong.
Tapi kalau dua tabelnya sama-sama 50.000 baris? 2,5 miliar baris. Query kamu bakal gantung, dan tim data bakal nanya siapa yang bikin server panas.
JOIN ... ON yang eksplisit, jangan koma di FROMCROSS JOIN secara eksplisit kalau emang itu yang kamu mau, biar orang lain tau itu disengajaSELECT COUNT(*) sebelum ngejalanin query beratLIMIT 100 waktu lagi eksperimenKombinasinya bengkak eksponensial. Selalu CROSS JOIN tabel kecil (daftar cabang, daftar kategori, daftar tanggal), bukan tabel transaksi.
-- Ini technically cross join + filter
FROM penjualan p, cabang c
WHERE p.cabang_id = c.cabang_id;
Hasilnya bener dan optimizer biasanya cukup pintar buat nanganin ini. Tapi bacanya susah, dan sekali kamu lupa nulis WHERE-nya, boom.
Bikin kerangka 5 tahun padahal cuma butuh 30 hari. Barisnya jadi 60x lebih banyak dari yang perlu, dan query-nya melambat tanpa alasan.
INNER JOIN butuh klausa ON dan cuma nampilin baris yang cocok berdasarkan kondisi itu. CROSS JOIN gak punya ON sama sekali dan masangin semua baris tabel kiri dengan semua baris tabel kanan tanpa syarat. Jumlah barisnya jadi hasil perkalian. Kalau kamu nulis JOIN tapi lupa ON-nya, sebagian database diam-diam ngejalanin itu sebagai CROSS JOIN.
Kemungkinan besar kamu kena cross join gak sengaja. Ini kejadian kalau kamu nulis dua tabel di klausa FROM dipisah koma tanpa kondisi di WHERE, atau kalau kondisi JOIN-nya salah kolom. Tabel 500 baris ketemu tabel 300 baris bakal ngasilin 150 ribu baris.
Yang paling sering: bikin kerangka laporan yang lengkap. Kamu CROSS JOIN daftar tanggal dengan daftar cabang atau produk, terus LEFT JOIN ke data transaksi. Hasilnya, tanggal yang gak ada penjualannya tetap muncul dengan nilai nol, bukan hilang dari laporan. Ini penting buat grafik tren biar garisnya gak lompat-lompat.
Iya, dua istilah ini merujuk ke hal yang sama. Cartesian product itu istilah matematikanya, CROSS JOIN itu sintaks SQL-nya. Keduanya berarti semua kombinasi yang mungkin dari dua himpunan. Kalau kamu baca error atau warning yang nyebut cartesian product, artinya query kamu lagi ngasilin perkalian baris.
Di PostgreSQL pakai generate_series. Di SQL Server pakai recursive CTE. Di MySQL 8 juga bisa pakai recursive CTE. Kalau database kamu gak punya semuanya, bikin tabel kalender permanen sekali aja dan isi tanggalnya buat 10 tahun ke depan.
Ringkasnya:
Query di atas siap copy-paste. Coba jalanin, terus bandingin jumlah barisnya sebelum dan sesudah kerangka dipasang.
Latihan interaktifnya ada di halaman fungsi CROSS JOIN. Istilah cartesian product aku jelasin lengkap di glosarium cartesian product.
Lanjut baca: Cara JOIN Lebih dari Dua Tabel di SQL (biar barisnya gak kegandain waktu tabelnya nambah).
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Analisa per kuartal, hari kerja, atau musim jadi ribet kalau tiap query ngitung ulang atribut tanggal. Tabel kalender nyimpen semua atribut itu sekali, biar tinggal di-JOIN.
Laporan penjualan harian sering bolong di hari tanpa transaksi. Ini cara bikin deret tanggal lengkap di SQL biar tiap hari muncul, walau nilainya nol.
Struktur organisasi tersimpan sebagai kolom id_atasan yang saling nunjuk. Ini cara narik rantai jabatan, hitung total bawahan, dan span of control cuma pakai SQL.