CROSS JOIN
Use this lesson to understand CROSS JOIN with practical syntax and examples.
Concept Overview
CROSS JOIN returns the Cartesian product of two inputs: every row from the left side is paired with every row from the right side.
If table A has m rows and table B has n rows, the result has m * n rows.
Why is it important?
- Generating combinations: sizes x colors, users x feature flags, days x products
- Building grids: create a complete matrix of dimensions for reporting
- Understanding join mechanics: accidental Cartesian products are a common bug when you forget a join condition
Where does it fit?
CROSS JOIN is a join type alongside INNER/LEFT/RIGHT. Unlike other joins, it does not use an ON condition. In PostgreSQL, you also often see cross joins with VALUES or generate_series() for controlled dimension sets.
Syntax & Rules
Core Syntax
SELECT select_list
FROM a
CROSS JOIN b;
Equivalent syntax (comma join):
SELECT select_list
FROM a, b;
Available Options / Parameters
| Part | What it does | Notes |
|---|---|---|
CROSS JOIN | produces all combinations | no ON clause |
WHERE | filters after combinations are created | useful, but can still be expensive |
VALUES | inline small tables | great for controlled sets |
generate_series() | generate sequences (dates/numbers) | common for calendars/time grids |
Key Rules and Considerations
- Cross joins can explode in size quickly; always estimate row counts.
- If you see unexpectedly huge results, check for a missing join condition in a regular join.
- Prefer building combinations from small dimension sets, then join to facts.
- Use
LIMITwhile experimenting.
Step-by-Step Examples
Example 1: Controlled Combinations with VALUES (Beginner)
SELECT s.size, c.color
FROM (VALUES ('S'), ('M'), ('L')) AS s(size)
CROSS JOIN (VALUES ('red'), ('blue')) AS c(color)
ORDER BY s.size, c.color;
Expected output:
size | color
------+------
L | blue
L | red
M | blue
M | red
S | blue
S | red
(6 rows)
Explanation:
- 3 sizes x 2 colors = 6 combinations.
Example 2: Product Variants (Intermediate)
CREATE TABLE products (
product_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL
);
INSERT INTO products (name) VALUES
('Laptop'),
('Monitor');
CREATE TABLE colors (
color text PRIMARY KEY
);
INSERT INTO colors (color) VALUES
('Black'),
('Silver');
SELECT p.name AS product, c.color
FROM products p
CROSS JOIN colors c
ORDER BY p.product_id, c.color;
Expected output:
product | color
---------+--------
Laptop | Black
Laptop | Silver
Monitor | Black
Monitor | Silver
(4 rows)
Explanation:
- This is useful for generating variants before inserting into a
product_variantstable.
Example 3: Build a Calendar Grid with generate_series() (Advanced)
-- Create a 3-day calendar and cross it with products
SELECT d.day::date, p.name
FROM generate_series('2026-03-01'::date, '2026-03-03'::date, interval '1 day') AS d(day)
CROSS JOIN products p
ORDER BY d.day, p.product_id;
Expected output:
day | name
------------+---------
2026-03-01 | Laptop
2026-03-01 | Monitor
2026-03-02 | Laptop
2026-03-02 | Monitor
2026-03-03 | Laptop
2026-03-03 | Monitor
(6 rows)
Explanation:
- This pattern is common in reporting (show all days even when sales are 0).
Example 4: Accidental Cartesian Product (Missing Join Condition) (Advanced)
In PostgreSQL, writing JOIN without an ON/USING clause is a syntax error, but you can still accidentally create a Cartesian product by using a comma join (which behaves like a cross join).
CREATE TABLE suppliers (
supplier_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
supplier_name text NOT NULL
);
CREATE TABLE products_with_supplier (
product_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL,
supplier_id bigint NOT NULL REFERENCES suppliers(supplier_id)
);
INSERT INTO suppliers (supplier_name) VALUES
('Acme'),
('Globex');
INSERT INTO products_with_supplier (name, supplier_id) VALUES
('Laptop', 1),
('Monitor', 2);
-- Wrong: comma join creates a Cartesian product
SELECT p.name, s.supplier_name
FROM products_with_supplier p, suppliers s
ORDER BY p.product_id, s.supplier_id;
Expected output (wrong):
name | supplier_name
---------+--------------
Laptop | Acme
Laptop | Globex
Monitor | Acme
Monitor | Globex
(4 rows)
Fix (use a real join condition):
SELECT p.name, s.supplier_name
FROM products_with_supplier p
JOIN suppliers s ON s.supplier_id = p.supplier_id
ORDER BY p.product_id;
Expected output (fixed):
name | supplier_name
---------+--------------
Laptop | Acme
Monitor | Globex
(2 rows)
Explanation:
- A Cartesian product is correct only when you truly want all combinations.
- When you intended to match relationships, always use explicit
JOIN ... ON ....
Practical Use Cases
1) Generate all possible variants
SELECT p.product_id, s.size, c.color
FROM products p
CROSS JOIN sizes s
CROSS JOIN colors c;
2) Scheduling grids (employees x shifts)
SELECT e.employee_id, sh.shift_id
FROM employees e
CROSS JOIN shifts sh;
3) Complete time series reporting (days x store)
SELECT day, store_id
FROM generate_series(current_date - 6, current_date, interval '1 day') AS g(day)
CROSS JOIN stores;
4) Test-case generation
SELECT os.name AS os, br.name AS browser
FROM operating_systems os
CROSS JOIN browsers br;
5) Feature flag exposure matrix
SELECT u.user_id, f.flag_key
FROM users u
CROSS JOIN feature_flags f;
Common Mistakes & Troubleshooting
1) Using CROSS JOIN on large tables
Wrong approach:
SELECT *
FROM big_fact_table
CROSS JOIN another_big_table;
Bad outcome:
- Query can run for a long time or exhaust resources.
Fix:
- Cross join only small dimension sets, or add constraints before cross joining.
2) Expecting an ON condition
Wrong SQL:
FROM a CROSS JOIN b ON a.id = b.id
Typical error:
ERROR: syntax error at or near "ON"
Fix:
FROM a JOIN b ON a.id = b.id
3) Confusing CROSS JOIN with INNER JOIN
Wrong mental model:
- expecting CROSS JOIN to match by keys.
Fix:
- CROSS JOIN always returns all combinations; use an inner join for matched relationships.
4) Filtering after a cross join (too late)
Wrong approach:
SELECT *
FROM a
CROSS JOIN b
WHERE b.type = 'small';
Bad outcome:
- Still creates all combinations before filtering.
Fix:
SELECT *
FROM a
CROSS JOIN (SELECT * FROM b WHERE type = 'small') b;
Debugging checklist:
- Estimate row counts on both sides before running the query.
- Verify you actually need all combinations.
- If results are unexpectedly huge, look for a missing join condition.
- Use a small dimension set (
VALUES) to validate logic. - Use
LIMITandEXPLAINwhen exploring.
Best Practices
- ✅ Use
VALUESor small lookup tables as cross-join inputs. ❌ Avoid cross joining two large fact tables. - ✅ Filter dimension inputs before the cross join.
❌ Avoid relying on
WHEREto “save you” after a huge Cartesian product. - ✅ Use cross joins to build complete grids for reporting. ❌ Avoid cross joins for matched relationships (use inner/outer joins instead).
- ✅ Add
LIMITwhile developing queries. ❌ Avoid running a new cross join query in production without checking size. - ✅ Treat accidental Cartesian products as a bug.
❌ Avoid unqualified joins (or joins without
ON) unless you truly want CROSS JOIN behavior.
Hands-On Practice
Use this setup for the exercises:
CREATE TABLE practice_fruits (name text PRIMARY KEY);
CREATE TABLE practice_sizes (size_label text PRIMARY KEY);
INSERT INTO practice_fruits (name) VALUES ('Apple'), ('Banana');
INSERT INTO practice_sizes (size_label) VALUES ('Small'), ('Large');
Exercise 1 (Easy): All combinations
Task: Return all fruit/size combinations.
-- Your SQL here
Solution:
SELECT f.name, s.size_label
FROM practice_fruits f
CROSS JOIN practice_sizes s
ORDER BY f.name, s.size_label;
Exercise 2 (Medium): Filtered combinations
Task: Return only combinations where size is Large.
-- Your SQL here
Solution:
SELECT f.name, s.size_label
FROM practice_fruits f
CROSS JOIN practice_sizes s
WHERE s.size_label = 'Large'
ORDER BY f.name;
Exercise 3 (Advanced): Count combinations
Task: Count how many rows the cross join produces.
-- Your SQL here
Solution:
SELECT COUNT(*) AS total_combinations
FROM practice_fruits
CROSS JOIN practice_sizes;
Connection to Other Concepts
| Concept | Why it matters |
|---|---|
INNER JOIN | inner joins need ON conditions; cross joins do not |
WHERE | filters after the cross join result set exists |
GROUP BY | often used to aggregate grids created by cross joins |
generate_series() | common PostgreSQL tool paired with cross joins for calendars |
LEFT JOIN | used after cross joins to attach optional fact data |
Visual Learning Diagram
flowchart TD
A[Input A] --> B[CROSS JOIN]
C[Input B] --> B
B --> D[Cartesian Product]
D --> E[Grid / Matrix Output]
D --> F[Row Explosion Risk]
E --> G[Reporting: Days x Product]
E --> H[Variants: Size x Color]
classDef allNodes fill:#3e3e3e,stroke:#ffffff,stroke-width:2px,color:#f5f5f5
classDef highlight fill:#3e3e3e,stroke:#ffffff,stroke-width:4px,color:#f5f5f5
class A,B,C,D,E,F,G,H allNodes
class B highlight
Common Pitfalls
| Pitfall | Consequence | Prevention |
|---|---|---|
| Cross joining large tables | massive result sets | cross join only small inputs; filter early |
Forgetting an ON clause in a normal join | accidental Cartesian product | always write explicit JOIN ... ON ... |
| Using CROSS JOIN for matched relationships | incorrect results | use INNER JOIN/LEFT JOIN with keys |
| Filtering too late | wasted work | reduce inputs before cross joining |
| No LIMIT during exploration | runaway queries | start with LIMIT and inspect |
Quick Reference
SELECT * FROM a CROSS JOIN b;
SELECT s.size, c.color FROM (VALUES ('S'),('M')) s(size) CROSS JOIN (VALUES ('red'),('blue')) c(color);
SELECT d::date FROM generate_series('2026-03-01'::date,'2026-03-03'::date,'1 day') AS g(d);
SELECT day, store_id FROM generate_series(current_date-6,current_date,'1 day') g(day) CROSS JOIN stores;
SELECT COUNT(*) FROM a CROSS JOIN b;
What's Next
- Previous: RIGHT JOIN - Review the previous lesson to reinforce context.
- Next: SELF JOIN - Continue to the next concept with incremental complexity.
- Module Overview - Return to this module index and choose another related lesson.