This assignment covers foundational to advanced SQL querying against the craftBrewery relational database, which models a small craft brewery's operations including beers, styles, batches, taproom sales, wholesale accounts, orders, and territories. Students are expected to demonstrate mastery of SELECT with filtering and sorting (Q1), INNER JOIN across two tables (Q2), LEFT JOIN with aggregation for zero-count inclusion (Q3), WHERE+GROUP BY+HAVING for filtered aggregation (Q4), multi-table join chains with aggregation and status filtering (Q5), correlated subquery with IN (Q6), conditional aggregation using CASE WHEN inside SUM for pivoting (Q7), and scalar subquery inside HAVING for cross-group comparison (Q8). Grading should reward correct logic and result accuracy, with meaningful partial credit for syntactically sound but logically incomplete attempts.
Partial credit should reflect how close the student's logic is to the correct solution, not just whether results match. A query that uses the right technique with a minor error (wrong column name, missing ORDER BY, off-by-one in HAVING threshold) should receive 70–85% of points. A query that demonstrates the right approach but has a fundamental logical flaw (wrong join type, missing required filter, wrong aggregate) should receive 40–60%. A query that queries the right tables but uses incorrect technique (e.g., JOIN instead of required subquery) should receive 30–50%. No credit should be awarded for queries that do not execute or that query entirely wrong tables. Syntactically valid queries that return zero rows due to a filter error should receive no more than 40% — the output is meaningless even if the structure is partially right.
1. Identify the target table: craftbrewery.beers contains all required columns (beerName, abv, ibu, isActive). 2. Write SELECT specifying exactly beerName, abv, ibu. 3. Add WHERE beers.isActive = 1 to filter out inactive beers. 4. Add ORDER BY beers.abv DESC to sort highest ABV first. Final query: SELECT beers.beerName, beers.abv, beers.ibu FROM craftbrewery.beers WHERE beers.isActive = 1 ORDER BY beers.abv DESC; Expected: 77 rows.
Award full 8 points for correct columns, correct filter, and correct descending order. Deduct 2 points if ORDER BY is ASC instead of DESC. Deduct 2 points if WHERE clause is missing (returns inactive beers). Deduct 1 point if extra unrequested columns are selected but logic is otherwise correct. Award 5/8 if columns and filter are correct but ordering is entirely absent.
Why might a brewery store isActive as a tinyint (0/1) rather than a boolean or a varchar like 'Yes'/'No'? What are the trade-offs in terms of storage, readability, and portability across database systems?
1. Both beers and styles tables are needed. 2. The join key is beers.styleId = styles.styleId (FK relationship). 3. Use INNER JOIN (or JOIN) because only beers with a matching style should appear. 4. SELECT beers.beerName, styles.styleName, styles.description. 5. ORDER BY styles.styleName ASC, beers.beerName ASC — style name is the primary sort, beer name is the tiebreaker. Final query: SELECT beers.beerName, styles.styleName, styles.description FROM craftbrewery.beers JOIN craftbrewery.styles ON beers.styleId = styles.styleId ORDER BY styles.styleName ASC, beers.beerName ASC; Expected: 260 rows.
Full 11 points for correct join type, correct ON clause, correct columns, and correct two-level ORDER BY. Deduct 3 points for using LEFT JOIN (returns extra rows). Deduct 2 points if ORDER BY has only one level or is reversed. Deduct 1 point for wrong join key. Award 6/11 if the correct tables and intent are shown but join condition and ordering both have errors.
In this brewery context, would it ever make sense to have a beer with no associated style? How does the database design (FK constraint) reflect a business rule, and how does JOIN type selection enforce that rule at query time?
1. Start from accounts (the 'many' side we want to preserve). 2. LEFT JOIN orders ON accounts.accountId = orders.accountId to keep accounts with no orders. 3. Use COUNT(orders.orderId) — this correctly returns 0 when orderId is NULL (no orders). COUNT(*) would incorrectly return 1. 4. GROUP BY accounts.accountId, accounts.accountName, accounts.accountType — include accountId to avoid name collision issues. 5. ORDER BY totalOrders DESC. Final query: SELECT accounts.accountName, accounts.accountType, COUNT(orders.orderId) AS totalOrders FROM craftbrewery.accounts LEFT JOIN craftbrewery.orders ON accounts.accountId = orders.accountId GROUP BY accounts.accountId, accounts.accountName, accounts.accountType ORDER BY totalOrders DESC; Expected: 260 rows.
Full 11 points for correct LEFT JOIN, COUNT(orders.orderId), proper GROUP BY, and correct ORDER BY. Deduct 4 points for INNER JOIN (fundamentally wrong — excludes zero-order accounts). Deduct 3 points for COUNT(*) with LEFT JOIN (zero-order accounts show 1 instead of 0). Deduct 1 point for missing accountId in GROUP BY if database still runs but is technically ambiguous. Award partial credit of 6/11 for correct structure with both COUNT and JOIN type wrong.
Why is COUNT(column) different from COUNT(*) specifically in the context of outer joins? Can you think of other aggregate functions where NULL handling from a LEFT JOIN could lead to subtly wrong results?
1. Join styles to beers on styleId. Use INNER JOIN because we only care about styles that have active beers. 2. Add WHERE beers.isActive = 1 to filter to active beers BEFORE grouping. 3. GROUP BY styles.styleId, styles.styleName. 4. Add HAVING COUNT(beers.beerId) > 5 to keep only styles with more than 5 active beers. 5. SELECT styles.styleName and COUNT(beers.beerId) AS activeBeerCount. 6. ORDER BY activeBeerCount DESC. Final query: SELECT styles.styleName, COUNT(beers.beerId) AS activeBeerCount FROM craftbrewery.styles JOIN craftbrewery.beers ON styles.styleId = beers.styleId WHERE beers.isActive = 1 GROUP BY styles.styleId, styles.styleName HAVING COUNT(beers.beerId) > 5 ORDER BY activeBeerCount DESC; Expected: 9 rows.
Full 12 points for correct JOIN, WHERE for isActive, GROUP BY, HAVING threshold, and ORDER BY. Deduct 3 points if WHERE isActive = 1 is in HAVING instead — logic is wrong even if results accidentally match on this dataset. Deduct 4 points if isActive filter is missing entirely. Deduct 1 point if styleId is missing from GROUP BY but query runs. Award 7/12 for correct JOIN and HAVING structure but wrong filter placement or missing isActive filter.
In SQL, the logical execution order is: FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY. How does understanding this order help you decide whether to place a condition in WHERE versus HAVING? Are there performance implications?
1. The join chain must be: territories → accounts (on territoryId) → orders (on accountId) → orderLines (on orderId). 2. Add WHERE orders.status = 'Delivered' to restrict to delivered orders only. 3. SELECT territories.territoryName, territories.salesRep, SUM(orderLines.lineTotal) AS totalRevenue. 4. GROUP BY territories.territoryId, territories.territoryName, territories.salesRep. 5. ORDER BY totalRevenue DESC. Final query: SELECT territories.territoryName, territories.salesRep, SUM(orderLines.lineTotal) AS totalRevenue FROM craftbrewery.territories JOIN craftbrewery.accounts ON territories.territoryId = accounts.territoryId JOIN craftbrewery.orders ON accounts.accountId = orders.accountId JOIN craftbrewery.orderLines ON orders.orderId = orderLines.orderId WHERE orders.status = 'Delivered' GROUP BY territories.territoryId, territories.territoryName, territories.salesRep ORDER BY totalRevenue DESC; Expected: 12 rows.
Full 12 points for complete join chain, correct WHERE filter, correct SUM target, and correct GROUP BY and ORDER BY. Deduct 4 points for missing the status filter. Deduct 3 points for a broken join chain (missing one table). Deduct 2 points for SUM on wrong column. Deduct 1 point for missing territoryId in GROUP BY. Award 6/12 for correct intent and partial join chain with missing filter.
This query requires chaining four tables. How does the order in which you write joins affect readability versus performance? Does SQL's query optimizer rearrange join order, and if so, why might a developer still care about the written order?
1. The outer query selects from batches: batchCode, brewDate, volumeLitres, status. 2. The filter is WHERE batches.beerId IN (...). 3. The subquery selects beerTags.beerId FROM craftbrewery.beerTags WHERE beerTags.tagName = 'Hoppy'. 4. This returns all beerIds associated with the 'Hoppy' tag. 5. ORDER BY batches.brewDate ASC. Final query: SELECT batches.batchCode, batches.brewDate, batches.volumeLitres, batches.status FROM craftbrewery.batches WHERE batches.beerId IN (SELECT beerTags.beerId FROM craftbrewery.beerTags WHERE beerTags.tagName = 'Hoppy') ORDER BY batches.brewDate ASC; Expected: 551 rows.
Full 15 points for correct subquery structure, correct tagName filter inside subquery, correct outer columns, and correct ORDER BY. Deduct 4 points for using a JOIN instead of IN subquery (technique not demonstrated). Deduct 3 points if subquery is missing the tagName = 'Hoppy' filter. Deduct 2 points for wrong ORDER BY direction. Award 10/15 for correct IN subquery structure with tagName filter missing or misspelled.
This query could also be written as a JOIN between batches and beerTags. What are the advantages and disadvantages of each approach (IN subquery vs JOIN)? Are there cases where one significantly outperforms the other?
1. Join beers to taproomSales on beerId. 2. GROUP BY beers.beerId, beers.beerName — one row per beer. 3. For smallTotal: SUM(CASE WHEN taproomSales.servingType IN ('Flight', 'Half Pint') THEN taproomSales.saleAmount ELSE 0 END). 4. For standardTotal: SUM(CASE WHEN taproomSales.servingType = 'Pint' THEN taproomSales.saleAmount ELSE 0 END). 5. For largeTotal: SUM(CASE WHEN taproomSales.servingType IN ('Growler Fill', 'Crowler') THEN taproomSales.saleAmount ELSE 0 END). 6. ORDER BY beers.beerName ASC. The INNER JOIN ensures only beers that have taproom sales appear. Final query as specified in solution_sql. Expected: 234 rows.
Full 16 points for correct JOIN, correct three CASE WHEN expressions with ELSE 0, correct GROUP BY, and correct ORDER BY. Deduct 3 points for missing ELSE 0 in any CASE (produces NULLs in totals). Deduct 2 points per category with wrong serving type mapping. Deduct 2 points for grouping by beerName only. Deduct 3 points for using AVG instead of SUM. Award 10/16 for correct structure with one or two serving type mismatches.
The CASE WHEN inside SUM pattern is often called 'conditional aggregation' or a 'pivot.' SQL has no native PIVOT syntax in MySQL. When would you choose conditional aggregation in SQL versus pivoting the data in application code or a BI tool? What are the scalability trade-offs?
1. Join accounts → orders (on accountId) → orderLines (on orderId). 2. GROUP BY accounts.accountId, accounts.accountName, accounts.city, accounts.accountType. 3. SELECT accounts.accountName, accounts.city, accounts.accountType, ROUND(AVG(orderLines.unitPrice), 2) AS avgUnitPrice. 4. In HAVING: AVG(orderLines.unitPrice) > (SELECT AVG(orderLines.unitPrice) FROM craftbrewery.orderLines) — the scalar subquery computes the global average across ALL order lines. Note: use the unrounded AVG in HAVING for precision. 5. ORDER BY avgUnitPrice DESC. Final query as in solution_sql. Expected: 90 rows.
Full 15 points for correct three-table join, correct GROUP BY with accountId, scalar subquery in HAVING, ROUND on display, and ORDER BY. Deduct 4 points for hardcoding the global average as a literal number. Deduct 4 points for missing the HAVING altogether or using WHERE for the aggregate comparison. Deduct 2 points for missing ROUND. Deduct 1 point for missing accountId in GROUP BY. Award 8/15 for correct join chain and HAVING structure but missing scalar subquery (uses hardcoded or no global average).
A scalar subquery in HAVING is evaluated once per group (or optimized to run once total by most engines). How does this differ from a correlated subquery? Could this query be rewritten using a CTE or a join to a derived table for the global average? What might be the readability or performance trade-offs?