What this quiz covers
This quiz focuses on Communicating Query Assumptions, giving you a quick way to practice the rules, question types, and explanations that matter most for SQL.
An analyst reports revenue from orders containing accessories with this query:
SELECT o.region, SUM(o.total_amount)
FROM orders AS o
JOIN order_items AS i ON i.order_id = o.order_id
WHERE i.category = 'Accessories'
GROUP BY o.region;
orders has one row per order, while order_items has one row per item.
Which statement best communicates the query's key assumption and limitation?
total_amount contains only the value assigned to the joined accessory item.SQL Quiz
Practice Communicating Query Assumptions 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 Communicating Query Assumptions, 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.
An analyst reports revenue from orders containing accessories with this query:
SELECT o.region, SUM(o.total_amount)
FROM orders AS o
JOIN order_items AS i ON i.order_id = o.order_id
WHERE i.category = 'Accessories'
GROUP BY o.region;
orders has one row per order, while order_items has one row per item.
Which statement best communicates the query's key assumption and limitation?
total_amount contains only the value assigned to the joined accessory item.orders (one row per order) joined to order_items (many rows per order) — you must ask yourself: how many times does each parent row appear in the result set? That's the heart of this question.
When an order contains two accessory items, the JOIN produces two rows for that order, each carrying the same total_amount. Summing that column therefore double-counts the full order value. The query doesn't isolate accessory line-item revenue — it accumulates the entire order total once per matching item row. B is correct because it accurately names both the mechanical behavior (repetition of full order value) and the narrow condition under which the query could be valid (if full-order, multi-attributed credit is genuinely intended).
A is wrong because total_amount lives on the orders table and represents the whole order, not a single line item. The JOIN doesn't magically extract accessory-only revenue from that column.
C is wrong because GROUP BY region collapses rows by region, not by order. If one order has three accessory items, all three rows still roll into the regional sum — each contributing the full total_amount separately.
D is wrong because an inner JOIN retains all matching rows, including orders with multiple accessory items. It drops orders with zero matching items, not orders with many.
Study tip: Any time you see SUM or COUNT after a one-to-many JOIN, immediately check whether the aggregated column belongs to the "one" side — if so, duplicate rows will silently inflate your result.A monthly dashboard calculates daily active users with COUNT(DISTINCT user_id) for each date and then adds the daily results to produce a value labeled monthly active users.
Which note best communicates what the summed value actually represents?
COUNT(DISTINCT user_id) per date correctly counts unique users on that date, but when you sum those daily figures, a user active on 15 different days gets counted 15 times — once per day. The resulting number measures active user-days, not unique people. That's exactly what C captures: it's a sum of daily unique users, and users active on multiple dates inflate the total. This is the honest, precise label for what the metric represents.
A is wrong because it assumes distinctness is transitive across addition — that separate distinct counts somehow remain globally distinct when summed. They don't. Adding two sets of distinct values is not the same as taking the distinct union of those sets. B mischaracterizes the counting mechanism entirely; COUNT(DISTINCT user_id) doesn't count sessions, it collapses multiple sessions per user into a single count per day. The session framing is a fabricated distractor. D gets the direction of the error exactly backwards — summing daily distinct counts overcounts users who return on multiple days, producing a value above the true unique monthly count, not a lower bound.
A useful rule to remember: SUM of DISTINCTs ≠ DISTINCT of all values. Whenever you see a metric built by aggregating distinct counts across time windows, ask yourself whether a returning user would be counted multiple times — if yes, the metric measures activity-days, not unique users.A query chooses each customer's latest address using:
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY updated_at DESC
) AS rn
It then keeps rn = 1. The system permits two address rows for the same customer to have identical updated_at values.
Which assumption or limitation should be documented?
customer_id is unique within each window partition, even when update timestamps tie.ROW_NUMBER(), the critical question to ask is: what happens when the ordering criteria produces ties? Unlike RANK() or DENSE_RANK(), ROW_NUMBER() always assigns a unique integer to every row — but when the ORDER BY column has duplicate values, the database engine gets to decide which tied row receives which number. That decision is not guaranteed to be consistent across executions, query plans, or database versions.
This is exactly why C is correct. When two rows share the same updated_at value, ROW_NUMBER() will assign rn = 1 to exactly one of them — but which one is implementation-defined and effectively arbitrary. The query will still return only one row per customer (that's ROW_NUMBER()'s guarantee), but you cannot predict or rely on which tied address gets selected. This behavior must be documented so engineers don't assume correctness that isn't there.
A is wrong because SQL databases do not implicitly fall back to insertion order when timestamps tie. Physical storage order has no formal role in ORDER BY semantics, so this assumption is dangerous and unfounded. B describes the behavior of RANK() or DENSE_RANK() filtered with rn = 1, not ROW_NUMBER() — ROW_NUMBER() never produces duplicate rank values, so tied customers will not appear more than once. D is wrong because uniqueness of customer_id within the partition is unrelated to tie-breaking on updated_at; it doesn't make the result deterministic.
As a study tip: whenever you see ROW_NUMBER() ORDER BY on a non-unique column, immediately flag the need for a deterministic tie-breaker (like a primary key) appended to the ORDER BY clause.A historical sales report joins each order to customers.customer_segment. The customer table stores only the customer's current segment, and customers may move between segments after placing an order.
Which limitation should be stated when presenting sales by customer segment?
customers table holds only each customer's current segment, any join between historical orders and that table stamps every past order with today's segment label. If a customer moved from "Bronze" to "Gold" after placing an order, that old order now appears under "Gold." The segment distribution in your report reflects the present, not the past. Answer A correctly identifies this limitation — the historical classification is driven by the current segment, not the one active at order time.
B is wrong because it falsely implies the join captures the segment effective on the order date. That would require a slowly changing dimension (SCD) or a segment-history table — neither of which exists here. One current row per customer does not solve the problem.
C describes a different scenario entirely — one where a customer appears in multiple segments simultaneously. Since only the current segment is stored, a customer occupies exactly one segment in the report, not all historical ones.
D is dangerously misleading. Segment name uniqueness has no bearing on historical accuracy. A customer's completed order does not freeze their segment; the join will always pull today's value regardless.
As a study habit, whenever you see a join to a "current-state" table, immediately flag it as a potential point-in-time accuracy problem — this is a classic data warehouse pitfall that exams love to test.A report labeled January sign-ins uses the following filter:
WHERE occurred_at >= TIMESTAMP '2026-01-01 00:00:00+00'
AND occurred_at < TIMESTAMP '2026-02-01 00:00:00+00'
The occurred_at column stores UTC timestamps. Most users are in North American time zones.
Which note most accurately communicates the reporting assumption created by this filter?
>= '2026-01-01 00:00:00+00' and < '2026-02-01 00:00:00+00', which is a classic half-open interval — the start is included, the end is excluded. Both boundaries are expressed in UTC (+00). That means "January" in this report is purely a UTC construct. A user in New York (UTC−5) who logs in at 11:30 PM on December 31st local time is actually at 4:30 AM UTC on January 1st — captured in the report. Conversely, a user who signs in at 11:30 PM on January 31st local time (4:30 AM February 1st UTC) is excluded. This is exactly what C describes: UTC boundaries with a half-open interval, where local-calendar January can differ near either edge.
A is wrong because the filter does not reference user time zones at all — there's no per-user conversion happening. B introduces the concept of ingestion time, which has nothing to do with the filter shown; occurred_at reflects event timestamps, not pipeline arrival times. D misreads the interval — the < operator excludes February 1st exactly, so events at that precise moment are not included in January.
As a study habit, always parse interval endpoints carefully: < vs <= is a one-character difference with real business consequences, especially when time zones are involved.A dashboard for the previous day's transactions refreshes at 02:00 UTC. The source team's documented service level states that all prior-day files are normally delivered and processed by 06:00 UTC. The query itself succeeds and shows no processing errors.
Which caveat is most appropriate for the dashboard?
A report totals January orders in a common currency by joining each order to exchange_rates on currency code and rate_date = CURRENT_DATE. The join is an inner join, and the rate table does not always contain a current-date row for every currency.
Which note most accurately communicates the report's assumptions and limitations?
exchange_rates on currency code and rate_date = CURRENT_DATE. This means every order is converted using today's exchange rate — not its original order date's rate. If exchange_rates has no current-date row for a given currency, that join condition fails, and the order is silently dropped from the result set entirely. Answer B captures exactly this behavior: current-date rates are applied, and orders in currencies without a current rate are omitted.
Answer A is wrong because it claims missing historical rates are "replaced with the current rate" — the opposite of what happens. The report doesn't touch historical rates at all; it only looks up today's rate, and if that's missing, the order disappears rather than being substituted. Answer C is wrong because it misunderstands the join entirely — joining on CURRENT_DATE doesn't freeze values in their original currency; it actively applies a rate conversion to restate amounts. Answer D invents behavior that doesn't exist anywhere in the query: there's no averaging logic, and inner joins don't produce zero-value rows for unmatched records — they produce no rows at all.
Your takeaway: on SQL questions, distinguish between what an inner join excludes versus what a LEFT JOIN would preserve as NULL. Silently dropping rows is a common real-world data integrity trap worth flagging in any report design.A support dashboard calculates:
AVG(closed_at - created_at)
The report filters tickets by created_at during the quarter. Open tickets have a null closed_at.
Which annotation most accurately describes the reported average?
AVG() in SQL, the critical concept to understand is how NULL values are handled. SQL's AVG() function ignores NULLs entirely — it sums only the non-null values and divides by the count of non-null rows, not the total row count.
Here, closed_at - created_at produces a NULL whenever closed_at is null (i.e., the ticket is still open). So AVG() only sees results from tickets that have actually been closed, and those open tickets contribute nothing — not a zero, not a partial value, nothing. The filter on created_at means the pool of tickets considered is restricted to those created during the quarter, but the average is then computed only over the subset with non-null closure times. That makes D the most precise description.
A is wrong because it claims open tickets are treated as having zero resolution time. They aren't — a NULL in the average is dropped entirely, not counted as zero. That's a subtle but critical distinction.
B is wrong because it misidentifies the filter. The query filters by created_at (when tickets were created), not by closed_at (when they were closed). Tickets closed this quarter but created last quarter would be excluded.
C is wrong because it implies open tickets contribute to the average measured through the report run time — but since closed_at is null, those rows are silently excluded, not measured through any alternative endpoint.
The key study tip: whenever you see AVG(), ask yourself which rows have NULLs in that column? Those rows vanish from the calculation entirely — a frequent source of misleading aggregate results.A report calculates customer conversion using this structure:
FROM customers AS c
JOIN sessions AS s
ON s.customer_id = c.customer_id
AND s.started_at >= :month_start
AND s.started_at < :next_month
LEFT JOIN purchases AS p
ON p.customer_id = c.customer_id
AND p.purchased_at >= :month_start
AND p.purchased_at < :next_month
The numerator counts distinct customers with a purchase, and the denominator counts distinct joined customers.
Which description should accompany the conversion rate?
customers before applying joins to sessions and purchases.customers is inner-joined to sessions with a date filter baked directly into the ON clause. That means only customers who have at least one session during the specified month make it past this join. A registered customer with zero sessions in that period simply disappears from the result set — they produce no matching rows and are dropped entirely. The subsequent LEFT JOIN to purchases then operates only on that already-filtered population, preserving all session-having customers regardless of whether they purchased. So the denominator — your count of distinct joined customers — represents customers with at least one session during the month, making B correct.
A mistakes the starting table for the denominator. Beginning with customers doesn't mean all customers are counted; it just means you're starting there before filtering. The inner join immediately narrows the set.
C misreads the left join's behavior. A LEFT JOIN keeps rows from the left side even when no match exists on the right — it doesn't remove them. Customers without purchases still appear; their purchase columns are simply NULL.
D describes a FULL OUTER JOIN or a UNION-style logic. Here, the inner join on sessions creates an intersection, not a union — customers without sessions are excluded regardless of purchase activity.
As a study tip: always read join types and ON clause conditions together. A date-filtered inner join is a hidden filter that shapes your dataset before any aggregation occurs.To estimate unique visitors in a very large event table, an analyst applies block-level TABLESAMPLE SYSTEM (10), computes APPROX_COUNT_DISTINCT(visitor_id), and multiplies the result by 10. Events are physically clustered by account, and visitors may generate different numbers of events.
Which disclosure most accurately states the assumptions and limitations of this estimate?
TABLESAMPLE SYSTEM, which samples at the block level, not the row level. Because events are physically clustered by account, entire blocks may belong to specific accounts, meaning some accounts could be over- or under-represented in the sample. The scale-up assumption — that 10× the sampled visitor count approximates total visitors — only holds if the sampled blocks happen to capture visitors proportionally across all accounts and activity levels. That's an assumption, not a guarantee. Additionally, APPROX_COUNT_DISTINCT introduces its own algorithmic approximation error (typically via HyperLogLog). So the final estimate inherits both sources of uncertainty: the sampling representativeness assumption and the distinct-count algorithm error. Answer A correctly identifies both limitations without overstating or understating either.
Answer B is wrong because proportional event sampling does not guarantee proportional visitor coverage — a visitor who generates many events is more likely to appear in any given sample than a low-activity visitor, distorting the distinct count scaling.
Answer C is wrong because block-level sampling is not automatically uniform across accounts when data is physically clustered. Clustering breaks the uniformity assumption that row-level random sampling would provide.
Answer D is wrong because the estimate can actually exceed the true count — approximation algorithms and non-representative sampling can both inflate results, not just deflate them.
A useful rule of thumb: whenever you see stacked estimation techniques (sampling plus approximation), assume stacked errors — each layer contributes its own assumptions and inaccuracies.