This assignment covers foundational to intermediate SQL querying skills using the Chinook music store database. Students are expected to demonstrate proficiency in SELECT with filtering and sorting (Q1–Q2), multi-table JOINs (Q3, Q6), LEFT JOIN with aggregation (Q4), HAVING-based group filtering (Q5), subquery-based anti-join patterns (Q7), and conditional aggregation with CASE WHEN (Q8). By the end, students should be able to construct queries that span multiple tables, apply aggregate functions correctly, filter at both row and group levels, and perform conditional classification of aggregated results. Watch for whether students understand the semantic difference between WHERE and HAVING, when to use LEFT vs INNER JOIN, and how NOT IN behaves with NULLs.
Partial credit should reflect demonstrated understanding of the relevant SQL concepts, not just whether the final output is correct. A query that uses the right technique (e.g., LEFT JOIN, HAVING, NOT IN) but has a minor syntactic or key-mapping error should receive 60–75% credit. A query that retrieves from the correct tables with correct columns but is missing a critical clause (WHERE, ORDER BY, HAVING) should receive 50–65% credit. A query that shows no understanding of the primary technique being tested (e.g., using WHERE instead of HAVING for Q5, using INNER JOIN instead of LEFT JOIN for Q4, or completely missing the subquery for Q7) should receive no more than 40% credit even if the remaining logic is correct. Queries that produce correct output through an approach not taught in the course (e.g., procedural workarounds) should receive credit for output correctness but the instructor should note the preferred approach. Always award 0 points for plagiarized queries, but consult institutional policy before assigning academic integrity violations.
Step 1: Identify the source table — only chinook.Customer is needed. Step 2: Select the three required columns: FirstName, LastName, Email. Step 3: Add a WHERE clause filtering Country = 'USA' (exact string match). Step 4: Add ORDER BY LastName ASC, FirstName ASC to satisfy the sort requirement. Final query: SELECT Customer.FirstName, Customer.LastName, Customer.Email FROM chinook.Customer WHERE Customer.Country = 'USA' ORDER BY Customer.LastName ASC, Customer.FirstName ASC;
Award full 5 points for a correct query producing the right columns, correct filter, and correct sort order. Deduct 1 point for wrong sort order (e.g., FirstName before LastName). Deduct 1–2 points for the wrong country string if the student can articulate they understood the concept. Deduct 1 point for including extra columns beyond what was asked. Award 3/5 if the correct table and filter logic are present but the ORDER BY is entirely missing. Award 1–2/5 if only a basic SELECT with no WHERE is present.
Why might storing country names as free-text strings (like 'USA') rather than ISO country codes cause problems in real-world databases? How would a database designer mitigate this?
Step 1: Source table is chinook.Track. Step 2: Select Name and UnitPrice only. Step 3: WHERE UnitPrice > 0.99 — strictly greater than, not greater-than-or-equal. Step 4: ORDER BY UnitPrice DESC, Name ASC — price highest first, then alphabetically within same price tier. Final query: SELECT Track.Name, Track.UnitPrice FROM chinook.Track WHERE Track.UnitPrice > 0.99 ORDER BY Track.UnitPrice DESC, Track.Name ASC;
Deduct 2 points for using >= instead of > (this is a semantic error, not just stylistic). Deduct 1 point for missing the secondary sort by Name. Deduct 1 point for wrong sort direction on either column. Award 4/5 if the filter and primary sort are correct but secondary sort is missing. Award 2/5 if the student retrieves from the right table with the right columns but the filter logic is entirely wrong.
In a real e-commerce database, why might you avoid storing prices as plain NUMERIC columns and instead use a separate pricing table with effective dates? What business scenarios make this important?
Step 1: Start from chinook.Track as the central table. Step 2: JOIN chinook.Genre ON Track.GenreId = Genre.GenreId — this brings in Genre.Name. Step 3: JOIN chinook.MediaType ON Track.MediaTypeId = MediaType.MediaTypeId — this brings in MediaType.Name. Step 4: SELECT Track.Name, Genre.Name, MediaType.Name — qualify all three to avoid ambiguity. Step 5: ORDER BY Genre.Name ASC, Track.Name ASC. Recommended to alias Genre.Name AS GenreName and MediaType.Name AS MediaTypeName for clarity. Final query: SELECT Track.Name, Genre.Name, MediaType.Name FROM chinook.Track JOIN chinook.Genre ON Track.GenreId = Genre.GenreId JOIN chinook.MediaType ON Track.MediaTypeId = MediaType.MediaTypeId ORDER BY Genre.Name ASC, Track.Name ASC;
Award full 10 points for correct joins on correct keys, correct columns selected, and correct sort order. Deduct 3 points for missing one of the two JOINs entirely. Deduct 2 points for joining on incorrect keys (e.g., GenreId = MediaTypeId). Deduct 1 point for missing or incorrect sort order. Deduct 1 point for unqualified column names that would cause ambiguity errors. Award partial credit of 5/10 if the student correctly joins two of the three tables and demonstrates understanding of the JOIN concept.
This query produces a Cartesian-product-style concern if joins are done incorrectly. How does specifying the correct ON condition prevent a cross join? What would happen to the row count if you forgot the ON clause entirely?
Step 1: Start from chinook.Artist — this is the LEFT side because we want all artists. Step 2: LEFT JOIN chinook.Album ON Artist.ArtistId = Album.ArtistId — for artists with no albums, Album columns will be NULL. Step 3: GROUP BY Artist.ArtistId, Artist.Name — group per artist. Step 4: COUNT(Album.AlbumId) — counting a nullable column from the right side of a LEFT JOIN correctly returns 0 when there are no matching albums. Step 5: Alias the count as AlbumCount. Step 6: ORDER BY AlbumCount DESC, Artist.Name ASC. Final query: SELECT Artist.Name, COUNT(Album.AlbumId) AS AlbumCount FROM chinook.Artist LEFT JOIN chinook.Album ON Artist.ArtistId = Album.ArtistId GROUP BY Artist.ArtistId, Artist.Name ORDER BY AlbumCount DESC, Artist.Name ASC;
The LEFT JOIN vs INNER JOIN distinction is the core learning objective here — deduct 4 points if INNER JOIN is used (the result is logically wrong). Deduct 2 points for COUNT(*) instead of COUNT(Album.AlbumId) (partial conceptual understanding). Deduct 1 point for missing secondary sort. Award 6/10 if the student uses LEFT JOIN correctly but counts incorrectly. Award 4/10 if the student uses INNER JOIN but otherwise has correct GROUP BY and COUNT logic.
Why does COUNT(column) behave differently from COUNT(*) when used with a LEFT JOIN? Can you think of a scenario where COUNT(*) would actually give you the wrong answer compared to COUNT(column)?
Step 1: Source tables are chinook.Genre and chinook.Track. Step 2: JOIN chinook.Track ON Genre.GenreId = Track.GenreId — INNER JOIN is appropriate since we only care about genres that have tracks. Step 3: GROUP BY Genre.GenreId, Genre.Name — one row per genre. Step 4: HAVING COUNT(Track.TrackId) > 50 — filter groups where track count exceeds 50. Step 5: SELECT Genre.Name, COUNT(Track.TrackId) AS TrackCount. Step 6: ORDER BY TrackCount DESC. Final query: SELECT Genre.Name, COUNT(Track.TrackId) AS TrackCount FROM chinook.Genre JOIN chinook.Track ON Genre.GenreId = Track.GenreId GROUP BY Genre.GenreId, Genre.Name HAVING COUNT(Track.TrackId) > 50 ORDER BY TrackCount DESC;
The WHERE vs HAVING distinction is the primary learning objective — if a student uses WHERE COUNT(...) > 50, that is a syntactic/conceptual error; deduct 5 points but award credit for correct GROUP BY and JOIN. Award 10/15 if HAVING is used correctly but ORDER BY is missing. Award 7/15 if the student demonstrates correct grouping and aggregation but applies the filter incorrectly. Award full credit if a student uses a subquery to filter (e.g., wrapping the grouped query) even if HAVING was not used directly — it demonstrates equivalent understanding.
What is the logical order of SQL clause execution (FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY)? Why does understanding this order matter when deciding whether to use WHERE or HAVING?
Step 1: Start from chinook.Employee. Step 2: JOIN chinook.Customer ON Employee.EmployeeId = Customer.SupportRepId — connects each employee to their supported customers. Step 3: JOIN chinook.Invoice ON Customer.CustomerId = Invoice.CustomerId — connects customers to their invoices. Step 4: WHERE Employee.Title = 'Sales Support Agent' — pre-filter to relevant employees before aggregation. Step 5: GROUP BY Employee.EmployeeId, Employee.FirstName, Employee.LastName, Employee.Title — one row per employee. Step 6: SUM(Invoice.Total) AS TotalRevenue — sum all invoice totals for that employee's customers. Step 7: ORDER BY TotalRevenue DESC. Final query: SELECT Employee.FirstName, Employee.LastName, Employee.Title, SUM(Invoice.Total) AS TotalRevenue FROM chinook.Employee JOIN chinook.Customer ON Employee.EmployeeId = Customer.SupportRepId JOIN chinook.Invoice ON Customer.CustomerId = Invoice.CustomerId WHERE Employee.Title = 'Sales Support Agent' GROUP BY Employee.EmployeeId, Employee.FirstName, Employee.LastName, Employee.Title ORDER BY TotalRevenue DESC;
Award full 15 points for correct three-table join path, correct filter, correct aggregation, and correct sort. Deduct 4 points for a missing JOIN (e.g., Employee joined directly to Invoice). Deduct 3 points for COUNT instead of SUM for revenue. Deduct 2 points for missing the WHERE Title filter. Deduct 1 point for missing ORDER BY or wrong direction. Deduct 1 point for incomplete GROUP BY. Award 8/15 for a query that demonstrates correct aggregation and grouping logic but has the wrong join path.
In a real business scenario, why might attributing revenue to a support representative be a useful metric? What are the limitations of this approach — for example, what if a customer's support rep changed over time?
Step 1: The goal is to find Customer rows with no corresponding Invoice rows. Step 2 (NOT IN approach): Write a subquery SELECT Invoice.CustomerId FROM chinook.Invoice to get all customer IDs that appear in the Invoice table. Step 3: In the outer query, WHERE Customer.CustomerId NOT IN (subquery) — this filters to customers whose ID is not in that set. Step 4: SELECT FirstName, LastName, Email. Step 5: ORDER BY LastName ASC, FirstName ASC. Alternative (LEFT JOIN): SELECT Customer.FirstName, Customer.LastName, Customer.Email FROM chinook.Customer LEFT JOIN chinook.Invoice ON Customer.CustomerId = Invoice.CustomerId WHERE Invoice.CustomerId IS NULL ORDER BY Customer.LastName ASC, Customer.FirstName ASC. Both approaches are fully correct. Note: In the Chinook sample dataset, all customers have invoices, so the expected result is an empty set — this is correct behavior.
Award full 20 points for either NOT IN with subquery, NOT EXISTS with correlated subquery, or LEFT JOIN + IS NULL anti-join pattern. Deduct 5 points if the student uses NOT IN but puts the wrong column in the subquery (e.g., InvoiceId instead of CustomerId). Deduct 5 points if a student uses an INNER JOIN and tries to filter — they likely have the logic inverted. Award 14/20 if the anti-join concept is correct but the student misidentifies the joining key. Award 5/20 if the student demonstrates awareness of the anti-join concept but cannot execute it correctly. Note on empty result: do not penalize students for returning zero rows if their query is logically correct.
What are the performance implications of NOT IN vs NOT EXISTS vs LEFT JOIN IS NULL for anti-join patterns? In particular, what dangerous behavior does NOT IN exhibit when the subquery contains NULL values, and how does NOT EXISTS avoid this?
Step 1: Only chinook.Invoice is needed. Step 2: GROUP BY Invoice.BillingCountry — one row per country. Step 3: SELECT BillingCountry, COUNT(Invoice.InvoiceId) AS InvoiceCount, ROUND(SUM(Invoice.Total), 2) AS TotalRevenue. Step 4: Write CASE WHEN expression operating on SUM(Invoice.Total) — the aggregate, not individual rows: CASE WHEN SUM(Invoice.Total) > 100 THEN 'High' WHEN SUM(Invoice.Total) >= 50 THEN 'Medium' ELSE 'Low' END AS RevenueTier. Note: because CASE evaluates top-to-bottom and stops at first match, the second condition WHEN SUM >= 50 implicitly means 50 <= SUM <= 100. Step 5: ORDER BY TotalRevenue DESC. Final query: SELECT Invoice.BillingCountry, COUNT(Invoice.InvoiceId) AS InvoiceCount, ROUND(SUM(Invoice.Total), 2) AS TotalRevenue, CASE WHEN SUM(Invoice.Total) > 100 THEN 'High' WHEN SUM(Invoice.Total) >= 50 THEN 'Medium' ELSE 'Low' END AS RevenueTier FROM chinook.Invoice GROUP BY Invoice.BillingCountry ORDER BY TotalRevenue DESC;
Award full 20 points for correct GROUP BY, correct COUNT and SUM aggregation, correct ROUND, correct CASE WHEN with proper boundary conditions, and correct ORDER BY. Deduct 5 points for CASE WHEN applied to non-aggregated Invoice.Total instead of SUM(Invoice.Total). Deduct 3 points for incorrect boundary conditions in CASE WHEN (e.g., both thresholds use > instead of the correct mix of > and >=). Deduct 2 points for missing ROUND. Deduct 2 points for missing ORDER BY. Award 12/20 if GROUP BY and aggregation are correct but CASE WHEN is missing entirely. Award 8/20 if the student correctly groups and counts but applies CASE WHEN at the wrong level.
CASE WHEN evaluates conditions in order and stops at the first match — this is called 'short-circuit evaluation.' How does this behavior affect how you write your conditions? Would the query produce different results if you swapped the order of the WHEN clauses (putting the >= 50 condition first)?