What you will be able to do
- Explain why a WHERE, HAVING or JOIN condition that evaluates to NULL drops rows, and rewrite it so the rows you want are kept
- Replace = with the null-safe <=> operator when NULL keys should match
- Choose the join type that returns the rows a question asks for, and spot a missing join condition that turns a join into a CROSS JOIN
Key concept
Three-valued logic in filters — In Databricks SQL a condition can be True, False or Unknown (NULL), and WHERE, HAVING and JOIN keep a row only when the condition is True. Many queries that run without error but return the wrong rows come down to a NULL turning a condition into Unknown, which quietly removes the row.
1.Why rows go missing: NULL in WHERE and HAVING
The hardest query bugs to find are the ones that throw no error. The query runs and returns a result that looks reasonable, but rows are missing. The usual cause is NULL. The Databricks NULL semantics reference uses a small person table with seven rows. Two of them, Marry and Albert, have a NULL age. Keep that table in mind for the next question.
Here is why. The comparison operators >, >=, =, < and <= return NULL whenever either operand is NULL. WHERE, HAVING and JOIN keep a row only when the condition is True. A condition that comes out NULL is treated like False, so the row is dropped without any warning.
-- Persons whose age is unknown (`NULL`) are filtered out from the result set.
> SELECT * FROM person WHERE age > 0;
name age
-------- ---
Michelle 30
Fred 50
Mike 18
Dan 50
Joe 30To keep those rows, say so explicitly. Add OR age IS NULL to the condition. IS NULL belongs to the group of expressions designed to handle NULL input. It returns True for a NULL value, so the OR branch is True for Marry and Albert, and all seven rows come back.
-- `IS NULL` expression is used in disjunction to select the persons
-- with unknown (`NULL`) records.
> SELECT * FROM person WHERE age > 0 OR age IS NULL;The logical operators follow the same three-valued rules. TRUE OR NULL is True, but FALSE OR NULL is NULL. TRUE AND NULL is NULL, but FALSE AND NULL is False. NOT NULL is still NULL. So wrapping a comparison in NOT does not bring back NULL rows: WHERE NOT (age > 18) also drops Marry and Albert. HAVING behaves the same way. In the reference example GROUP BY age HAVING max(age) > 18, the group whose age is NULL is skipped.
Checkpoint 1 of 5· Check yourself
Which expression evaluates to True?
OR is True as soon as one side is True, whatever the other side is. Every other expression listed evaluates to NULL.
“True | NULL | True | NULL”Source: docs.databricks.com
Checkpoint 2 of 5· Exam question
An analyst runs this query in Databricks SQL and gets an error that `product` must appear in the `GROUP BY` clause or be used in an aggregate function: ``` SELECT region, product, SUM(sales_amount) AS total_sales FROM sales GROUP BY region ``` The analyst wants total sales broken out by both region and product. What change fixes the query so it returns the intended per-product totals?
Correct answer: A — Add `product` to the grouping list so the clause reads `GROUP BY region, product`, matching every non-aggregated column named in the `SELECT` list.
- A. This is correct: every column referenced outside an aggregate function must be listed in `GROUP BY`, so adding `product` produces one row per region-product pair with the sales correctly summed within each pair.
- B. This removes the error but changes the result: `FIRST()` returns whatever product happens to appear first for that region, so the query no longer reports true per-product totals as the analyst intended.
- C. This avoids the grouping error, but dropping the column means the query can no longer show sales split out by product at all, which does not satisfy the stated requirement.
- D. Filtering on `product` in `WHERE` restricts which rows are included rather than grouping by them, so the output still collapses to one row per region and never shows a per-product breakdown.
Sources1
2.Matching NULL to NULL: the null-safe <=> operator
The same rule causes problems with equality. NULL = NULL evaluates to NULL, not True. A join or self-join on a column that can be NULL therefore never matches rows where that column is NULL. In the reference self-join of person on p1.age = p2.age AND p1.name = p2.name, only five rows come back, because Marry and Albert do not match even themselves.
The null-safe equal operator <=>. It returns True when both operands are NULL and False when only one is. It never returns NULL, so the condition is always definitely True or False.
| Left operand | Right operand | = | <=> |
|---|---|---|---|
| NULL | Any value | NULL | False |
| Any value | NULL | NULL | False |
| NULL | NULL | NULL | True |
When <=> replaces = on the age comparison, the same self-join returns all seven rows, and Albert and Marry now pair with themselves. Only make this change if NULL should really count as a match. If unknown values should not be paired, = already gives the right answer.
Checkpoint 3 of 5· Fill the gap
Complete the self-join so that persons with an unknown (NULL) age are still matched to themselves.
> SELECT * FROM person p1, person p2
WHERE p1.age ? p2.age
AND p1.name = p2.name;<=> returns True when both sides are NULL. With =, the comparison returns NULL and the join drops those rows.
Sources1
3.Wrong join type, missing join condition
The other common reason for a wrong row count is the join itself. If you write JOIN with no qualifier, you get an INNER join, which keeps only rows that match on both sides. When the question says "all employees, with their department if known", an inner join silently drops every employee without a matching department. Each join type answers a different question:
| Join type | Rows returned |
|---|---|
| [ INNER ] (default) | Rows with matching values in both table references |
| LEFT [ OUTER ] | All left rows plus matched right values; NULL where there is no match |
| RIGHT [ OUTER ] | All right rows plus matched left values; NULL where there is no match |
| FULL [ OUTER ] | All rows from both sides, NULL on the side without a match |
| [ LEFT ] SEMI | Left rows that have a match on the right (left columns only) |
| [ LEFT ] ANTI | Left rows that have no match on the right |
| CROSS | The Cartesian product of the two relations |
The reference example has six employees and three departments. An inner join returns three rows. Switching to LEFT JOIN returns all six employees, with NULL deptname for Chloe, Evan and Amy, whose departments 5, 4 and 6 do not exist:
-- Use employee and department tables to demonstrate left join.
> SELECT id, name, employee.deptno, deptname
FROM employee
LEFT JOIN department ON employee.deptno = department.deptno;
105 Chloe 5 NULL
103 Paul 3 Engineering
101 John 1 Marketing
102 Lisa 2 Sales
104 Evan 4 NULL
106 Amy 6 NULLTo find employees *without* a department, don't write a LEFT JOIN and then filter on NULL. An ANTI JOIN does exactly that: it returns Chloe, Evan and Amy, with only the employee columns. SEMI JOIN is the opposite: it returns the matching employees without adding any department columns.
No error is raised. When the join criteria are left out, any join type behaves as a CROSS JOIN, so every employee is paired with every department and you get 6 × 3 = 18 rows. When a join suddenly returns far more rows than either table has, check whether the condition is missing. If you join with USING (column), the column must exist in both tables, or Databricks raises UNRESOLVED_USING_COLUMN_FOR_JOIN. SELECT * with USING shows each join column only once.
Checkpoint 4 of 5· Match them up
Match each requirement to the join that answers it
Tap a term, then the definition that fits it.
ANTI keeps left rows without a match and SEMI keeps left rows with one. LEFT keeps all left rows and adds NULLs. CROSS is the Cartesian product.
“Returns the values from the left table reference that have no match with the right table reference.”Source: docs.databricks.com
Checkpoint 5 of 5· Check yourself
A LEFT JOIN between a 6-row table and a 3-row table returns 18 rows. What is the most likely fix?
18 = 6 × 3 is a Cartesian product, and that is what any join type produces when the join criteria are left out.
“If you omit the join_criteria the semantic of any join_type becomes that of a CROSS JOIN.”Source: docs.databricks.com
Sources2
Exam traps
Each one states something that sounds right. Open it to see what is actually true.
1.A filter such as
WHERE age > 0keeps rows where age is NULL, because NULL is not a failing value.Why is that wrong?A comparison with NULL returns NULL, and WHERE keeps only rows whose condition is True, so NULL rows are dropped unless you add
OR age IS NULL.2.Joining on
a.key = b.keywill match rows where both keys are NULL.Why is that wrong?NULL = NULLevaluates to NULL, so those rows are filtered out by the join. Only the null-safe<=>returns True for two NULLs.Covered in Matching NULL to NULL: the null-safe <=> operator
3.A JOIN without an ON clause raises a syntax error, so a missing condition is easy to spot.Why is that wrong?
Leaving out the join criteria is valid SQL. It turns any join into a CROSS JOIN and silently multiplies the rows.
Covered in Wrong join type, missing join condition
Sources
Every claim above is drawn from one of these pages, quoted as it was written on the date shown.
- 1.
“The result of these operators is unknown or NULL when one of the operands or both the operands are unknown or NULL.”
↩︎ Why rows go missing: NULL in WHERE and HAVING“`IS NULL` expression is used in disjunction to select the persons”
↩︎ Why rows go missing: NULL in WHERE and HAVING“returns False when one of the operand is NULL and returns True when both the operands are NULL”
↩︎ Matching NULL to NULL: the null-safe <=> operator“a condition expression is a boolean expression and can return True, False or Unknown (NULL).”
↩︎ Key concept“The result of these operators is unknown or NULL when one of the operands or both the operands are unknown or NULL.”
↩︎ Exam trap 1“The persons with unknown age (`NULL`) are filtered out by the join operator.”
↩︎ Exam trap 2“Persons whose age is unknown (`NULL`) are filtered out from the result set.”
↩︎ Prediction“True | NULL | True | NULL”
↩︎ Checkpoint - 2.
“Returns the rows that have matching values in both table references. The default join-type.”
↩︎ Wrong join type, missing join condition“If you omit the join_criteria the semantic of any join_type becomes that of a CROSS JOIN.”
↩︎ Wrong join type, missing join condition“If you omit the join_criteria the semantic of any join_type becomes that of a CROSS JOIN.”
↩︎ Exam trap 3“Returns the values from the left table reference that have no match with the right table reference.”
↩︎ Checkpoint