🔍 Ctrl+K
🟢 Easy

Sample Tables — Simple Data

Venn view — df1 (4) • df2 (3) INNER only overlap LEFT all left RIGHT all right FULL OUTER all rows

Venn view of 7 JOIN types — refer to df1 (4 rows) / df2 (3 rows) sample.

All PySpark JOIN examples use these two small DataFrames. Visualize them before reading each Venn diagram.

df1 — employees

idnamedept_id
1Alice10
2Bob20
3Charlie10
4DavidNULL

df2 — departments

dept_iddept_name
10HR
20Engineering
30Marketing
How to read
df1 has 4 rows (David has NULL), df2 has 3 rows (Marketing has no match). Every JOIN card shows which of the 4+3 rows survive.
🟡 Intermediate

Spark Join Types

Spark supports several types of joins for combining DataFrames. Understanding when to use each type is crucial for both interviews and production code.

🟢 Easy

Inner Join

Returns only rows that have matching keys in both DataFrames.

DF1
DF2
df1.join(df2, df1.id == df2.id, "inner")

Result Example:

id name value
1 Alice 100
3 Charlie 300
Use case: When you need only matching records from both sides
🟢 Easy

Left Join (Left Outer)

Returns all rows from the left DataFrame, and matching rows from the right DataFrame. NULL for non-matching right rows.

DF1
DF2
df1.join(df2, df1.id == df2.id, "left")

Result Example:

id name value
1 Alice 100
2 Bob NULL
3 Charlie 300
Use case: When you need all records from the left table with optional matching from right
🟢 Easy

Right Join (Right Outer)

Returns all rows from the right DataFrame, and matching rows from the left DataFrame. NULL for non-matching left rows.

DF1
DF2
df1.join(df2, df1.id == df2.id, "right")

Result Example:

id name value
1 Alice 100
3 Charlie 300
4 NULL 400
Use case: When you need all records from the right table with optional matching from left
🟢 Easy

Full Outer Join

Returns all rows from both DataFrames. NULL where there is no match.

DF1
DF2
df1.join(df2, df1.id == df2.id, "outer")
Use case: When you need all records from both tables, with NULLs for non-matches
🟡 Intermediate

Left Semi Join

Returns rows from the left DataFrame where a match exists in the right DataFrame, but returns only columns from the left side.

DF1
DF2
df1.join(df2, df1.id == df2.id, "left_semi")
Use case: Similar to EXISTS in SQL. Useful for filtering left DataFrame based on right.

Expected Output (left_semi - only left columns, only matched):

idname
1Alice
3Charlie

Bob (id 2, no match) and David (id 4, NULL) excluded. Only left side columns returned.

🟡 Intermediate

Left Anti Join

Returns rows from the left DataFrame where no match exists in the right DataFrame.

DF1
DF2
df1.join(df2, df1.id == df2.id, "left_anti")
Use case: Similar to NOT EXISTS in SQL. Useful for finding records in left that don't have matches in right.

Expected Output (left_anti - only unmatched left):

idname
2Bob
4David

Alice and Charlie have matches, so excluded. Only Bob and David remain.

🔴 Advanced

Broadcast Join

Broadcast join sends the smaller DataFrame to all executors so that the large DataFrame does not need to perform a large shuffle.

Large DataFrame
Row 1
Row 2
Row 3
Row 4
Row 5
JOIN
Result
Small DataFrame
Key A
Key B
→ broadcast →
Executor 1
Key A
Key B
Executor 2
Key A
Key B
Executor 3
Key A
Key B
from pyspark.sql.functions import broadcast

# Broadcast the smaller DataFrame
result = large_df.join(
    broadcast(small_df),
    "key"
)

Expected Output (same as inner join, but faster):

idnamedept
1AliceHR
2BobEngineering
3CharlieHR

Result identical to INNER JOIN on sample tables (David excluded), but no shuffle of small_df.

Configuration:

# Set broadcast threshold (default 10MB)
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", "10m")

✅ Advantages

  • Can avoid large shuffle
  • Fast when the smaller DataFrame fits in executor memory

❌ Disadvantages

  • Not suitable if the supposedly small DataFrame is too large
  • Memory pressure can occur
Use case: Large Transaction Data + Small Dimension Data

💡 Interview Tip: Broadcast joins are the fastest join strategy but only work when one side is small enough to fit in memory.

🔴 Advanced

Sort-Merge Join

Sort-Merge Join is commonly used when joining two large DataFrames. Both sides are partitioned, sorted, and then merged.

DataFrame A
Partition
Sort
Merge
Result
DataFrame B
Partition
Sort

Process Steps:

  1. Both DataFrames are hash-partitioned on the join key
  2. Each partition is sorted
  3. Sorted partitions are merged

Expected Output (same as inner, via sort-merge):

idnamedept
1AliceHR
2BobEngineering
3CharlieHR

Both DataFrames were partitioned and sorted on dept_id before merging. David (NULL) excluded.

✅ Advantages

  • Suitable for large datasets
  • Default join strategy for large-large joins

❌ Disadvantages

  • Requires sorting
  • Can require significant shuffling
  • Sorting/shuffling can increase execution time

💡 Interview Tip: Sort-merge join is Spark's default for large datasets. Understanding its mechanics helps optimize join performance.

🔴 Advanced

Join Optimization Tips

1

Use Broadcast when possible

Broadcast the smaller DataFrame to avoid shuffle.

from pyspark.sql.functions import broadcast

# Force broadcast join
result = large_df.join(broadcast(small_df), "key")
2

Partition by join key

Pre-partition data to reduce shuffle.

# Repartition by join key before joining
df1_repartitioned = df1.repartition("join_key")
df2_repartitioned = df2.repartition("join_key")

result = df1_repartitioned.join(df2_repartitioned, "join_key")
3

Handle data skew

Use salting for skewed join keys.

import pyspark.sql.functions as F
import random

# Add salt to handle skew
salt_buckets = 10
df1_salted = df1.withColumn(
    "salted_key",
    F.concat(F.col("key"), F.lit("_"), (F.rand() * salt_buckets).cast("int"))
)

# Explode the salted keys on the other side
df2_exploded = df2.withColumn(
    "salt", F.explode(F.array([F.lit(i) for i in range(salt_buckets)]))
).withColumn(
    "salted_key",
    F.concat(F.col("key"), F.lit("_"), F.col("salt"))
)

result = df1_salted.join(df2_exploded, "salted_key")
4

Use broadcast hint

Force broadcast join with hint.

# Using broadcast hint
result = df1.join(
    df2.hint("broadcast"),
    "key"
)

# Or using SQL hint
df1.createOrReplaceTempView("table1")
df2.createOrReplaceTempView("table2")

result = spark.sql("""
    SELECT /*+ BROADCAST(table2) */ *
    FROM table1
    JOIN table2 ON table1.key = table2.key
""")
5

Filter early

Reduce data before joining.

# Filter before join to reduce data size
df1_filtered = df1.filter(F.col("date") >= "2024-01-01")
df2_filtered = df2.filter(F.col("status") == "active")

result = df1_filtered.join(df2_filtered, "key")

💡 Interview Tip: Always consider broadcast join first for small-large joins. It can improve performance by orders of magnitude.