col NOT IN (a, b, c) is internally equivalent to col <> a AND col <> b AND col <> c. If the list contains even one NULL, col <> NULL → UNKNOWN, and UNKNOWN propagates through the AND chain, so all rows are excluded.
-- When the list contains NULL, UNKNOWN propagates and all rows are excluded col NOT IN (10, 30, NULL) -- Internal expansion: col <> 10 AND col <> 30 AND col <> NULL (← UNKNOWN) -- Solution 1: NOT EXISTS (unaffected by NULL) WHERE NOT EXISTS (SELECT 1 FROM sub WHERE sub.col = t.col) -- Solution 2: exclude NULL from the subquery WHERE col NOT IN (SELECT col FROM sub WHERE col IS NOT NULL)
From the employees table, extract employees in departments that are not managed (their dept_id does not exist in managed_depts). Return the columns emp_id, name, dept_id, ordered by emp_id ascending.
| emp_id | name | dept_id |
|---|---|---|
| 1 | Tanaka | 10 |
| 2 | Suzuki | 20 |
| 3 | Sato | 30 |
| 4 | Ito | 40 |
| 5 | Yamada | NULL |
| 6 | Takahashi | 20 |
| dept_id | dept_name |
|---|---|
| 10 | Sales |
| 30 | HR |
| NULL | Undefined |
Note: managed_depts.dept_id contains NULL (an undefined department). Writing NOT IN (SELECT dept_id FROM managed_depts) lets NULL enter the list and makes all rows disappear. Use NOT EXISTS for a NULL-safe result.
Expected output (emp_id ascending):
| emp_id | name | dept_id |
|---|---|---|
| 2 | Suzuki | 20 |
| 4 | Ito | 40 |
| 5 | Yamada | NULL |
| 6 | Takahashi | 20 |
Note: dept_id=10 (Tanaka) and dept_id=30 (Sato) are managed departments and are excluded. With NOT IN, the NULL in managed_depts makes UNKNOWN exclude all 6 rows, producing 0 rows.
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
Aggregate functions automatically ignore NULL. Only COUNT(*) counts every row; COUNT(col), SUM(col), and AVG(col) exclude rows where the column is NULL. In particular, AVG's shrinking denominator creates a quiet bug in which "the average for representatives with results" is reported as "the average for everyone."
-- Different behavior COUNT(*) -- count all rows (including NULL) COUNT(amount) -- count non-NULL rows only -- AVG excludes NULL and shrinks the denominator (equivalent to SUM / COUNT(amount)) AVG(amount) -- Average for everyone, treating NULL as 0 AVG(COALESCE(amount, 0))
From the sales_results table, calculate the headcount, number of representatives with results, average sales for representatives with results, and average for everyone including 0 results by department (dept). Return the columns dept, head_count, active_reps, avg_amount, true_avg, ordered by dept ascending.
| rep_id | dept | amount |
|---|---|---|
| 1 | Tokyo | 250000 |
| 2 | Tokyo | 300000 |
| 3 | Tokyo | NULL |
| 4 | Tokyo | NULL |
| 5 | Osaka | 150000 |
| 6 | Osaka | 200000 |
| 7 | Osaka | 180000 |
| 8 | Osaka | NULL |
Note: rep_id=3,4 (Tokyo) and rep_id=8 (Osaka) have amount=NULL (no results this month). COUNT(*) counts everyone, while COUNT(amount) counts only representatives with results. Pay attention to how far avg_amount and true_avg diverge.
Expected output (dept ascending):
| dept | head_count | active_reps | avg_amount | true_avg |
|---|---|---|---|---|
| Osaka | 4 | 3 | 176667 | 132500 |
| Tokyo | 4 | 2 | 275000 | 137500 |
Note: Osaka: SUM=530000, AVG (denominator 3)=176667, true_avg (denominator 4)=132500. Tokyo: SUM=550000, AVG (denominator 2)=275000, true_avg (denominator 4)=137500. The gap between avg_amount and true_avg shows the effect of excluding NULL.
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
The purpose of a LEFT JOIN is to preserve every row from the left table. If you want to specify a condition on right-table columns (such as orders above a certain amount) while still preserving left-table rows that do not meet the condition as NULL, write the filter condition in the ON clause.
-- Writing the condition in ON preserves non-matching rows as NULL FROM customers c LEFT JOIN orders o ON c.customer_id = o.customer_id AND o.amount > 20000
For every customer in the customers table, display the maximum amount of orders above 20,000 yen (large_order). For customers with no large order or no orders at all, display NULL for large_order. Return the columns customer_id, customer_name, large_order, ordered by customer_id ascending.
| customer_id | customer_name |
|---|---|
| 1 | Tanaka Trading |
| 2 | Suzuki Trading |
| 3 | Sato Trading |
| 4 | New Customer |
| order_id | customer_id | amount |
|---|---|---|
| 1 | 1 | 10000 |
| 2 | 1 | 35000 |
| 3 | 1 | 40000 |
| 4 | 2 | 28000 |
| 5 | 2 | 8000 |
| 6 | 3 | 5000 |
| 7 | 3 | 12000 |
Note: customer 4 (New Customer) has no orders. Customer 3 (Sato Trading) has orders, but none above 20,000 yen. Put the filter condition in the ON clause to preserve customers 3 and 4 in the result.
Expected output (customer_id ascending):
| customer_id | customer_name | large_order |
|---|---|---|
| 1 | Tanaka Trading | 40000 |
| 2 | Suzuki Trading | 28000 |
| 3 | Sato Trading | NULL |
| 4 | New Customer | NULL |
The ON-clause filter preserves non-matching customers 3 and 4 as NULL, so all 4 customers are correctly returned.
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
NULLIF(expr, value) returns NULL when expr = value; otherwise it returns expr unchanged. Using it to prevent division by zero safely returns NULL instead of raising an error.
-- Basic: NULL when a = b; otherwise return a NULLIF(a, b) -- Prevent division by zero (return NULL when units=0, making the result NULL) revenue / NULLIF(units, 0) -- Return 0 when division by zero would occur COALESCE(revenue / NULLIF(units, 0), 0)
For each daily campaign record in the campaign_daily table, calculate and output clicks, conversion rate (conv_rate = conversions / clicks), and revenue per click (rev_per_click = revenue / clicks). For rows where clicks is 0, display NULL for conv_rate and rev_per_click (prevent division by zero with NULLIF). Return the columns date, campaign, clicks, conv_rate, rev_per_click, ordered by date and campaign ascending.
| date | campaign | clicks | conversions | revenue |
|---|---|---|---|---|
| 2024-03-01 | CP_A | 1000 | 50 | 200000 |
| 2024-03-01 | CP_B | 0 | 0 | 0 |
| 2024-03-02 | CP_A | 800 | 32 | 160000 |
| 2024-03-02 | CP_B | 500 | 20 | 100000 |
| 2024-03-03 | CP_A | 0 | 0 | 0 |
| 2024-03-03 | CP_B | 600 | 24 | 120000 |
Note: CP_B (2024-03-01) and CP_A (2024-03-03) have clicks=0 (days with no delivery). Because dividing by 0 raises an error, convert zero to NULL with NULLIF(clicks, 0) before division. Round conv_rate and rev_per_click to 4 decimal places.
Expected output (date and campaign ascending):
| date | campaign | clicks | conv_rate | rev_per_click |
|---|---|---|---|---|
| 2024-03-01 | CP_A | 1000 | 0.0500 | 200.0000 |
| 2024-03-01 | CP_B | 0 | NULL | NULL |
| 2024-03-02 | CP_A | 800 | 0.0400 | 200.0000 |
| 2024-03-02 | CP_B | 500 | 0.0400 | 200.0000 |
| 2024-03-03 | CP_A | 0 | NULL | NULL |
| 2024-03-03 | CP_B | 600 | 0.0400 | 200.0000 |
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
LAG(col, offset, default) returns the value from a previous row, but NULL can arise for two different reasons: the column itself is NULL, or the first row has no previous row (a boundary NULL). These causes must be distinguished, and the design must prevent unintended NULL propagation.
-- ① Prevent boundary NULL only (the first row becomes 0, but a NULL in the previous row remains NULL) LAG(revenue, 1, 0) OVER (ORDER BY month) -- ② Prevent both boundary NULL and NULL in the data itself (practical pattern) LAG(COALESCE(revenue, 0), 1, 0) OVER (ORDER BY month)
LAG is used only when the previous row does not exist, such as for the first row. It cannot prevent NULL when the previous row exists but its value is NULL; in that case, NULL is returned unchanged. In practice, protect the target column with COALESCE and also provide LAG's third argument to obtain a reliable default value (such as 0) in every situation.From the monthly_revenue table, calculate monthly revenue (revenue), previous-month revenue (prev_revenue), and month-over-month growth rate (mom_growth).
When revenue is NULL because data was not collected, treat it as 0. When using LAG() to obtain the previous month's revenue, also return 0 for the first month and when the previous month's revenue is 0 (including a value corrected from NULL).
Calculate mom_growth = ROUND((current month - previous month) / previous month, 4), and display NULL when the previous month is 0 and the calculation cannot be performed (division by zero). Return month, revenue, prev_revenue, mom_growth, ordered by month ascending.
| month | revenue |
|---|---|
| 2024-01 | 1000000 |
| 2024-02 | 1200000 |
| 2024-03 | NULL |
| 2024-04 | 1100000 |
| 2024-05 | 1400000 |
| 2024-06 | 1350000 |
Note: treat revenue=NULL in 2024-03 (data not collected) as 0. For the first month, such as 2024-01, and when the previous month is 0 (2024-04), use NULLIF to safely output NULL instead of raising a division-by-zero error.
Expected output (month ascending):
| month | revenue | prev_revenue | mom_growth |
|---|---|---|---|
| 2024-01 | 1000000 | 0 | NULL |
| 2024-02 | 1200000 | 1000000 | 0.2000 |
| 2024-03 | 0 | 1200000 | -1.0000 |
| 2024-04 | 1100000 | 0 | NULL |
| 2024-05 | 1400000 | 1100000 | 0.2727 |
| 2024-06 | 1350000 | 1400000 | -0.0357 |
Note: in 2024-03, revenue becomes 0 and month-over-month growth is -1.0000 (-100%). In 2024-04, prev_revenue is 0, so division by zero is avoided and mom_growth is NULL.
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free