What this quiz covers
This quiz focuses on Checking Null Rates, giving you a quick way to practice the rules, question types, and explanations that matter most for SQL.
A customers table has 100 customers. Exactly 10 have region IS NULL. Each of those 10 customers has 10 orders, while each of the other 90 customers has one order. An analyst joins customers to orders and calculates AVG(CASE WHEN c.region IS NULL THEN 1.0 ELSE 0 END) over the joined rows.
What does the query report, and how should the customer-level NULL rate be obtained?
customers before performing the one-to-many joinorders because orders determine the joined denominatorAVG with COUNT(DISTINCT c.customer_id) after the joinSQL Quiz
Practice Checking Null Rates in SQL with focused quiz questions that help you check what you know, review explanations, and build confidence with test-style prompts.
This quiz focuses on Checking Null Rates, giving you a quick way to practice the rules, question types, and explanations that matter most for SQL.
Try each quiz question before looking at the correct answer. Use the explanations to review missed ideas, then come back to similar questions until the pattern feels familiar.
A customers table has 100 customers. Exactly 10 have region IS NULL. Each of those 10 customers has 10 orders, while each of the other 90 customers has one order. An analyst joins customers to orders and calculates AVG(CASE WHEN c.region IS NULL THEN 1.0 ELSE 0 END) over the joined rows.
What does the query report, and how should the customer-level NULL rate be obtained?
customers before performing the one-to-many join (correct answer)orders because orders determine the joined denominatorAVG with COUNT(DISTINCT c.customer_id) after the joinAVG(CASE WHEN c.region IS NULL THEN 1.0 ELSE 0 END) computes 190100≈52.6%. Answer A is correct — it reports roughly 52.6%, and to get the true customer-level rate you should calculate the NULL proportion directly from the customers table before joining.
Answer B is wrong because the join does not give each customer equal weight. Customers with more orders appear in more rows, so high-order customers are over-represented in the average.
Answer C is wrong in its reasoning. While 190 is indeed the joined denominator, the NULL rows total 100 (not 90), so 100/190≈52.6%, not 47.4%. That figure would arise from mistakenly counting the non-NULL rows as the numerator.
Answer D is wrong because COUNT(DISTINCT c.customer_id) would count distinct customers in each group, not compute a rate, and 5% doesn't follow from any coherent calculation here.
Study tip: Any time you compute a rate or average on a column from the "one" side of a one-to-many join, pre-aggregate on that table first. Otherwise, entities with more related rows silently inflate their weight.In orders, 12 rows have customer_id IS NULL, and 8 additional rows have non-NULL customer IDs that do not exist in customers. customers.customer_id is a non-NULL primary key. The goal is to return all 20 orders with either an absent foreign key value or an unmatched foreign key.
Which query returns exactly the intended set?
SELECT o.* FROM orders o JOIN customers c ON c.customer_id = o.customer_id WHERE c.customer_id IS NULL;SELECT o.* FROM orders o LEFT JOIN customers c ON c.customer_id = o.customer_id WHERE c.customer_id IS NULL; (correct answer)SELECT o.* FROM orders o WHERE o.customer_id IS NULL AND o.customer_id NOT IN (SELECT customer_id FROM customers);SELECT o.* FROM orders o WHERE o.customer_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM customers c WHERE c.customer_id = o.customer_id);WHERE c.customer_id IS NULL then filters to only the rows where no matching customer was found — which happens both when o.customer_id IS NULL (no join is possible) and when o.customer_id has a value that simply doesn't exist in customers (the join attempted but failed). Both of your 20 target rows satisfy this condition.
Option A uses an INNER JOIN, which only returns rows where a match exists on both sides. Since it requires a valid customer match, c.customer_id can never be NULL after an inner join — so this query returns zero rows. Option C applies IS NULL AND NOT IN simultaneously, which is logically contradictory: if o.customer_id IS NULL, the NOT IN subquery behaves unpredictably (NULL comparisons in NOT IN can suppress all results), and more importantly the two conditions can never both be true for the same row. Option C captures neither group. Option D only retrieves the 8 rows with non-NULL unmatched IDs — it explicitly excludes NULL customer IDs with o.customer_id IS NOT NULL, missing the 12 NULL rows entirely.
Your takeaway: whenever you need to find "unmatched or absent" foreign keys, LEFT JOIN + WHERE right-side key IS NULL is the reliable, all-in-one pattern.A child table has a composite foreign key (region_id, account_id). Five rows contain these pairs: (1, 2), (1, NULL), (NULL, 2), (3, 4), and (NULL, NULL). The parent contains (1, 2) but not (3, 4). A key is incomplete if either component is NULL. An orphan must have both components present but no matching parent pair.
Which classification logic produces 3 incomplete keys and 1 orphan?
Region East has 1,000 records with a phone NULL rate of 10%. Region West has 100 records with a phone NULL rate of 40%. An analyst averages the two regional rates and reports 25% as the company-wide NULL rate.
Which assessment correctly recalculates the company-wide row-level NULL rate?
A transaction extract contains 1,000 rows. COUNT(customer_id) returns 920, and COUNT(DISTINCT customer_id) returns 700. Multiple transactions may legitimately belong to the same customer. The requested metric is the percentage of transaction rows with a missing customer key.
Which conclusion is supported by these results?
customer_id IS NULL (correct answer)COUNT(customer_id) unusableCOUNT() in SQL, the most important distinction to internalize is that COUNT(column_name) counts only non-NULL values, while COUNT(*) counts every row regardless of NULLs. This question tests whether you understand what each aggregate function actually measures.
Here's the key logic: the extract has 1,000 total rows, and COUNT(customer_id) returns 920. Because COUNT(customer_id) skips NULL values, it tells you directly that 920 rows have a valid customer key. That means 1,000−920=80 rows have customer_id IS NULL. The missing-key rate is therefore 1,00080=8%, confirming C as the correct answer.
A is wrong because the 700 distinct customer IDs describe uniqueness among valid keys — not missingness. The gap between 1,000 and 700 reflects both NULLs and repeat customers, so you cannot isolate missing keys this way.
B makes a similar error by subtracting 920 (non-NULL count) from 1,000 and calling the 80-row gap a 20% missing rate — but 1,00080=8%, not 20%. This is a simple arithmetic mistake in the final step.
D is a trap for students who distrust COUNT(column_name) in the presence of duplicates. Duplicates are irrelevant here — COUNT(customer_id) ignores NULLs and counts duplicates normally, which is exactly the behavior you need.
Study tip: Whenever you need to count missing values, remember that COUNT(*) - COUNT(column) isolates NULLs precisely — DISTINCT is a separate concern about uniqueness, not completeness.A column contains 500 values: 30 SQL NULLs, 20 empty strings, 10 strings containing only spaces, 5 occurrences of 'N/A', and 435 usable values. The data-quality rule defines all four exceptional forms as missing.
Which result and predicate correctly implement the business-defined missing-value rate?
value IS NULL, because SQL NULL is the only database-level missing valuevalue IS NULL OR TRIM(value) = '', excluding the documented sentinel valueNULLIF(TRIM(value), '') IS NULL, which also treats 'N/A' as emptyvalue IS NULL OR TRIM(value) = '' OR TRIM(value) = 'N/A' (correct answer)'N/A' sentinel values (5), totaling 30+20+10+5=65 missing values out of 500. That gives a rate of 65÷500=13%, achieved with value IS NULL OR TRIM(value) = '' OR TRIM(value) = 'N/A'. The TRIM() call collapses space-only strings to empty strings, catching both empty and whitespace-only entries in a single condition, and the final clause catches the documented sentinel. That makes D the correct answer.
Choice A only flags SQL NULLs — that's 30÷500=6% — completely ignoring the three other business-defined missing forms. Choice B uses value IS NULL OR TRIM(value) = '', which catches NULLs, empty strings, and space-only strings (30+20+10=60), reaching 12%, but it deliberately excludes 'N/A', contradicting the stated data-quality rule. Choice C applies NULLIF(TRIM(value), '') IS NULL, which converts empty-after-trim values to NULL and then checks for NULL — but 'N/A' trimmed is still 'N/A', not an empty string, so it is not caught by this predicate. C reaches the same 12% as B, not 12% with 'N/A' included as the answer claims.
When you see a missing-value rate problem, always inventory all exceptional forms the business defines, then verify your predicate covers each one individually. A single overlooked sentinel can shift your rate — and your answer.A profiling query reports that sales_leads contains 240 rows and that COUNT(email) returns 198. Duplicate email addresses are allowed, and no rows are filtered.
Which SQL expression correctly calculates the NULL rate for email?
1.0 * (COUNT(*) - COUNT(email)) / COUNT(*), which returns 17.5% (correct answer)1.0 * COUNT(email) / COUNT(*), which returns 82.5%1.0 * (COUNT(*) - COUNT(email)) / COUNT(email), which returns about 21.2%1.0 * COUNT(DISTINCT email) / COUNT(*), which cannot be derived from the given countsCOUNT(*) counts every row regardless of NULLs, while COUNT(column) counts only non-NULL values. So the number of NULL rows is simply COUNT(*)−COUNT(email)=240−198=42. Dividing by the total gives the NULL rate: 42/240=0.175, or 17.5%. The 1.0 * prefix forces floating-point division so you don't get integer truncation. That's exactly what A computes, making it correct.
B inverts the question entirely — it calculates the fill rate (how often email is present, 82.5%), not the NULL rate. It's measuring the complement of what you want.
C uses the right numerator (42 missing rows) but divides by COUNT(email) (198) instead of COUNT(*) (240). That denominator represents only the non-NULL rows, so the result — about 21.2% — has no meaningful real-world interpretation as a rate.
D introduces COUNT(DISTINCT email), which removes duplicates. Since the passage explicitly allows duplicate emails, this count is unknown from the given information, so the calculation can't even be completed.
A reliable memory anchor: NULL rate = missing ÷ total, and "total" always means COUNT(*). Any formula using COUNT(column) in the denominator is almost certainly a trap.A staging table incoming_codes(code) must be checked against valid_codes(code). Both columns are nullable, and valid_codes currently contains at least one NULL. The required output is every row whose incoming code is non-NULL and has no equal non-NULL value in valid_codes.
Which query reliably identifies the missing reference keys despite the NULL in valid_codes?
SELECT s.* FROM incoming_codes s WHERE s.code IS NOT NULL AND s.code NOT IN (SELECT code FROM valid_codes);SELECT s.* FROM incoming_codes s WHERE s.code IS NOT NULL AND NOT EXISTS (SELECT 1 FROM valid_codes v WHERE v.code = s.code); (correct answer)SELECT s.* FROM incoming_codes s WHERE s.code IN (SELECT code FROM valid_codes WHERE code IS NOT NULL);SELECT s.* FROM incoming_codes s WHERE s.code IS NULL OR EXISTS (SELECT 1 FROM valid_codes v WHERE v.code = s.code);NOT IN with a subquery that might return NULLs, stop and think carefully — this is one of SQL's most common and dangerous traps.
Here's why it matters: SQL uses three-valued logic (TRUE, FALSE, UNKNOWN). When you compare any value to NULL using =, the result is UNKNOWN, not FALSE. The NOT IN operator internally checks whether your value equals any value in the list. If that list contains even one NULL, every comparison produces UNKNOWN, and NOT IN returns UNKNOWN rather than TRUE — meaning no rows are returned at all, regardless of what's actually in valid_codes.
Option B is correct because NOT EXISTS sidesteps this entirely. It checks for the presence of a matching row, returning FALSE when no match is found and ignoring NULLs that don't participate in the join condition v.code = s.code. The s.code IS NOT NULL guard ensures you're only checking meaningful incoming values, giving you exactly the rows that are non-NULL and absent from valid references.
Option A fails precisely because of the NULL trap described above — the NOT IN subquery pulls the NULL from valid_codes, poisoning every comparison and returning zero rows.
Option C inverts the logic entirely: it returns rows whose code is found in valid_codes, which is the opposite of what's needed.
Option D returns rows that are NULL or already matched — essentially identifying rows that should probably be excluded, not flagged as missing.
Your takeaway: never use NOT IN with a subquery unless you can guarantee the subquery returns no NULLs. When in doubt, reach for NOT EXISTS instead — it's NULL-safe by design.An analyst intends to measure the NULL rate of status after excluding only rows whose status is 'cancelled'. The analyst runs: SELECT AVG(CASE WHEN status IS NULL THEN 1.0 ELSE 0.0 END) FROM jobs WHERE status <> 'cancelled'; The result is 0% even though the source contains rows with status IS NULL.
Which modification preserves the intended denominator and correctly exposes the NULL statuses?
COUNT(status) * 1.0 / COUNT(*)WHERE status <> 'cancelled' AND status IS NULL and keep the same averageWHERE status <> 'cancelled' OR status IS NULL and keep the same average (correct answer)WHERE status = 'cancelled' OR status IS NOT NULL and invert the averageAVG. The core issue here is how <> interacts with NULL values in SQL's three-valued logic.
In SQL, NULL <> 'cancelled' evaluates to UNKNOWN, not TRUE. This means the WHERE status <> 'cancelled' clause silently eliminates all NULL rows — exactly the rows you're trying to measure. That's why the result is 0%: no NULL rows survive to be counted.
The fix is answer C: changing the filter to WHERE status <> 'cancelled' OR status IS NULL. This explicitly rescues NULL rows by adding a second condition. Since status IS NULL evaluates to TRUE for those rows, the OR ensures they pass the filter regardless of the first condition's UNKNOWN result. Your denominator now includes all non-cancelled rows plus NULL rows, and the AVG correctly exposes the NULL rate.
A is wrong because COUNT(status) skips NULLs by definition — it counts only non-NULL values — so the ratio still hides NULL rows rather than measuring them.
B is wrong because WHERE status <> 'cancelled' AND status IS NULL is logically contradictory: it keeps only NULL rows, making the denominator just the NULL rows themselves and destroying the meaningful rate calculation.
D is wrong because WHERE status = 'cancelled' OR status IS NOT NULL does the opposite — it includes cancelled rows and excludes NULLs, completely inverting the intended population.
Study tip: Always remember that <> value silently drops NULLs in SQL. Whenever your filter might interact with NULLs, explicitly handle them with IS NULL or IS NOT NULL.