What this quiz covers
This quiz focuses on Avoiding Join Mistakes, giving you a quick way to practice the rules, question types, and explanations that matter most for SQL.
A report must list every customer and the number of that customer's shipped orders, including customers with no shipped orders. The query uses customers c LEFT JOIN orders o ON c.customer_id = o.customer_id, followed by WHERE o.status = 'SHIPPED'.
Which revision avoids unintentionally discarding customers while also producing a zero count for those without shipped orders?
WHERE, but replace the left join with a full outer join.WHERE and calculate COUNT(*) after grouping the rows by customer.ON clause and calculate COUNT(*) for each customer.o.status = 'SHIPPED' into the ON clause and calculate COUNT(o.order_id) for each customer.SQL Quiz
Practice Avoiding Join Mistakes 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 Avoiding Join Mistakes, 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 report must list every customer and the number of that customer's shipped orders, including customers with no shipped orders. The query uses customers c LEFT JOIN orders o ON c.customer_id = o.customer_id, followed by WHERE o.status = 'SHIPPED'.
Which revision avoids unintentionally discarding customers while also producing a zero count for those without shipped orders?
WHERE, but replace the left join with a full outer join.WHERE and calculate COUNT(*) after grouping the rows by customer.ON clause and calculate COUNT(*) for each customer.o.status = 'SHIPPED' into the ON clause and calculate COUNT(o.order_id) for each customer. (correct answer)LEFT JOIN queries, the placement of filter conditions is critical. A LEFT JOIN preserves all rows from the left table (customers), filling right-table columns with NULL when no match exists. However, the moment you add a WHERE clause that filters on the right table (like o.status = 'SHIPPED'), you silently convert it into an inner join — rows with NULL values fail the condition and get discarded.
The correct fix is D: move o.status = 'SHIPPED' into the ON clause and use COUNT(o.order_id). Placing the condition in ON means it applies during the join, not after. Customers without shipped orders still appear in results, but their order_id column is NULL. Using COUNT(o.order_id) rather than COUNT(*) is essential — COUNT ignores NULL values, so those customers correctly receive a count of 0 instead of 1.
A is wrong because a FULL OUTER JOIN with a WHERE filter on o.status still discards non-matching rows — the same trap, just with a different join type. B fails for the same fundamental reason as the original query: filtering WHERE o.status = 'SHIPPED' after the join eliminates customers with no shipped orders before grouping even occurs, so no grouping logic can recover them. C correctly moves the condition to ON, but using COUNT(*) would count the NULL placeholder row as 1 for customers with no shipped orders, producing inaccurate results.
A reliable rule of thumb: when you need a left join with a filter on the right table, put the filter in ON and use COUNT(right_table_column) — never COUNT(*).An imported contact file contains email addresses but no internal customer identifiers. A report joins it to customers on normalized email address. The customers table permits several customer records to share an email address, and the import can also contain repeated email addresses.
Which approach best prevents accidental many-to-many amplification while retaining a defensible match between the two sources?
DISTINCT on all selected customer and import columns afterward.DISTINCT after the join collapses some duplicate output rows, but only those that are completely identical across all selected columns. Any column that varies between duplicates (like a customer ID or import timestamp) will let phantom rows survive, giving you a false sense of cleanliness.
Answer C misunderstands what LEFT JOIN does. Preserving all import rows doesn't prevent multiple customer rows from matching a single import email — it just ensures unmatched import rows aren't dropped. The many-to-many problem remains entirely intact.
The key study tip: deduplication must happen before the join, not after. Think of it as narrowing each source to a one-row-per-key dataset first, then joining clean inputs together.A self-join is used to list every pair of different employees who work in the same department. The condition includes e1.department_id = e2.department_id and e1.employee_id <> e2.employee_id. The result contains both (Ana, Bo) and (Bo, Ana).
Which predicate change removes mirrored duplicates while retaining every unordered employee pair exactly once?
e1.employee_id IS NOT NULL to exclude duplicate employee combinations.e1.employee_id = e2.employee_id, keeping the department equality condition.e1.employee_id < e2.employee_id, keeping the department equality condition. (correct answer)(Ana, Bo) and (Bo, Ana) show up as separate rows. Your goal is to keep only one representative from each mirrored pair.
The cleanest solution is C: replacing e1.employee_id <> e2.employee_id with e1.employee_id < e2.employee_id. This works because the less-than operator enforces a strict ordering between the two IDs. For any pair (Ana=1, Bo=2), only the row where the smaller ID appears on the left survives — the mirror image (Bo=2, Ana=1) is automatically filtered out because 2 < 1 is false. You still get every unique pairing exactly once, and same-employee rows are implicitly excluded since no ID is less than itself.
A is wrong because adding e1.employee_id IS NOT NULL only filters out null employee IDs — it does nothing to eliminate mirrored duplicates, which exist regardless of nullability.
B is wrong because replacing the inequality with e1.employee_id = e2.employee_id would return only rows where both aliases point to the same employee — the exact opposite of what you want.
D is wrong because switching to a LEFT JOIN changes which rows are included (adding NULLs for unmatched rows), but it doesn't eliminate mirrored duplicates at all.
A practical tip: whenever you need unordered pairs in a self-join, reach for < instead of <> — it's a reliable pattern for deduplication that you'll encounter frequently in SQL interview and exam questions.A query joins invoices to invoice_tags and then calculates SUM(DISTINCT invoices.amount) by account. Repeated tag matches no longer multiply most invoice amounts, but an account containing two different invoices for the same amount is undercounted.
Which revision corrects the logical flaw without assuming invoice amounts are unique?
GROUP BY clause so each invoice amount is counted separately per account.LEFT JOIN so identical invoice amounts are preserved by the aggregate.SUM(DISTINCT amount) by account.SUM(DISTINCT amount) tries to fix this by deduplicating amounts, but it deduplicates by value, not by invoice identity. If two legitimate invoices carry the same dollar amount, DISTINCT collapses them into one, undercounting your total. The real fix is to remove duplicates at the invoice level before aggregating.
Option B is correct because it pre-aggregates to one row per invoice first (using the invoice identifier as the grain, typically with a subquery or CTE like SELECT invoice_id, account_id, MAX(amount) FROM ... GROUP BY invoice_id, account_id), then sums those deduplicated rows by account. This approach deduplicates by identity, not by value, so two invoices with the same amount are both counted.
Option A is wrong because adding invoice_id to the outer GROUP BY breaks the account-level summary — you'd get one row per invoice rather than one row per account, defeating the purpose of the aggregation.
Option C is wrong because switching to LEFT JOIN doesn't eliminate the row multiplication problem; a tag join still creates duplicates for matched invoices, and unmatched invoices simply get a NULL tag row. The core duplication flaw remains.
Option D is wrong because rounding amounts doesn't solve the distinctness problem — it actually makes it worse by potentially collapsing even more distinct amounts into the same value before DISTINCT filters them.
As a study habit, whenever you see SUM(DISTINCT col), ask yourself: "Am I deduplicating by value or by identity?" If your grain isn't naturally unique, pre-aggregate first.Each sale belongs to one product. A product may belong to several categories through product_category. A query joins sales through the bridge and reports revenue by category. Individual category totals are correct, but adding all category totals exceeds total company revenue.
Assuming the bridge contains no duplicate product-category pairs, which interpretation and remedy are most appropriate?
DISTINCT to sale amounts before calculating category totals.DISTINCT on sale amounts wouldn't help and misdiagnoses the issue. Answer C misunderstands the problem; adding product to the GROUP BY would give you finer granularity but the same double-counting when you roll up to category level — it doesn't make totals reconcile. Answer D is wrong because arbitrarily picking one category per product destroys valid business information and produces misleading category reports.
A useful rule of thumb: when subtotals exceed a known grand total after a join, always check whether your join is traversing a many-to-many relationship — that's almost always the culprit.A developer expects an inner join from order_items to products to return exactly one result row for every item: no item should disappear, and no item should be duplicated. The join is order_items.product_id = products.product_id.
Which database conditions are sufficient to guarantee that expectation for all valid stored data?
order_items.product_id has an index, and products.product_id is frequently used in equality joins.order_items.product_id is non-null and references a unique, non-null products.product_id key. (correct answer)products.product_id is non-null, while duplicate product identifiers are removed from the query with DISTINCT.order_items.product_id may be null, but every non-null value has at least one matching product row.order_items.product_id = products.product_id, every item row must find a match (no disappearing rows), and each match must be unique (no duplicates). Answer B delivers both. If order_items.product_id is non-null, null can never silently drop the row (nulls never satisfy equality conditions in SQL). If products.product_id is a unique, non-null key, then each order_items row can match at most one product row — preventing fan-out duplication. Together, these two properties ensure the one-to-one correspondence the developer expects.
A is wrong because indexes are a performance tool, not a data integrity tool. An indexed column can still contain nulls or duplicates; indexing changes query speed, not row counts.
C is wrong for two reasons. DISTINCT collapses duplicates after the fact — it doesn't guarantee a structural one-to-one match, and it can actually hide problems by silently discarding rows you might want to see. Also, only making products.product_id non-null ignores whether order_items.product_id is null or missing a match.
D is wrong because allowing null in order_items.product_id means those rows produce no match in an inner join and vanish — violating the "no disappearing rows" requirement.
Study tip: Whenever a question asks about join correctness, mentally check two constraints: referential integrity (every FK value exists in the PK table) and uniqueness (the PK side has no duplicates). Both must hold for a clean one-to-one inner join.A customer-history table stores several versions of each customer, with valid_from and valid_to dates. An order report joins orders to history using only customer_id. Consequently, each order can match several historical customer versions.
Which join design most directly assigns each order to the correct historical version without duplicate amplification?
DISTINCT over the order columns while omitting history attributes.valid_from date overall.valid_to value to be non-null.order_date >= valid_from AND order_date < valid_to), you guarantee at most one history row matches per order. When the intervals are properly maintained as nonoverlapping, this is a clean one-to-one match with no duplicates and no lost history attributes.
Option A is a workaround, not a solution. Using DISTINCT on order columns simply hides the duplicates rather than preventing them — you're still joining incorrectly and then discarding data. Worse, you've already thrown away the history columns you may need.
Option B selects the history row with the greatest valid_from overall — meaning the most recent version of the customer regardless of when the order was placed. An order from three years ago would be incorrectly attributed to today's customer profile, which corrupts historical reporting.
Option D is nearly meaningless as a filter. Requiring valid_to to be non-null typically just excludes the current open-ended record (often stored as NULL to mean "still active"), which paradoxically drops the most recent version rather than resolving the fanout problem.
Your study tip: whenever you see a history or slowly changing dimension table, immediately think "I need a date-range join" — that's almost always the correct temporal design pattern.A query contains:
FROM customers c, orders o
The intended relationship is c.customer_id = o.customer_id, but no predicate connecting the tables appears in the WHERE clause. The query selects only customer region and order month, then uses DISTINCT.
Why might this mistake escape a quick review of the result while still producing an invalid and potentially expensive query?
DISTINCT can collapse repeated projected values, masking some Cartesian combinations without correcting the missing relationship. (correct answer)DISTINCT causes the optimizer to infer the omitted foreign-key equality and apply it before producing rows.DISTINCT interacts with that explosion and why it can hide the problem.
DISTINCT eliminates duplicate rows in the projected output — in this case, just region and order month. Because there are only a handful of distinct regions and twelve possible months, the output might collapse to a small, tidy-looking grid of combinations. The problem is that those combinations aren't meaningfully paired; they simply reflect every region crossed with every month that exists anywhere in the data. The query looks reasonable, but the underlying computation scanned millions of bogus row pairings. A is correct because it precisely describes this masking effect: DISTINCT removes surface-level repetition without fixing the logical error or reducing the Cartesian join work done by the engine.
B is wrong because the optimizer has no mechanism to infer a missing predicate from DISTINCT — the optimizer can reorder joins and choose indexes, but it cannot manufacture filter conditions that don't exist in your SQL. C is wrong because the comma syntax in FROM carries no implicit foreign-key awareness; it simply produces a cross join regardless of any declared constraints. D is wrong because the problem is both logical and costly — selecting columns from both tables does nothing to guarantee valid pairings; it just projects two columns from each meaningless combination.
As a study habit, always verify that every table in your FROM clause has a corresponding join condition. If you use DISTINCT and the row count seems suspiciously low, treat that as a warning sign, not a sign of correctness.In a multi-tenant system, orders has the composite primary key (tenant_id, order_id). The shipments table uses the same two columns as a foreign key. An analyst joins the tables using only order_id. Order identifiers are unique within a tenant but can be reused by other tenants.
The query currently returns correct-looking results in a test database containing only one tenant. Which revision directly addresses the latent join mistake?
tenant_id to the join condition so both columns of the relationship are matched. (correct answer)DISTINCT to the select list so cross-tenant shipment matches are removed automatically.LEFT JOIN so each order remains present regardless of shipment matches.order_id so shipments belonging to other tenants collapse together.order_id alone is ambiguous because the same value can legally appear across multiple tenants. In a single-tenant test database, this works by accident — there are no duplicate order_id values across tenants yet. But in production, joining only on order_id would match an order from Tenant A with shipments belonging to Tenant B, silently corrupting your results. Answer A fixes this directly by adding tenant_id to the join condition, ensuring both columns of the composite relationship are evaluated together and rows are matched only within the correct tenant context.
Answer B is wrong because DISTINCT removes duplicate rows in the output, not incorrect matches. Cross-tenant rows that look distinct would still appear — you'd just suppress legitimate duplicates alongside them. Answer C is a red herring: switching to a LEFT JOIN changes nullability behavior for unmatched orders but does nothing to prevent wrong tenants from matching in the first place. Answer D is similarly flawed — GROUP BY order_id would collapse rows together, hiding the problem rather than fixing it, and likely producing aggregated nonsense across tenants.
As a study tip: whenever you see a composite foreign key, always verify your join conditions cover every column in that key. A join on a subset of a composite key is a correctness bug that test data often masks.An orders table has one row per order. An order_lines table has multiple rows per order, and a payments table can also have multiple rows per order. A report joins all three tables and groups by customer to calculate merchandise sold and payments received. Orders with multiple lines and multiple payments show inflated totals.
Which change most reliably prevents the duplicate amplification while preserving both measures?
orders before grouping by customer. (correct answer)SUM(DISTINCT amount) separately to line amounts and payment amounts.LEFT JOIN, then group the resulting rows by customer and order identifier.SELECT order_id, SUM(amount) FROM order_lines GROUP BY order_id), then joins those single-row-per-order summaries to orders. By the time you group by customer, every order contributes exactly one line total and one payment total — no amplification possible.
B is tempting because SUM(DISTINCT ...) sounds like it removes duplicates, but it deduplicates by value, not by row identity. If two line items legitimately have the same dollar amount, they'll be collapsed into one — silently undercounting your revenue. It's unreliable, not just slow.
C switches to LEFT JOIN, which controls which orders appear but does nothing to reduce the number of detail rows that cross-join with each other. You'll still get the same Cartesian explosion; you'll just also include orders with no lines or payments.
D selecting distinct joined rows only helps if the duplicates are perfectly identical — but because line and payment identifiers differ across the amplified rows, DISTINCT won't collapse them at all.
Study tip: Whenever you're aggregating from two independent child tables, pre-aggregate each one to the parent's grain first, then join. This "aggregate-before-joining" pattern is the standard defense against fan-out multiplication in SQL.