Sometimes a data task boils down to generating every possible combination of two lists. A clothing store might need to pair every fabric type with every fit style so the design team can plan the full product lineup. That’s exactly where PySpark’s crossJoin comes in, and it’s simpler than it looks.
The Scenario
Consider a data analyst at ThreadCraft, a clothing e commerce company. The product team has finalized two things for the upcoming season:
- A list of fabrics they’ll be working with.
- A list of fit styles each garment can come in.
The goal? Generate every single fabric and fit combination so the design team can sketch mockups and the inventory team can start planning warehouse space.
It starts with two small PySpark DataFrames:
fabrics_df = spark.createDataFrame(
[("Linen",), ("Denim",), ("Cotton",), ("Corduroy",)],
["fabric"]
)
fits_df = spark.createDataFrame(
[("Slim",), ("Regular",), ("Relaxed",)],
["fit"]
)
fabrics_df looks like this:
| fabric |
|---|
| Linen |
| Denim |
| Cotton |
| Corduroy |
And fits_df looks like this:
| fit |
|---|
| Slim |
| Regular |
| Relaxed |
The question is: how can every fabric be paired with every fit without writing a loop, without hardcoding anything, and in a way that scales to thousands of rows?
Enter cross
A cross join (also called a Cartesian product) takes every row from the first table and matches it with every row from the second table. If table A has 4 rows and table B has 3 rows, the result has 4 × 3 = 12 rows, one for every possible pair.
The PySpark syntax is refreshingly minimal:
catalog_df = fabrics_df.crossJoin(fits_df)
catalog_df.show()
Output:
+--------+-------+
| fabric| fit|
+--------+-------+
| Linen| Slim|
| Linen|Regular|
| Linen|Relaxed|
| Denim| Slim|
| Denim|Regular|
| Denim|Relaxed|
| Cotton| Slim|
| Cotton|Regular|
| Cotton|Relaxed|
|Corduroy| Slim|
|Corduroy|Regular|
|Corduroy|Relaxed|
+--------+-------+
That’s it. One line of code, twelve product variations, zero loops.
What’s Actually Happening Under the Hood?
Think of it like a restaurant menu. If a restaurant offers 4 types of pasta and 3 types of sauce, the full menu of possible dishes is every pasta paired with every sauce. “Penne + Marinara” isn’t skipped just because “Penne + Alfredo” already exists. Every pairing gets listed.
crossJoin does the same thing. It doesn’t look for matching keys between the two tables (that’s what regular joins do). It simply says: give me every combination.
Making It More Useful
In practice, it often helps to enrich the result. For example, generating a product SKU for each combination:
from pyspark.sql.functions import concat, lit, upper, monotonically_increasing_id
catalog_df = (
fabrics_df
.crossJoin(fits_df)
.withColumn(
"sku",
concat(
upper(fabrics_df["fabric"]),
lit("_"),
upper(fits_df["fit"]),
lit("_"),
monotonically_increasing_id()
)
)
)
catalog_df.show(truncate=False)
+--------+-------+-------------------+
|fabric |fit |sku |
+--------+-------+-------------------+
|Linen |Slim |LINEN_SLIM_0 |
|Linen |Regular|LINEN_REGULAR_1 |
|Linen |Relaxed|LINEN_RELAXED_2 |
|Denim |Slim |DENIM_SLIM_3 |
|... |... |... |
+--------+-------+-------------------+
Now each combination has a unique identifier the warehouse team can actually use.
A Word of Caution: Size Matters
Cross joins multiply row counts. That’s fine when combining 4 fabrics with 3 fits, since the result is only 12 rows. But cross joining a table of 10,000 customers with a table of 5,000 products would suddenly produce 50 million rows. That can bring a cluster to its knees.
A good rule of thumb: if either DataFrame has more than a few thousand rows, it’s worth pausing to consider whether every combination is truly needed, or whether a filtered join would do the job.
When to Use crossJoin
Cross joins shine in a few specific situations:
- Catalog generation pairs dimensions like fabric × fit, color × size, or topping × crust to build out a full product matrix.
- Simulation and testing creates every combination of input parameters for a test harness or a hypothetical analysis.
- Date scaffolding crosses a list of stores with a list of dates to build a skeleton table that will later be filled with sales data.
- Pairwise comparison compares every item against every other item (think recommendation engines or distance calculations).
Wrapping Up
crossJoin is one of those PySpark methods that does exactly one thing and does it well. When every combination of two sets of data is needed, with no conditions, no key matching, just the full Cartesian product, it’s the right tool. Keeping an eye on row counts and using it intentionally will prevent performance headaches and eliminate the need for awkward nested loops or convoluted SQL.
For the ThreadCraft team, those 12 rows are now ready to hand off to the designers. Every fabric, every fit, every possibility accounted for in a single line of PySpark.


*The views expressed here are my own and do not represent those of my employer.*
Hello, I’m Bruno — a dual citizen of Brazil and Sweden. I bring a global perspective shaped by experiences in both South America and Europe, with a strong focus on collaboration and innovation across cultures. I am a Computer Scientist, PhD Candidate in Information and Communication Technologies, focusing on Data Science and Artificial Intelligence, and hold dual Master’s degrees in Data Science and Cybersecurity. With over fifteen years of international experience spanning Brazil, Hungary, and Sweden, I have collaborated with global organizations such as IBM, Playtech, and Oracle, as well as contributed remotely to projects across multiple regions. My professional interests include Databases, Cybersecurity, Cloud Computing, Data Science, Data Engineering, Big Data, Artificial Intelligence, Programming, and Software Engineering, all driven by a deep passion for transforming data into strategic business value.