This assignment assesses students' ability to query a multi-table relational database (bikeShop) using progressively complex SQL techniques. Questions 1–4 test foundational SELECT, WHERE, LIKE, and ORDER BY skills on single tables. Questions 5–7 introduce JOINs, GROUP BY, aggregate functions, and HAVING clauses. Question 8, worth 32 points, is the capstone: students must chain four tables, apply COUNT(DISTINCT ...) to avoid row inflation, filter with HAVING, and sort aggregated results. By the end, students should demonstrate comfort with filter logic, multi-table joins, aggregate correctness, and the WHERE-vs-HAVING distinction.
Partial credit is awarded when a student demonstrates understanding of the core concept but makes a recoverable error. In general: correct table selection and join logic earns 50–60% of points even if filters or aggregates are wrong. A correct filter/aggregate with a wrong join earns 40–50%. A structurally correct query with only a sort direction or alias error earns 85–90%. Queries that produce 0 rows due solely to a string literal case mismatch (e.g., 'green bay' vs 'Green Bay') should receive 70–80% credit if all other logic is sound, with a written note to the student. No credit is awarded for queries that return all rows from a table without any meaningful filtering or aggregation when the question requires it. For Question 8 specifically, use the point-breakdown rubric in its grading_tips to award granular partial credit across the five key components.
1. Identify the source table: bikeshop.customers contains firstName, lastName, email, and city. 2. Write SELECT customers.firstName, customers.lastName, customers.email to project only the requested columns. 3. Add FROM bikeshop.customers. 4. Add WHERE customers.city = 'Green Bay' to restrict rows to Green Bay residents — note the exact capitalization. 5. Add ORDER BY customers.lastName ASC to sort alphabetically by last name. Final query: SELECT customers.firstName, customers.lastName, customers.email FROM bikeshop.customers WHERE customers.city = 'Green Bay' ORDER BY customers.lastName ASC; Expected: 8 rows.
Award full 8 points for a correct result set (8 rows, 3 columns, correct order). Deduct 2 points if ORDER BY is missing or wrong direction. Deduct 2 points if extra columns appear in output. Deduct 1 point for cosmetic issues like missing schema prefix if the student's environment does not require it but the instructor's rubric does. Give 0 points if WHERE clause is missing entirely (wrong data returned).
Why might a production database store city names in a normalized lookup table (e.g., a cities table with a foreign key) rather than a plain varchar column, and how would that change this query?
1. Source table: bikeshop.bikes. 2. SELECT bikes.brand, bikes.model, bikes.bikeType, bikes.purchasePrice. 3. FROM bikeshop.bikes. 4. WHERE bikes.bikeType = 'Mountain' — exact capitalization required. 5. ORDER BY bikes.purchasePrice DESC for highest to lowest. Final query: SELECT bikes.brand, bikes.model, bikes.bikeType, bikes.purchasePrice FROM bikeshop.bikes WHERE bikes.bikeType = 'Mountain' ORDER BY bikes.purchasePrice DESC; Expected: 59 rows.
Full 8 points for correct 59-row result with DESC ordering. Deduct 2 points for wrong sort direction (ASC). Deduct 2 points for missing ORDER BY entirely. Deduct 1–2 points for extra or missing columns in SELECT. If 0 rows are returned due to case mismatch on 'Mountain', award partial credit (3–4 points) if the logic structure is otherwise correct.
If the database contained 'mountain', 'Mountain', and 'MOUNTAIN' as different bikeType values, how would you write a case-insensitive filter in standard SQL? Does this differ between MySQL, PostgreSQL, and SQL Server?
1. Source table: bikeshop.parts. 2. SELECT parts.partName, parts.partNumber, parts.category, parts.unitCost. 3. FROM bikeshop.parts. 4. WHERE parts.partName LIKE '%bundle%' — the wildcards on both sides ensure any partName containing the substring 'bundle' (anywhere) is matched. The question states case-insensitive is acceptable, so '%bundle%' is fine if the collation is case-insensitive; otherwise use LOWER(parts.partName) LIKE '%bundle%'. 5. ORDER BY parts.unitCost ASC. Final query: SELECT parts.partName, parts.partNumber, parts.category, parts.unitCost FROM bikeshop.parts WHERE parts.partName LIKE '%bundle%' ORDER BY parts.unitCost ASC; Expected: 15 rows.
Full 8 points for 15 correct rows in ASC order. Deduct 3 points for using LIKE 'bundle%' or LIKE '%bundle' (missing rows). Deduct 4 points for using = 'bundle' (almost certainly 0 rows). Deduct 2 points for wrong sort direction. Award 5–6 points if LIKE pattern is correct but ORDER BY is missing. If the student uses LOWER() or ILIKE for case insensitivity and gets correct rows, award full credit.
How does a full-text index differ from a LIKE '%bundle%' pattern match in terms of performance, and when would you recommend each approach in a production system?
1. Source table: bikeshop.employees. 2. SELECT employees.firstName, employees.lastName, employees.role, employees.hourlyRate. 3. FROM bikeshop.employees. 4. WHERE employees.role = 'Technician' — exact capitalization. 5. ORDER BY employees.hourlyRate DESC. Final query: SELECT employees.firstName, employees.lastName, employees.role, employees.hourlyRate FROM bikeshop.employees WHERE employees.role = 'Technician' ORDER BY employees.hourlyRate DESC; Expected: 114 rows.
Full 8 points for 114 correct rows, 4 columns, DESC sort. Deduct 2 points for ASC sort. Deduct 2 points for missing ORDER BY. Deduct 1–2 points for extra columns. Award 3–4 points if WHERE clause is correct but ORDER BY is entirely missing. Give 0 if WHERE is absent (all employees returned).
In a real HR system, what are the security implications of allowing all application users to query employee hourly rates, and how might you use SQL views or row-level security to restrict this?
1. Identify tables: repairOrders (for order info) and employees (for name). 2. Join condition: repairOrders.employeeId = employees.employeeId. 3. SELECT repairOrders.repairOrderId, repairOrders.dropOffDate, repairOrders.status, employees.firstName, employees.lastName. 4. FROM bikeshop.repairOrders JOIN bikeshop.employees ON repairOrders.employeeId = employees.employeeId. 5. WHERE repairOrders.status = 'In Progress'. 6. ORDER BY repairOrders.dropOffDate ASC. Final query: SELECT repairOrders.repairOrderId, repairOrders.dropOffDate, repairOrders.status, employees.firstName, employees.lastName FROM bikeshop.repairOrders JOIN bikeshop.employees ON repairOrders.employeeId = employees.employeeId WHERE repairOrders.status = 'In Progress' ORDER BY repairOrders.dropOffDate ASC; Expected: 250 rows.
Full 12 points for 250 correct rows with correct columns and ASC date order. Deduct 4 points if WHERE filter is missing (all statuses returned). Deduct 2 points for DESC instead of ASC ordering. Deduct 3 points for wrong JOIN key producing inflated or incorrect rows. Deduct 2 points for missing ORDER BY. Award 6–7 points if JOIN is correct but WHERE filter is absent. Award 8–9 points if everything is correct except sort direction.
What would happen to the result set if an employee was deleted from the employees table but their repairOrders records remained? How could you detect or prevent orphan records like this using database constraints?
1. Tables: stores (always show) LEFT JOIN repairOrders (may have 0 matches). 2. Join condition: stores.storeId = repairOrders.storeId. 3. SELECT stores.storeName, stores.city, COUNT(repairOrders.repairOrderId) AS totalRepairOrders. Using COUNT on the FK column means NULL values (stores with no orders) count as 0. 4. GROUP BY stores.storeId, stores.storeName, stores.city — include storeId to uniquely identify each store. 5. ORDER BY totalRepairOrders DESC. Final query: SELECT stores.storeName, stores.city, COUNT(repairOrders.repairOrderId) AS totalRepairOrders FROM bikeshop.stores LEFT JOIN bikeshop.repairOrders ON stores.storeId = repairOrders.storeId GROUP BY stores.storeId, stores.storeName, stores.city ORDER BY totalRepairOrders DESC; Expected: 12 rows.
Full 12 points for 12 rows with correct counts, including any zero-count stores, in DESC order. Deduct 4 points for using INNER JOIN (missing zero-order stores). Deduct 3 points for COUNT(*) if it inflates zero-count stores to 1. Deduct 2 points for missing GROUP BY (query likely errors). Deduct 2 points for wrong sort order. Award 7–8 points if JOIN type is wrong but aggregation and grouping are otherwise correct.
Why is it important to COUNT a column from the right (optional) side of a LEFT JOIN rather than using COUNT(*) when you want to count related records? Can you construct a small example that shows the difference in output?
1. Source table: bikeshop.bikes only — no join needed. 2. GROUP BY bikes.brand to create one group per brand. 3. SELECT bikes.brand, COUNT(bikes.bikeId) AS totalBikes, AVG(bikes.purchasePrice) AS avgPurchasePrice. 4. HAVING COUNT(bikes.bikeId) > 30 to keep only brands with more than 30 bikes — this filters groups, not individual rows. 5. ORDER BY avgPurchasePrice DESC. Final query: SELECT bikes.brand, COUNT(bikes.bikeId) AS totalBikes, AVG(bikes.purchasePrice) AS avgPurchasePrice FROM bikeshop.bikes GROUP BY bikes.brand HAVING COUNT(bikes.bikeId) > 30 ORDER BY avgPurchasePrice DESC; Expected: 3 rows.
Full 12 points for exactly 3 rows with correct brand names, counts, and averages in DESC avg price order. Deduct 5 points for using WHERE instead of HAVING (query may error or return wrong rows). Deduct 3 points for missing HAVING filter (returns all brands). Deduct 2 points for wrong sort direction. Deduct 2 points for missing GROUP BY. Award 6 points if GROUP BY and aggregates are correct but HAVING is missing. Award 8 points if HAVING threshold is correct but sort direction is wrong.
Explain 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 for a given filter?
1. Identify the join chain: customers → bikes (customers.customerId = bikes.customerId) → repairOrders (bikes.bikeId = repairOrders.bikeId) → repairItems (repairOrders.repairOrderId = repairItems.repairOrderId). 2. SELECT customers.firstName, customers.lastName, customers.city, COUNT(DISTINCT repairOrders.repairOrderId) AS totalRepairOrders, SUM(repairItems.lineTotal) AS totalRevenue. COUNT(DISTINCT ...) is required because each repair order may have multiple repairItems rows; without DISTINCT the count would be inflated. 3. FROM bikeshop.customers JOIN bikeshop.bikes ON customers.customerId = bikes.customerId JOIN bikeshop.repairOrders ON bikes.bikeId = repairOrders.bikeId JOIN bikeshop.repairItems ON repairOrders.repairOrderId = repairItems.repairOrderId. 4. GROUP BY customers.customerId, customers.firstName, customers.lastName, customers.city — include customerId to uniquely identify each customer even if names collide. 5. HAVING SUM(repairItems.lineTotal) > 500. 6. ORDER BY totalRevenue DESC. Final query: SELECT customers.firstName, customers.lastName, customers.city, COUNT(DISTINCT repairOrders.repairOrderId) AS totalRepairOrders, SUM(repairItems.lineTotal) AS totalRevenue FROM bikeshop.customers JOIN bikeshop.bikes ON customers.customerId = bikes.customerId JOIN bikeshop.repairOrders ON bikes.bikeId = repairOrders.bikeId JOIN bikeshop.repairItems ON repairOrders.repairOrderId = repairItems.repairOrderId GROUP BY customers.customerId, customers.firstName, customers.lastName, customers.city HAVING SUM(repairItems.lineTotal) > 500 ORDER BY totalRevenue DESC; Expected: 47 rows.
This question is worth 32 points — distribute partial credit carefully. Suggested breakdown: correct 4-table join chain (10 pts), correct GROUP BY with customerId (5 pts), COUNT(DISTINCT ...) for repair orders (5 pts), SUM(lineTotal) for revenue (5 pts), HAVING with correct threshold (5 pts), ORDER BY DESC (2 pts). Deduct 5 points for missing DISTINCT in COUNT (inflated order counts). Deduct 8 points for stopping at repairOrders and not reaching repairItems (wrong revenue column). Deduct 5 points for using WHERE instead of HAVING. Award 15–18 points for a query that has the right structure but wrong aggregate or missing DISTINCT. Award 20–22 points for a correct result set with a wrong but reasonable column alias.
Why does COUNT(repairOrders.repairOrderId) give a different result than COUNT(DISTINCT repairOrders.repairOrderId) in this context? Describe a real-world business scenario where failing to use DISTINCT would lead to a misleading report for management.