CertSafari
    Databricks Certified Data Engineer Associate· Lessons

    Domain 3 · Lesson 11/33

    DataFrame Joins in Databricks: Inner, Left, Multi-Key, Cross and Broadcast

    Combine DataFrames with operations

    14 min read
    3.14% of exam
    8 sources
    Published 2 Oct 2026
    Docs as of 30 Sep 2026

    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.

    Joining on a column name. Only Bob is in both DataFrames, and the shared name column appears once in the result.python
    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()?

    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.

    A SQL left join. The employee view has six rows, and department only has deptno 1, 2 and 3.sql
    > SELECT id, name, employee.deptno, deptname
       FROM employee
       LEFT JOIN department ON employee.deptno = department.deptno;
    Join types in Databricks SQL and the matching PySpark how values
    SQL join typePySpark how valueRows returned
    [ INNER ] (the default)innerOnly rows with matching values in both tables
    LEFT [ OUTER ]left / leftouter / left_outerEvery left row, with NULL on the right side when there is no match
    CROSS JOINcrossThe Cartesian product of the two tables
    [ LEFT ] SEMIsemi / leftsemi / left_semiLeft rows that have a match on the right
    [ LEFT ] ANTIanti / leftanti / left_antiLeft 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.

    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?

    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.

    A USING join on two keys is an ON join with one equality condition per key, joined with ANDsql
    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.b

    The 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?

    Sources21

    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.

    crossJoin pairs each of 3 people with each of 2 heights, giving 6 rowspython
    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?

    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?

    Sources34

    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()

    In 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.

    Three spellings of the broadcast join hintsql
    -- 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?

    Sources56783

    Exam traps

    Each one states something that sounds right. Open it to see what is actually true.

    1. 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. 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. 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. 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. 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. 5.
      “Marks a DataFrame as small enough for use in broadcast joins.”
      ↩︎ Broadcast joins: marking the small side
    5. 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
    6. 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
    7. 8.
      “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

    Continue to page 2 of 2

    Union vs Union All in Databricks: Stacking DataFrames by Position or by Name

    Spotted a mistake, or was something unclear? Tell us.