CROSS JOIN in MySQL
A CROSS JOIN produces the cartesian product of two tables: every row from the left table
is paired with every row from the right table. If the left table has 5 rows and the right has 4,
the result has 20 rows. Unlike other JOINs, there is no matching condition — every combination
is returned by design.
CROSS JOIN Syntax
-- Explicit CROSS JOIN keyword SELECT * FROM table_a CROSS JOIN table_b; -- No ON or USING clause — that's intentional -- Every row in table_a × every row in table_b -- Simple example: 4 sizes × 3 colors = 12 combinations SELECT s.size, c.color FROM sizes AS s CROSS JOIN colors AS c;
Concrete Cartesian Product Example
CREATE TEMPORARY TABLE sizes (size VARCHAR(5));
CREATE TEMPORARY TABLE colors (color VARCHAR(10));
INSERT INTO sizes VALUES ('S'), ('M'), ('L'), ('XL');
INSERT INTO colors VALUES ('Red'), ('Blue'), ('Green');
-- Result: 4 × 3 = 12 rows
SELECT s.size, c.color
FROM sizes AS s
CROSS JOIN colors AS c
ORDER BY s.size, c.color;size | color |
|---|---|
L | Blue |
L | Green |
L | Red |
M | Blue |
M | Green |
M | Red |
S | Blue |
S | Green |
S | Red |
XL | Blue |
XL | Green |
XL | Red |
Generating a Number Sequence Table
A CROSS JOIN of digit tables produces a sequence of integers without needing a real sequence table. This is a foundational pattern for generating calendar tables, test data, and gap analysis:
-- Generate integers 0–999 by crossing three digit tables SELECT a.n + b.n * 10 + c.n * 100 AS num FROM (SELECT 0 AS n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) AS a CROSS JOIN (SELECT 0 AS n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) AS b CROSS JOIN (SELECT 0 AS n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) AS c ORDER BY num;
numbers table with values 0–99999: CREATE TABLE numbers AS SELECT .... A numbers table is reusable for calendar generation, gap detection, and test data — much faster than inline CROSS JOIN subqueries.Calendar Table Generation
A calendar table (one row per date) is extremely useful for date-range reporting. Generate all dates in a year by crossing digit tables to produce offsets, then add them to a start date:
-- Generate every date in 2024 (366 days for leap year)
SELECT DATE_ADD('2024-01-01', INTERVAL seq.n DAY) AS calendar_date,
DAYNAME(DATE_ADD('2024-01-01', INTERVAL seq.n DAY)) AS day_name,
MONTH(DATE_ADD('2024-01-01', INTERVAL seq.n DAY)) AS month_num
FROM (
SELECT a.n + b.n * 10 + c.n * 100 AS n
FROM
(SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) a
CROSS JOIN
(SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) b
CROSS JOIN
(SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3) c
) AS seq
WHERE seq.n < 366
ORDER BY calendar_date;
-- Save for repeated use
CREATE TABLE calendar_2024 AS
SELECT DATE_ADD('2024-01-01', INTERVAL n DAY) AS dt
FROM (
SELECT a.n + b.n * 10 + c.n * 100 AS n
FROM (SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) a
CROSS JOIN
(SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) b
CROSS JOIN
(SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3) c
) seq WHERE n < 366;Product Variant Generation
-- E-commerce: create all SKU combinations for a new product
-- Given sizes, colors, and materials — generate every variant row
CREATE TEMPORARY TABLE materials (material VARCHAR(20));
INSERT INTO materials VALUES ('Cotton'), ('Polyester'), ('Wool');
-- 4 sizes × 3 colors × 3 materials = 36 variants
INSERT INTO product_variants (product_id, size, color, material, sku)
SELECT
42 AS product_id,
s.size,
c.color,
m.material,
CONCAT('PROD-42-', s.size, '-',
UPPER(LEFT(c.color, 3)), '-',
UPPER(LEFT(m.material, 3))) AS sku
FROM sizes AS s
CROSS JOIN colors AS c
CROSS JOIN materials AS m
ORDER BY s.size, c.color, m.material;Gap Detection in Time Series
LEFT JOIN a calendar table against your data to reveal dates with no activity:
-- Find days in January 2024 with no orders
SELECT cal.dt AS missing_date
FROM (
SELECT DATE_ADD('2024-01-01', INTERVAL n DAY) AS dt
FROM (SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) a
CROSS JOIN
(SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3) b
WHERE a.n + b.n * 10 < 31
) AS cal
LEFT JOIN orders o ON DATE(o.created_at) = cal.dt
WHERE o.id IS NULL
ORDER BY cal.dt;Report Matrix — All Customers x All Months
Business reports often need to show every combination even when data is absent (e.g., all months for all customers, with zeros for months with no sales). CROSS JOIN all customers with all months, then LEFT JOIN the actual sales data:
-- Monthly sales report: show all 12 months for every customer -- Even if a customer had no orders that month (shows 0) SELECT c.customer_id, c.name, m.month_num, COALESCE(SUM(o.total), 0) AS monthly_revenue FROM customers AS c CROSS JOIN ( SELECT 1 AS month_num UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION SELECT 10 UNION SELECT 11 UNION SELECT 12 ) AS m LEFT JOIN orders o ON o.customer_id = c.customer_id AND MONTH(o.created_at) = m.month_num AND YEAR(o.created_at) = 2024 GROUP BY c.customer_id, c.name, m.month_num ORDER BY c.name, m.month_num;
Implicit CROSS JOIN (Comma Notation)
A comma between table names in FROM without a WHERE join condition is an implicit CROSS
JOIN. This is how accidental cartesian products are introduced in legacy code.
-- Implicit CROSS JOIN — intentional (same as CROSS JOIN keyword) SELECT s.size, c.color FROM sizes AS s, colors AS c; -- DANGEROUS: looks like an INNER JOIN but the WHERE join condition is missing SELECT c.first_name, o.order_id FROM customers AS c, orders AS o; -- Returns customers * orders rows — ACCIDENTAL cartesian product -- The correct version adds the join condition in WHERE SELECT c.first_name, o.order_id FROM customers AS c, orders AS o WHERE c.customer_id = o.customer_id; -- now an implicit INNER JOIN
JOIN ... ON) in new code. Comma-separated FROM lists are a common source of accidental cross joins that can return billions of rows and bring down production databases.CROSS JOIN with WHERE to Exclude Unwanted Combinations
Adding a WHERE clause to a CROSS JOIN narrows the result — useful for generating all combinations and then filtering out ones that already exist:
-- Find all employee × shift slot pairs NOT yet assigned this week
SELECT
e.employee_id,
e.full_name,
s.slot_id,
s.slot_start,
s.slot_end
FROM employees AS e
CROSS JOIN shift_slots AS s
WHERE s.shift_date = '2024-07-15'
AND NOT EXISTS (
SELECT 1
FROM shift_assignments sa
WHERE sa.employee_id = e.employee_id
AND sa.slot_id = s.slot_id
)
ORDER BY s.slot_start, e.full_name;Scheduling Matrix Example
-- Generate an open schedule grid for a work week SELECT d.day_name, t.slot_label, t.start_time, t.end_time FROM ( SELECT 'Monday' AS day_name, 1 AS day_num UNION ALL SELECT 'Tuesday', 2 UNION ALL SELECT 'Wednesday', 3 UNION ALL SELECT 'Thursday', 4 UNION ALL SELECT 'Friday', 5 ) AS d CROSS JOIN ( SELECT '9am-1pm' AS slot_label, '09:00' AS start_time, '13:00' AS end_time UNION ALL SELECT '1pm-5pm', '13:00', '17:00' UNION ALL SELECT '5pm-9pm', '17:00', '21:00' ) AS t ORDER BY d.day_num, t.start_time; -- Produces 15 rows: 5 days × 3 time slots
Numbers Table as a Permanent Utility
A permanent numbers table is more efficient than inline CROSS JOIN digit sequences. Create it once and use it for all date generation, gap detection, and test data:
-- Create a reusable numbers table (0 to 99,999)
CREATE TABLE numbers (n INT UNSIGNED PRIMARY KEY);
INSERT INTO numbers (n)
SELECT a.n + b.n * 10 + c.n * 100 + d.n * 1000 + e.n * 10000 AS n
FROM
(SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) a
CROSS JOIN
(SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) b
CROSS JOIN
(SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) c
CROSS JOIN
(SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) d
CROSS JOIN
(SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) e;
-- 100,000 rows inserted in < 1 second
-- Use the numbers table to generate any date range
SELECT DATE_ADD('2024-01-01', INTERVAL n DAY) AS dt
FROM numbers
WHERE n < 366 -- 2024 is a leap year
ORDER BY dt;Using CROSS JOIN for Pivoting Data
CROSS JOIN combined with conditional aggregation can pivot row data into columns without using CASE WHEN repetition:
-- Pivot quarterly sales by product using CROSS JOIN + conditional SUM SELECT p.product_name, SUM(CASE WHEN q.quarter = 1 THEN s.amount ELSE 0 END) AS q1, SUM(CASE WHEN q.quarter = 2 THEN s.amount ELSE 0 END) AS q2, SUM(CASE WHEN q.quarter = 3 THEN s.amount ELSE 0 END) AS q3, SUM(CASE WHEN q.quarter = 4 THEN s.amount ELSE 0 END) AS q4 FROM products AS p CROSS JOIN ( SELECT 1 AS quarter UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 ) AS q LEFT JOIN sales s ON s.product_id = p.product_id AND QUARTER(s.sale_date) = q.quarter AND YEAR(s.sale_date) = 2024 GROUP BY p.product_id, p.product_name ORDER BY p.product_name;
CROSS JOIN Performance Warning
Result size grows multiplicatively. Always estimate the output size before running a CROSS JOIN on real tables:
-- Estimate result size before running SELECT (SELECT COUNT(*) FROM table_a) * (SELECT COUNT(*) FROM table_b) AS estimated_rows; -- If dangerously large, add WHERE conditions or LIMIT for previewing SELECT * FROM table_a CROSS JOIN table_b WHERE table_a.type = 'active' -- filter before cross product explodes LIMIT 100;
Left rows | Right rows | Result rows |
|---|---|---|
10 | 10 | 100 |
100 | 50 | 5,000 |
1,000 | 1,000 | 1,000,000 |
10,000 | 10,000 | 100,000,000 — dangerous |
100,000 | 100,000 | 10,000,000,000 — will crash the server |
Generating Test Data with CROSS JOIN
-- Generate realistic test user data by crossing name parts
INSERT INTO test_users (username, email, created_at)
SELECT
CONCAT(f.first_name, '_', l.last_name) AS username,
LOWER(CONCAT(f.first_name, '.', l.last_name, '@', d.domain)) AS email,
DATE_ADD('2020-01-01', INTERVAL FLOOR(RAND() * 1460) DAY) AS created_at
FROM
(SELECT 'alice' AS first_name UNION SELECT 'bob' UNION SELECT 'carol'
UNION SELECT 'dave' UNION SELECT 'eve' UNION SELECT 'frank'
UNION SELECT 'grace' UNION SELECT 'henry' UNION SELECT 'iris'
UNION SELECT 'james') AS f
CROSS JOIN
(SELECT 'smith' AS last_name UNION SELECT 'jones' UNION SELECT 'patel'
UNION SELECT 'garcia' UNION SELECT 'nguyen') AS l
CROSS JOIN
(SELECT 'example.com' AS domain UNION SELECT 'test.org' UNION SELECT 'demo.net') AS d;
-- Generates 10 * 5 * 3 = 150 test user rowsCROSS JOIN vs INNER JOIN — The Key Distinction
Adding an ON clause to a CROSS JOIN transforms it into a semantically equivalent INNER JOIN. Knowing this helps you understand that CROSS JOIN is just INNER JOIN without a filter condition:
-- These two are logically identical: -- CROSS JOIN with WHERE filter SELECT c.name, p.name AS product FROM customers AS c CROSS JOIN products AS p WHERE p.category_id = c.preferred_category_id; -- INNER JOIN with ON clause (preferred style) SELECT c.name, p.name AS product FROM customers AS c INNER JOIN products AS p ON p.category_id = c.preferred_category_id; -- Use CROSS JOIN syntax only when you INTEND a cartesian product. -- Use JOIN ... ON when you intend a filtered join — it documents intent clearly.
Real-World CROSS JOIN Use Cases Summary
Use Case | Left table | Right table | Result |
|---|---|---|---|
Product variants | sizes (4 rows) | colors × materials | All size/color/material combinations |
Calendar table | digits 0-9 | digits 0-9 × (0-3) | 365/366 sequential dates |
Report matrix | All customers | All months (1-12) | 12 rows per customer, even with no data |
Gap detection | Generated date sequence | Actual data (LEFT JOIN) | Dates where no data exists |
Scheduling grid | Days of week | Time slots | Every day × every time slot combination |
Test data | Name prefixes | Surnames × domains | Large set of realistic test user combinations |
Use CROSS JOIN intentionally for combinations, calendar tables, number sequences, and test data generation
Always estimate result size: left_count * right_count = output rows
Use the explicit CROSS JOIN keyword to document intent — not comma notation in FROM
Add WHERE or LIMIT when previewing large cartesian products
Use LEFT JOIN against a generated date/number series to detect gaps in time series data
Store frequently used calendar or number tables as real tables for better performance
A CROSS JOIN with a WHERE or ON clause is equivalent to an INNER JOIN — use JOIN syntax for filtered joins