What this quiz covers
This quiz focuses on Computing Derived Aggregates, giving you a quick way to practice the rules, question types, and explanations that matter most for SQL.
Each row in orders represents one order and contains order_id and total_amount. Each order can have several matching rows in order_items. A report joins the tables to identify orders containing an item in category 'A', then calculates average order amount among those qualifying orders. An order may contain several category-A items.
Which query avoids bias from join duplication?
SELECT AVG(1.0 * o.total_amount) FROM orders o JOIN order_items i ON i.order_id = o.order_id WHERE i.category = 'A';SELECT 1.0 * SUM(o.total_amount) / NULLIF(COUNT(DISTINCT o.order_id), 0) FROM orders o JOIN order_items i ON i.order_id = o.order_id WHERE i.category = 'A';SELECT 1.0 * SUM(DISTINCT o.total_amount) / NULLIF(COUNT(DISTINCT o.order_id), 0) FROM orders o JOIN order_items i ON i.order_id = o.order_id WHERE i.category = 'A';WITH q AS (SELECT DISTINCT o.order_id, o.total_amount FROM orders o JOIN order_items i ON i.order_id = o.order_id WHERE i.category = 'A') SELECT 1.0 * SUM(total_amount) / NULLIF(COUNT(*), 0) FROM q;SQL Quiz
Practice Computing Derived Aggregates 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 Computing Derived Aggregates, 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.
Each row in orders represents one order and contains order_id and total_amount. Each order can have several matching rows in order_items. A report joins the tables to identify orders containing an item in category 'A', then calculates average order amount among those qualifying orders. An order may contain several category-A items.
Which query avoids bias from join duplication?
SELECT AVG(1.0 * o.total_amount) FROM orders o JOIN order_items i ON i.order_id = o.order_id WHERE i.category = 'A';SELECT 1.0 * SUM(o.total_amount) / NULLIF(COUNT(DISTINCT o.order_id), 0) FROM orders o JOIN order_items i ON i.order_id = o.order_id WHERE i.category = 'A';SELECT 1.0 * SUM(DISTINCT o.total_amount) / NULLIF(COUNT(DISTINCT o.order_id), 0) FROM orders o JOIN order_items i ON i.order_id = o.order_id WHERE i.category = 'A';WITH q AS (SELECT DISTINCT o.order_id, o.total_amount FROM orders o JOIN order_items i ON i.order_id = o.order_id WHERE i.category = 'A') SELECT 1.0 * SUM(total_amount) / NULLIF(COUNT(*), 0) FROM q; (correct answer)total_amount, that one order contributes three times instead of once, inflating or skewing the result. The safest fix is to deduplicate before aggregating — which is exactly what D does. The CTE uses SELECT DISTINCT o.order_id, o.total_amount to collapse duplicates first, then computes a clean SUM / COUNT(*) on the already-unique rows. This is the correct answer.
A is the most dangerous distractor. AVG(o.total_amount) looks clean, but it averages every row, not every order — so a duplicated order is counted multiple times with no correction whatsoever.
B improves on A by using COUNT(DISTINCT o.order_id) in the denominator, but the SUM(o.total_amount) in the numerator still adds every duplicate row. For an order appearing three times, you divide by 1 (distinct order) but sum the amount 3 times — the numerator is still inflated.
C tries to fix B by using SUM(DISTINCT o.total_amount), but this is subtly broken: if two different orders happen to have the same total_amount, DISTINCT drops one of them entirely, undercounting your sum.
Study tip: When you see a JOIN to a child table followed by aggregation on the parent, always ask yourself: did I deduplicate first? A CTE or subquery with DISTINCT on the parent's key is the cleanest, most reliable pattern.A summary table has one row per department with columns successful_cases and attempted_cases. Departments vary greatly in size. Management wants the organization's overall success rate, so every attempted case—not every department—must receive equal weight.
Which aggregate expression produces the required organization-wide rate?
AVG(1.0 * successful_cases / NULLIF(attempted_cases, 0))1.0 * SUM(successful_cases) / NULLIF(SUM(attempted_cases), 0) (correct answer)1.0 * SUM(successful_cases) / NULLIF(COUNT(attempted_cases), 0)1.0 * AVG(successful_cases) / NULLIF(SUM(attempted_cases), 0)1.0 * SUM(successful_cases) / NULLIF(SUM(attempted_cases), 0). The 1.0 forces floating-point division, and NULLIF(..., 0) safely handles a zero denominator. A large department with 1,000 cases contributes proportionally more than a small department with 10 cases — which is precisely what "equal weight per case" requires.
Option A is the classic Simpson's Paradox trap: AVG(rate) computes each department's individual rate and then averages those rates equally across departments, giving a small 10-case department the same influence as a large 1,000-case department. This inflates or deflates the true rate depending on how department size correlates with success.
Option C replaces SUM(attempted_cases) with COUNT(attempted_cases), which counts the number of rows (departments), not the total number of attempted cases. This produces a meaningless denominator unless every department happened to have exactly one attempted case.
Option D mixes AVG(successful_cases) — an average count, not a sum — with SUM(attempted_cases), creating a numerator and denominator that are not on comparable scales, yielding a distorted fraction.
Study tip: Whenever a question involves rates across unequal-sized groups, ask yourself: "Should I pool the totals or average the rates?" Equal weight per event always means pool first, then divide.A report starts with all rows in users and left joins sessions. A user is active if the user has at least one session whose session_date is on or after '2026-01-01'. The required active-user rate is the number of active registered users divided by all registered users. Users can have many qualifying sessions or none.
Which expression correctly computes the active-user rate while preserving users with no sessions?
1.0 * COUNT(DISTINCT CASE WHEN s.session_date >= '2026-01-01' THEN u.user_id END) / NULLIF(COUNT(DISTINCT u.user_id), 0) (correct answer)1.0 * COUNT(CASE WHEN s.session_date >= '2026-01-01' THEN u.user_id END) / NULLIF(COUNT(DISTINCT u.user_id), 0)1.0 * COUNT(DISTINCT u.user_id) / NULLIF(COUNT(DISTINCT CASE WHEN s.session_date >= '2026-01-01' THEN u.user_id END), 0)1.0 * COUNT(DISTINCT CASE WHEN s.session_date >= '2026-01-01' THEN u.user_id END) / NULLIF(COUNT(s.session_id), 0)COUNT(DISTINCT ...) to avoid counting the same user multiple times. The denominator must also count distinct users so that every registered user appears exactly once, regardless of how many total sessions they have. Option A does exactly this — COUNT(DISTINCT CASE WHEN s.session_date >= '2026-01-01' THEN u.user_id END) returns one count per qualifying user, and NULLIF(COUNT(DISTINCT u.user_id), 0) safely counts all registered users while preventing division-by-zero. The LEFT JOIN ensures users with no sessions still appear (their session columns are NULL, so they simply don't satisfy the CASE condition and don't enter the numerator).
Option B drops the DISTINCT in the numerator, so a user with three qualifying sessions gets counted three times, inflating the rate. Option C has the numerator and denominator flipped — it divides all users by qualifying users, which gives values greater than 1 and is the inverse of what's needed. Option D uses COUNT(s.session_id) in the denominator, which counts total session rows (not distinct users), producing a meaningless denominator that shrinks as users have more sessions.
A reliable pattern to remember: whenever a LEFT JOIN can produce multiple rows per parent record, always use COUNT(DISTINCT ...) in any count that touches the child table's columns — otherwise duplicates silently corrupt your result.A grouped query calculates total profit divided by total exposure. Individual exposure values can be positive, negative, or zero. Company policy requires the rate to be NULL whenever the summed exposure for a group equals zero.
Which expression applies the zero check at the correct level?
1.0 * SUM(profit) / SUM(NULLIF(exposure, 0))1.0 * SUM(NULLIF(profit, 0)) / SUM(exposure)1.0 * SUM(profit) / NULLIF(SUM(exposure), 0) (correct answer)SUM(1.0 * profit / NULLIF(exposure, 0))1.0 * SUM(profit) / NULLIF(SUM(exposure), 0), is correct because it first computes SUM(exposure) across the entire group, then uses NULLIF to convert that result to NULL if it equals zero. This perfectly matches the policy: if a group's total exposure is zero, the rate becomes NULL.
Option A, 1.0 * SUM(profit) / SUM(NULLIF(exposure, 0)), applies NULLIF inside the SUM, which nullifies individual rows where exposure is zero before summing. This is wrong because a group where, say, exposures of +100 and -100 cancel to zero would not be caught — the row-level values aren't zero, only their sum is.
Option B applies NULLIF to individual profit values inside SUM, which is irrelevant to the zero-division problem entirely. It protects against nothing meaningful here.
Option D, SUM(1.0 * profit / NULLIF(exposure, 0)), computes a ratio per row and sums those ratios — a fundamentally different calculation from total profit divided by total exposure. It also fails to catch a group where individual exposures are nonzero but sum to zero.
The key study tip: always match the level of your guard (NULLIF, CASE WHEN) to the level where the condition applies. If the rule is about a group total, protect the aggregate result, not the individual inputs.A financial table stores integer columns current_revenue and prior_revenue. For each division, revenue growth rate is defined as the change in total revenue divided by total prior revenue. If total prior revenue is zero, the result must be NULL.
Which expression correctly computes the division's growth rate?
1.0 * (SUM(current_revenue) - SUM(prior_revenue)) / NULLIF(SUM(prior_revenue), 0) (correct answer)1.0 * (SUM(current_revenue) - SUM(prior_revenue)) / NULLIF(SUM(current_revenue), 0)AVG(1.0 * (current_revenue - prior_revenue) / NULLIF(prior_revenue, 0))1.0 * SUM(current_revenue) - SUM(prior_revenue) / NULLIF(SUM(prior_revenue), 0)NULL returned when the denominator is zero. Answer A nails this exactly. Multiplying by 1.0 forces floating-point division (avoiding integer truncation), NULLIF(SUM(prior_revenue), 0) returns NULL when total prior revenue is zero, and the numerator correctly computes the aggregate change. This is the only option that matches the problem definition precisely.
B is wrong because it uses NULLIF(SUM(current_revenue), 0) as the denominator — but the problem defines the denominator as prior revenue, not current revenue. Dividing by current revenue measures something different entirely.
C computes an average of row-level growth rates, which is mathematically different from the division-level growth rate. If rows have different prior revenue magnitudes, this produces a distorted result — you'd be weighting each row equally rather than weighting by revenue size.
D has an operator precedence trap. Without parentheses around the subtraction, SQL evaluates it as: 1.0 \times \text{SUM(current_revenue)} - \frac{\text{SUM(prior_revenue)}}{\text{NULLIF(SUM(prior_revenue), 0)}} which simplifies incorrectly to SUM(current_revenue) - 1. Always parenthesize your numerator expressions to avoid silent precedence bugs.
As a study habit: whenever you write a ratio in SQL, explicitly wrap the full numerator and denominator in parentheses, and always double-check that NULLIF is guarding the correct column.A query groups support tickets by team. The required SLA rate is the number of closed tickets that met SLA divided by the number of closed tickets. Open tickets must not enter either count, and a team with no closed tickets must receive NULL rather than cause an error.
Which expression correctly computes the SLA rate without filtering the team out of the grouped result?
1.0 * SUM(CASE WHEN status = 'closed' AND met_sla = 1 THEN 1 ELSE 0 END) / NULLIF(COUNT(*), 0)AVG(CASE WHEN status = 'closed' AND met_sla = 1 THEN 1.0 ELSE 0.0 END)1.0 * SUM(CASE WHEN status = 'closed' AND met_sla = 1 THEN 1 ELSE 0 END) / NULLIF(SUM(CASE WHEN status = 'closed' THEN 1 ELSE 0 END), 0) (correct answer)1.0 * SUM(CASE WHEN met_sla = 1 THEN 1 ELSE 0 END) / NULLIF(SUM(CASE WHEN status = 'closed' THEN 1 ELSE 0 END), 0)SUM(CASE WHEN status = 'closed' AND met_sla = 1 THEN 1 ELSE 0 END) — only closed, SLA-met tickets. The denominator uses SUM(CASE WHEN status = 'closed' THEN 1 ELSE 0 END) wrapped in NULLIF(..., 0), which counts only closed tickets and returns NULL instead of causing a division-by-zero error when a team has none. This is exactly what the problem requires.
Option A fails because its denominator is COUNT(*), which counts all rows — including open tickets — inflating the denominator and producing an artificially low rate.
Option B uses AVG, which averages 1s and 0s across all rows in the group, not just closed ones. Open tickets contribute 0s to the average, diluting the result exactly as option A does, just through a different mechanism.
Option D has a subtle but critical bug: its numerator sums met_sla = 1 without filtering on status = 'closed', meaning open tickets that happen to have met_sla = 1 would be counted in the numerator even though they're explicitly excluded by the problem.
Study tip: Whenever you write a conditional rate in SQL, verify that your numerator and denominator use matching, explicitly stated conditions — never assume COUNT(*) or an unfiltered column aligns with your intended subset.A shipment table stores region, qty_shipped, and qty_ordered. Both quantity columns use an integer data type. For each region, the required fulfillment rate is total quantity shipped divided by total quantity ordered. The result must retain fractional values and return NULL when the total ordered quantity is zero.
Which expression correctly computes the fulfillment rate?
SUM(qty_shipped) / NULLIF(SUM(qty_ordered), 0)1.0 * SUM(qty_shipped) / NULLIF(SUM(qty_ordered), 0) (correct answer)AVG(1.0 * qty_shipped / NULLIF(qty_ordered, 0))1.0 * SUM(qty_shipped) / NULLIF(COUNT(qty_ordered), 0)NULLIF(expr, 0) elegantly returns NULL instead of 0, making the entire division return NULL as required.
B is correct because multiplying SUM(qty_shipped) by 1.0 first converts the integer sum to a decimal, so the subsequent division produces a fractional result. The NULLIF(SUM(qty_ordered), 0) then handles the zero-denominator case at the correct level — after aggregation — returning NULL when the total ordered quantity is zero. This satisfies both requirements perfectly.
A fails because it divides two plain integers: SUM(qty_shipped) / NULLIF(SUM(qty_ordered), 0). Even though NULLIF handles the zero case, integer ÷ integer still truncates decimals, so a rate of 0.75 would come back as 0.
C is semantically wrong at a conceptual level. AVG(qty_shipped / qty_ordered) computes a per-row ratio first, then averages those ratios — which is not the same as total shipped ÷ total ordered. It also applies NULLIF row-by-row rather than after aggregation, distorting the result.
D replaces SUM(qty_ordered) with COUNT(qty_ordered), which counts rows rather than summing the ordered quantities — a completely different value that does not represent fulfillment rate.
As a quick rule: whenever you see integer columns in division, ask yourself "do I need decimals?" If yes, cast or multiply by 1.0 before dividing.A campaign has two daily records. The first record contains 1 order from 2 visits, and the second contains 9 orders from 90 visits. An analyst needs the campaign's overall conversion rate, with each visit contributing equally.
Which SQL expression produces the required overall conversion rate?
AVG(1.0 * orders / NULLIF(visits, 0))1.0 * SUM(orders) / NULLIF(SUM(visits), 0) (correct answer)1.0 * AVG(orders) / NULLIF(SUM(visits), 0)1.0 * SUM(orders) / NULLIF(AVG(visits), 0)1.0 * SUM(orders) / NULLIF(SUM(visits), 0), does exactly this. The 1.0 * forces floating-point division, and NULLIF(..., 0) safely handles a zero-visit edge case.
Option A uses AVG(orders/visits), which computes each row's rate first — (1/2+9/90)/2=(0.5+0.1)/2=30% — then averages those rates equally by row. This over-weights the small 2-visit day and is visit-unweighted.
Option C mixes AVG(orders) in the numerator with SUM(visits) in the denominator: (5)/92≈5.4%. This is mathematically inconsistent — you're averaging one quantity while summing the other, producing a meaningless result.
Option D does the reverse: SUM(orders) / AVG(visits) = 10/46≈21.7%. Again, mixing aggregation levels creates a distorted denominator.
The key strategy: whenever a question asks for an overall rate "per unit" (per visit, per impression, etc.), always sum both the numerator and denominator separately before dividing — never average the rates directly.After excused assignments have been removed by a WHERE clause, each remaining row represents one required survey assignment. answer_text is NULL when the assignment was not completed. The completion rate is completed assignments divided by all remaining required assignments.
Which expression correctly computes the completion rate?
1.0 * COUNT(CASE WHEN answer_text IS NOT NULL THEN 1 ELSE 0 END) / NULLIF(COUNT(*), 0)1.0 * SUM(CASE WHEN answer_text IS NOT NULL THEN 1 ELSE 0 END) / NULLIF(COUNT(answer_text), 0)1.0 * COUNT(*) / NULLIF(SUM(CASE WHEN answer_text IS NOT NULL THEN 1 ELSE 0 END), 0)1.0 * SUM(CASE WHEN answer_text IS NOT NULL THEN 1 ELSE 0 END) / NULLIF(COUNT(*), 0) (correct answer)answer_text IS NOT NULL. The cleanest way to count completed rows is with SUM(CASE WHEN answer_text IS NOT NULL THEN 1 ELSE 0 END) — this adds 1 for each completed row and 0 for each incomplete row. The total required assignments is simply COUNT(*), which counts every row. Multiplying by 1.0 forces floating-point division, and NULLIF(COUNT(*), 0) guards against division by zero. That's exactly what D does, making it correct.
A is wrong because it uses COUNT(...) around the CASE expression instead of SUM. COUNT counts any non-NULL value — and since the CASE always returns either 1 or 0 (never NULL), it counts every row, not just completed ones. This would return 1.0 (100%) regardless of actual completions.
B is wrong in its denominator: COUNT(answer_text) only counts rows where answer_text IS NOT NULL — the completed ones. So you'd be dividing completed by completed, always returning 1.0.
C flips the numerator and denominator entirely — it divides total rows by completed rows, which gives you the inverse of the completion rate, not the rate itself.
Study tip: Memorize the difference between COUNT(column) (ignores NULLs), COUNT(*) (counts all rows), and SUM(CASE...) (conditional total). These three behave very differently and are a frequent trap in SQL aggregation questions.A review table contains product_id, score, and response_weight. score can be NULL when no score was supplied, while response_weight is non-NULL. The weighted average must include only weights associated with non-NULL scores and must return NULL if their total weight is zero.
Which expression correctly calculates the weighted average score for each product?
1.0 * SUM(score * response_weight) / NULLIF(SUM(response_weight), 0)AVG(1.0 * score * response_weight) / NULLIF(AVG(response_weight), 0)1.0 * SUM(score * response_weight) / NULLIF(SUM(CASE WHEN score IS NOT NULL THEN response_weight ELSE 0 END), 0) (correct answer)1.0 * SUM(CASE WHEN score IS NULL THEN response_weight ELSE score END) / NULLIF(SUM(response_weight), 0)SUM(score * response_weight) naturally ignores NULL scores because any multiplication involving NULL produces NULL, and aggregate functions skip NULLs. In the denominator, SUM(CASE WHEN score IS NOT NULL THEN response_weight ELSE 0 END) explicitly includes only weights paired with valid scores. Wrapping that in NULLIF(..., 0) ensures the expression returns NULL instead of causing a division-by-zero error when no valid scores exist.
Option A is the classic trap: the numerator correctly skips NULL scores, but the denominator uses SUM(response_weight) over all rows — including those with NULL scores. This over-counts the denominator and produces an incorrectly deflated average.
Option B misapplies AVG instead of SUM. The weighted average formula requires summing products and dividing by summed weights, not averaging them separately. This produces a mathematically incorrect result in most cases.
Option D's CASE expression is logically backwards — when score IS NULL, it substitutes response_weight into what should be a score value, mixing incompatible quantities in the numerator.
Study tip: Whenever NULLs can appear in one column but not another, ask yourself whether your denominator needs a CASE filter to stay consistent with what actually entered the numerator.