What you will be able to do
- Write an inner or left join with the PySpark join() method and with SQL JOIN syntax, and predict which rows each one returns
- Join on more than one key with a list of column names or USING (a, b), and know which columns the result keeps
- Recognise when a join becomes a cross join (a Cartesian product) and why that costs a lot
- Mark a small DataFrame for a broadcast join with broadcast() or a BROADCAST SQL hint
Key concept
Join type (the how argument) — Every join pairs rows from a left and a right input using a join condition. The join type decides what happens to rows that have no match: drop them, keep them with NULLs, or skip the condition and pair every row with every other row.
1.The join() call: other, on and how
In PySpark you combine two DataFrames side by side with df.join(other, on, how). other is the right side of the join. on says how to match rows. It can be a single column name, a list of column names, a Column expression such as df.name == df2.name, or a list of Columns. how names the join type. When on is a name or a list of names, each of those columns must exist in both DataFrames, and Spark runs an equi-join, meaning rows match when the values are equal.
df = spark.createDataFrame([Row(name="Alice", age=2), Row(name="Bob", age=5)])
df2 = spark.createDataFrame([Row(name="Tom", height=80), Row(name="Bob", height=85)])
df.join(df2, "name").show()
# +----+---+------+
# |name|age|height|
# +----+---+------+
# | Bob| 5| 85|
# +----+---+------+How you write on changes the shape of the result. The same reference page joins these DataFrames with the expression df.name == df2.name and an outer join, and that output has two name columns, one from each side. If you pass the string "name", the result has a single name column, as shown above. Alice and Tom are missing from the inner result because neither has a partner on the other side.
Checkpoint 1 of 8· Check yourself
Which value is NOT a valid way to pass the on argument of DataFrame.join()?
The on parameter accepts a string, a list of strings, a Column expression or a list of Columns. A dictionary is not one of the accepted forms.
“a string for the join column name, a list of column names, a join expression (Column), or a list of Columns.”Source: docs.databricks.com
Sources1
2.Inner join vs left join: what happens to rows without a match
Inner and left joins handle matched rows the same way. They differ on the left rows that have no partner. An inner join returns only rows whose key values match in both tables, and it is the default join type in SQL as well as in PySpark. A left join (also called a left outer join) returns every row from the left table. Where the right table has no match, its columns are filled with NULL.
> SELECT id, name, employee.deptno, deptname
FROM employee
LEFT JOIN department ON employee.deptno = department.deptno;The LEFT JOIN returns all six employees. Chloe, Evan and Amy get NULL for deptname because departments 5, 4 and 6 do not exist. The INNER JOIN returns only Paul, John and Lisa, the three employees whose deptno is in the department view.
| SQL join type | PySpark how value | Rows returned |
|---|---|---|
| [ INNER ] (the default) | inner | Only rows with matching values in both tables |
| LEFT [ OUTER ] | left / leftouter / left_outer | Every left row, with NULL on the right side when there is no match |
| CROSS JOIN | cross | The Cartesian product of the two tables |
| [ LEFT ] SEMI | semi / leftsemi / left_semi | Left rows that have a match on the right |
| [ LEFT ] ANTI | anti / leftanti / left_anti | Left rows that have no match on the right |
Checkpoint 2 of 8· Match them up
Match each join type to the rows it returns
Tap a term, then the definition that fits it.
Each join type is defined by what it does with matched and unmatched rows. LEFT OUTER keeps every left row and fills the right side with NULL where there is no match. LEFT ANTI also looks at unmatched left rows, but it returns only those rows and nothing from the right side.
“Returns all values from the left table reference and the matched values from the right table reference”Source: docs.databricks.com
Checkpoint 3 of 8· Exam question
A data engineer has a `orders` DataFrame and a `customers` DataFrame and wants to keep only the rows where the `customer_id` value exists in both DataFrames. Which join type produces this result?
Correct answer: A — Perform an inner join on `customer_id`, which returns only rows where the key matches in both DataFrames
- A. An inner join returns only the rows whose join key is present in both DataFrames, discarding unmatched rows on either side, which is exactly the requirement here.
- B. A left join keeps every row from `orders` regardless of whether a match exists in `customers`, filling unmatched columns with null instead of discarding rows without a match.
- C. A cross join produces the cartesian product of both DataFrames and ignores the join key entirely, so it does not filter to matching `customer_id` values.
- D. A right join keeps every row from `customers` regardless of whether it matches `orders`, which includes unmatched customer rows the requirement wants excluded.
Sources2
3.Joining on multiple keys
Sometimes one column cannot identify a match on its own. An order line may only be unique per (order_id, line_no), for example. In PySpark you pass a list of column names to on, and each name must exist in both DataFrames. In SQL you write USING (a, b), which matches rows by checking equality on every listed column. If a column is missing from one of the tables, Databricks raises UNRESOLVED_USING_COLUMN_FOR_JOIN. The SQL reference spells out what a multi-column USING join means.
PySpark has a second route to the same result. Instead of names, on can be a join expression (a Column) or a list of Columns, so you can write one equality per key yourself. That is the PySpark counterpart of the SQL ON first.a = second.a AND first.b = second.b form shown below. Like a single-key expression join, it keeps the key columns from both sides rather than merging them, while a list of names merges them.
SELECT * FROM first JOIN second USING (a, b)
is equivalent to
SELECT first.a, first.b,
first.* EXCEPT(a, b),
second.* EXCEPT(a, b)
FROM first JOIN second ON first.a = second.a AND first.b = second.bThe equivalent query also shows how the columns are laid out. With USING (or NATURAL), SELECT * returns each matching column once, at the front. Next come the other columns from the left table, then the other columns from the right table. An ON condition does not merge the key columns this way. That is why the PySpark expression join above returned two name columns.
Checkpoint 4 of 8· Check yourself
You run SELECT * FROM first JOIN second USING (a, b). Which statement about the result is correct?
USING with several columns requires every column to be equal (AND), and SELECT * shows each matching column only once, before the other columns.
“SELECT * will only show one occurrence for each of the columns used to match first”Source: docs.databricks.com
4.Cross joins: every row against every row
A cross join has no match condition. It returns the Cartesian product, pairing every row on the left with every row on the right. In PySpark you call crossJoin(other) or pass how="cross". In SQL you write CROSS JOIN. The output row count is the left row count multiplied by the right row count.
df = spark.createDataFrame(
[(14, "Tom"), (23, "Alice"), (16, "Bob")], ["age", "name"])
df2 = spark.createDataFrame(
[Row(height=80, name="Tom"), Row(height=85, name="Bob")])
df.crossJoin(df2.select("height")).select("age", "name", "height"
).orderBy("age", "name", "height").show()
# +---+-----+------+
# |age| name|height|
# +---+-----+------+
# | 14| Tom| 80|
# | 14| Tom| 85|
# | 16| Bob| 80|
# | 16| Bob| 85|
# | 23|Alice| 80|
# | 23|Alice| 85|
# +---+-----+------+Checkpoint 5 of 8· Check yourself
df has 3 rows and df2 has 2 rows. How many rows does df.crossJoin(df2) return?
crossJoin returns the cartesian product, so the row count is 3 x 2 = 6. No match condition is applied, and every pairing is kept.
“Returns the cartesian product with another DataFrame.”Source: docs.databricks.com
This is why a Cartesian product can show up without anyone asking for one. The size matters too. In the SQL reference example, 6 employees cross-joined with 3 departments return 18 rows. Databricks' join optimization guidance is blunt about cross joins: take them out of any workload that needs low latency or is recomputed often. Keep them for cases where you really want every combination.
Checkpoint 6 of 8· Exam question
A pipeline joins a `transactions` DataFrame to a `stores` DataFrame so that every transaction is retained in the output even when the corresponding `store_id` is missing from `stores`. Which join accomplishes this while preserving all transaction rows?
Correct answer: A — A left join with `transactions` as the left side, which fills `stores` columns with null for unmatched `store_id` values
- A. A left join keeps every row from the left DataFrame and populates columns from the right DataFrame with null when no match is found, satisfying the requirement to retain all transactions.
- B. An inner join removes rows whose key has no counterpart on the other side, so transactions with a missing store would be dropped rather than preserved.
- C. A cross join ignores the join key and multiplies rows by pairing every transaction with every store, which does not preserve one row per original transaction.
- D. A left semi join keeps only left-side columns for rows that have a match and drops rows without one, so it does not retain transactions that lack a matching store.
5.Broadcast joins: marking the small side
Many joins pair a large fact table with a small lookup table. PySpark gives you broadcast(df) in pyspark.sql.functions for this. It marks a DataFrame as small enough to use in a broadcast join. You wrap the small side in broadcast() and join as usual. The join condition and join type do not change.
Why bother? The broadcast hint tells Spark to send the smaller table to all executors instead of performing a shuffle join, so the large side does not have to be shuffled. Spark can also choose a broadcast on its own: spark.sql.autoBroadcastJoinThreshold sets the maximum size of a table that will be broadcast (10 MB by default, and -1 turns it off). An explicit hint overrides that limit. The hinted side is broadcast regardless of the threshold, so you can broadcast a table that Spark's size estimate would otherwise leave out.
Checkpoint 7 of 8· Fill the gap
Which function marks df_small as ready for a broadcast join?
from pyspark.sql import functions as dbf
df = spark.createDataFrame([1, 2, 3, 3, 4], "int")
df_small = spark.range(3)
df_b = dbf. ? (df_small)
df.join(df_b, df.value == df_small.id).show()broadcast() returns the same DataFrame marked for a broadcast join, and that marked DataFrame is then passed to join().
Source: docs.databricks.comIn SQL you do the same thing with a join hint inside /*+ ... */. BROADCAST, BROADCASTJOIN and MAPJOIN are interchangeable names, and the hint can go on either table in the join. If a table has an alias, the hint has to use the alias name. When conflicting strategy hints are given, BROADCAST wins over MERGE, SHUFFLE_HASH and SHUFFLE_REPLICATE_NL.
-- Join Hints for broadcast join
> SELECT /*+ BROADCAST(t1) */ * FROM t1 INNER JOIN t2 ON t1.key = t2.key;
> SELECT /*+ BROADCASTJOIN (t1) */ * FROM t1 left JOIN t2 ON t1.key = t2.key;
> SELECT /*+ MAPJOIN(t2) */ * FROM t1 right JOIN t2 ON t1.key = t2.key;Hints are only part of the story. On compute with Photon enabled, Databricks picks the join type for you. When a query has several joins, join the smallest tables first, then join that result to the larger tables.
Checkpoint 8 of 8· Check yourself
A query contains /*+ BROADCAST(t1), MERGE(t1, t2) */. Which hint does Databricks SQL apply?
When different join strategy hints conflict, BROADCAST has the highest priority. Databricks logs a warning that the MERGE hint is overridden.
“prioritizes the BROADCAST hint over the MERGE hint over the SHUFFLE_HASH hint”Source: docs.databricks.com
Exam traps
Each one states something that sounds right. Open it to see what is actually true.
1.If you leave out how, df.join(df2, "key") keeps every row of df, like a left join.Why is that wrong?
The default join type is inner. Rows of df with no match in df2 are dropped unless you pass how="left".
Covered in The join() call: other, on and how
2.A SQL join with no ON or USING clause fails, so you cannot get a Cartesian product by accident.Why is that wrong?
With no join criteria, any join type behaves as a CROSS JOIN and returns every pairing of rows.
Covered in Cross joins: every row against every row
Sources
Every claim above is drawn from one of these pages, quoted as it was written on the date shown.
- 1.
“the column(s) must exist on both sides, and this performs an equi-join.”
↩︎ The join() call: other, on and how“a string for the join column name, a list of column names, a join expression (Column), or a list of Columns.”
↩︎ Joining on multiple keys“default inner. Must be one of: inner, cross, outer, full, fullouter, full_outer, left, leftouter, left_outer”
↩︎ Key concept“default inner.”
↩︎ Exam trap 1“default inner.”
↩︎ Prediction“a string for the join column name, a list of column names, a join expression (Column), or a list of Columns.”
↩︎ Checkpoint - 2.
“Returns the rows that have matching values in both table references.”
↩︎ Inner join vs left join: what happens to rows without a match“Returns the values from the left table reference that have no match with the right table reference.”
↩︎ Inner join vs left join: what happens to rows without a match“Matches the rows by comparing equality for list of columns column_name which must exist in both relations.”
↩︎ Joining on multiple keys“If you omit the join_criteria the semantic of any join_type becomes that of a CROSS JOIN.”
↩︎ Exam trap 2“Returns all values from the left table reference and the matched values from the right table reference”
↩︎ Checkpoint“SELECT * will only show one occurrence for each of the columns used to match first”
↩︎ Checkpoint“If you omit the join_criteria the semantic of any join_type becomes that of a CROSS JOIN.”
↩︎ Prediction - 3.
“Cross joins are expensive. Remove cross joins from workloads and queries that require low latency or frequent recomputation.”
↩︎ Cross joins: every row against every row“When performing multiple joins, always join your smallest tables first and then join the result with larger tables.”
↩︎ Broadcast joins: marking the small side - 4.
“Returns the cartesian product with another DataFrame.”
↩︎ Cross joins: every row against every row - 5.
“Marks a DataFrame as small enough for use in broadcast joins.”
↩︎ Broadcast joins: marking the small side - 6.
“add a broadcast hint to instruct Spark to broadcast the smaller table to all executors instead of performing a shuffle join”
↩︎ Broadcast joins: marking the small side - 7.
“The join side with the hint is broadcast regardless of autoBroadcastJoinThreshold.”
↩︎ Broadcast joins: marking the small side“When a table name is occluded by an alias you must use the alias name in the hint”
↩︎ Broadcast joins: marking the small side“prioritizes the BROADCAST hint over the MERGE hint over the SHUFFLE_HASH hint”
↩︎ Checkpoint - 8.https://spark.apache.org/docs/latest/sql-performance-tuning.htmlSecondary source
“Configures the maximum size in bytes for a table that will be broadcast to all worker nodes when performing a join.”
↩︎ Broadcast joins: marking the small side