Hello! Welcome to your next lesson on transforming data with PySpark DataFrames.
In our previous lesson, you learned how to use groupBy() and agg() to calculate powerful summary statistics from a single DataFrame. This is a vital skill for condensing large datasets into meaningful insights.
However, real-world data is rarely contained in a single table. More often, you'll need to combine information from multiple sources—for instance, joining a table of customer data with a table of their orders. Today, you will learn how to join DataFrames using different join types in PySpark. Since you are already familiar with SQL joins, you'll find the concepts very familiar. Our focus will be on the PySpark syntax and the specific join strategies available in the Spark ecosystem.
1. The Anatomy of a PySpark Join
In PySpark, joins are performed using the .join() method on a DataFrame. The basic syntax is:
left_df.join(right_df, join_condition, how="join_type")
Let's break this down:
right_df: The DataFrame you want to join with.join_condition: An expression that tells Spark how to match rows. This is typically a comparison of columns from the two DataFrames (e.g.,left_df.id == right_df.id).how: A string specifying the type of join. If you omit this, PySpark performs aninnerjoin by default.
To get a quick overview of the available join types and their syntax, let's consult a reference guide.
PySpark Join Types | Join Two DataFrames
The article "PySpark Join Types | Join Two DataFrames" from SparkByExamples is an excellent reference for the join operation's syntax and parameters. It provides a solid foundation before we dive into video examples.
Please read the first two sections, "PySpark Join Syntax" and "PySpark Join Types". Focus on the parameters of the join() method and the table that maps the PySpark join strings (like left_outer) to their SQL equivalents.
2. Visualizing the Core Join Types
Before we start coding, it's helpful to have a clear mental model of what each join does. The four most common types are inner, left outer, right outer, and full outer.
The Venn diagrams in the image below provide a classic and effective way to visualize how these joins combine two tables.

- Inner Join: Returns only the rows that have matching keys in both tables.
- Left Join: Returns all rows from the left table, and the matched rows from the right table. If there's no match, the columns from the right table will be
null. - Right Join: Returns all rows from the right table, and the matched rows from the left table. If there's no match, the columns from the left table will be
null. - Full Outer Join: Returns all rows from both tables. It places
nulls where matches don't exist on either side.
3. Implementing Joins in PySpark
Now, let's see these concepts in action. The following video provides a practical, step-by-step walkthrough of performing different joins in a Databricks notebook.
09. Databricks | PySpark Join Types
This video from Raja's Data Engineering demonstrates how to create two sample DataFrames (employee and department) and then join them using various join types.
Please watch the following segments. Pay close attention to the how parameter in the .join() method and observe the output for each join type. Setup: (00:03:29 - 00:05:24) Watch how the employee and department DataFrames are created. This context is important for understanding the join results. Inner Join: (00:05:24 - 00:07:44) See how only employees with a valid department are returned. Full Outer Join: (00:07:44 - 00:08:50) Observe how all employees and all departments are included, with null values for non-matches. Left Outer Join: (00:08:50 - 00:09:53) Note that all employees are kept, even the one with a department ID that doesn't exist in the departments table. Right Outer Join: (00:09:53 - 00:11:06) See how all departments are kept, including one that has no employees. Left Semi Join: (00:11:06 - 00:12:34) This is a filtering join. Notice that the result contains only the columns from the left (employee) DataFrame for rows that have a match in the right DataFrame. Left Anti Join: (00:12:34 - 00:14:06) This is the opposite of a semi join. Observe that it returns only the employees who do not have a match in the departments table.
Join Types Summary
Let's recap the syntax for the joins you just saw.
# Assume we have empDF and deptDF DataFrames
# Inner Join (the default)
empDF.join(deptDF, empDF.emp_dept_id == deptDF.dept_id, "inner").show()
# Left Outer Join
empDF.join(deptDF, empDF.emp_dept_id == deptDF.dept_id, "left_outer").show()
# Right Outer Join
empDF.join(deptDF, empDF.emp_dept_id == deptDF.dept_id, "right_outer").show()
# Full Outer Join
empDF.join(deptDF, empDF.emp_dept_id == deptDF.dept_id, "full_outer").show()
# Left Semi Join (returns columns from left DF only)
empDF.join(deptDF, empDF.emp_dept_id == deptDF.dept_id, "left_semi").show()
# Left Anti Join (returns columns from left DF only)
empDF.join(deptDF, empDF.emp_dept_id == deptDF.dept_id, "left_anti").show()
The Left Semi and Left Anti joins are particularly useful. They act as filters rather than combiners.
- Semi join is equivalent to a
WHERE EXISTSsubquery in SQL: "Give me all employees for whom a department exists." - Anti join is equivalent to a
WHERE NOT EXISTSsubquery: "Give me all employees for whom a department does not exist."
These are excellent for data quality checks or for filtering one list based on another without needing to carry along any columns from the second list.
4. Hands-On Exercise
Time to apply what you've learned. In your Databricks notebook, create the following two DataFrames.
from pyspark.sql.functions import col
# Data for employees
employees_data = [
(1, "Alice", 101),
(2, "Bob", 102),
(3, "Charlie", 101),
(4, "David", 103),
(5, "Eve", 104) # Eve is in a department not listed in the departments table
]
employees_columns = ["emp_id", "emp_name", "dept_id"]
employees_df = spark.createDataFrame(data=employees_data, schema=employees_columns)
print("Employees DataFrame:")
employees_df.show()
# Data for departments
departments_data = [
(101, "Engineering"),
(102, "Marketing"),
(105, "Finance") # Finance department has no employees
]
departments_columns = ["dept_id", "dept_name"]
departments_df = spark.createDataFrame(data=departments_data, schema=departments_columns)
print("Departments DataFrame:")
departments_df.show()
Your Tasks:
- Find all employees and their department names. The result should only include employees who belong to a department that exists in the
departments_df. - Create a list of all employees, including their department name if available. If an employee's department is not in the
departments_df, the department name should benull. - Find all departments that have no employees.
- Identify any "orphan" employees. These are employees assigned to a
dept_idthat does not exist in thedepartments_df.
Solution Walkthrough:
Try to solve the tasks on your own first.
# Task 1: Inner join to find employees with a valid department
print("\nTask 1: Employees with their departments (Inner Join)")
employees_df.join(departments_df, employees_df.dept_id == departments_df.dept_id, "inner") \
.select("emp_id", "emp_name", "dept_name") \
.show()
# Task 2: Left join to list all employees
print("\nTask 2: All employees and their departments (Left Join)")
employees_df.join(departments_df, employees_df.dept_id == departments_df.dept_id, "left") \
.select("emp_id", "emp_name", "dept_name") \
.show()
# Task 3: Find departments with no employees. A right join is a good way to do this.
# We look for departments where the employee information is null.
print("\nTask 3: Departments with no employees (Right Join + Filter)")
departments_df.join(employees_df, employees_df.dept_id == departments_df.dept_id, "right") \
.where(col("emp_id").isNull()) \
.select("dept_name") \
.show()
# An alternative for Task 3 using a left_anti join after swapping the dataframes.
print("\nTask 3 (Alternative): Departments with no employees (Left Anti Join)")
departments_df.join(employees_df, departments_df.dept_id == employees_df.dept_id, "left_anti").show()
# Task 4: Find orphan employees
print("\nTask 4: Employees with a non-existent department (Left Anti Join)")
employees_df.join(departments_df, employees_df.dept_id == departments_df.dept_id, "left_anti").show()
Conclusion
Excellent work! You can now effectively combine data from multiple sources using the full range of join operations available in PySpark. This is a fundamental skill for any data engineer, allowing you to build rich, consolidated datasets for analysis and processing.
Key Takeaways
- The
.join()method is used to combine two DataFrames. The syntax isdf1.join(df2, condition, how). - The
howparameter specifies the join type:inner,left,right,full,left_semi, andleft_antiare the most common. - Inner, Left, Right, and Full joins combine columns from both DataFrames based on the matching logic.
- Left Semi and Left Anti joins act as filters, returning only columns from the left DataFrame based on the presence or absence of a match in the right DataFrame.
- Your existing knowledge of SQL joins directly translates to PySpark, with the main difference being the method-based syntax.
Next Steps
Now that you can read, transform, aggregate, and join data to create a polished final dataset, the next logical step is to save it. In our next lesson, you will learn how to write DataFrames to Delta tables using various save modes, a crucial step for building reliable data pipelines on the Databricks Lakehouse Platform.