Learn with Abdou

LAB 11 / PREPARATION & TEAM PRACTICE

Harbor Snow: build a reporting layer

Lab 11 preparation with eight readings and six team investigations. Run SQL in modern desktop SQLite; keep predictions and evidence here.

Get the right database and tool

Extract the kit and make a working copy of harbor_snow.db. Open that copy in your desktop SQLite tool, check SELECT sqlite_version(); and enable PRAGMA foreign_keys=ON; in the connection. Window functions require SQLite 3.25 or newer. Views are installed; read setup.sql and views.sql to understand them.

The browser playground uses an older SQLite engine. Run these SQL examples on your computer, then record predictions, outputs and explanations below. The kit README explains how to rebuild a fresh practice database. Topic 8 commits a change; use a fresh copy for each investigation.

Quick schema: tables, columns, relationships & views

Harbor Snow schema

Original practice data. Row counts are for a fresh kit copy. The official class database is Spirit Mountain, downloaded from Canvas.

guests · 6 rows

id INTEGER PK; name TEXT NOT NULL; city TEXT NOT NULL; balance INTEGER NOT NULL CHECK >= 0

One registered guest. Balance is prepaid whole-dollar credit.

sales · 8 rows

id INTEGER PK; guest_id INTEGER nullable FK → guests.id; sale_date TEXT NOT NULL; amount INTEGER NOT NULL CHECK >= 0

One admission purchase. NULL guest_id is anonymous. Two identical-looking purchases remain two sales. Dates use YYYY-MM-DD.

coaches · 3 rows

id INTEGER PK; name TEXT NOT NULL

One coach, including a coach with no lessons.

lessons · 4 rows

id INTEGER PK; guest_id INTEGER NOT NULL FK → guests.id; coach_id INTEGER NOT NULL FK → coaches.id; lesson_date TEXT NOT NULL; price INTEGER NOT NULL CHECK >= 0

One lesson. These fees are separate from the admission sales report.

Saved views

guest_spend (id, name, city, purchases, total)
6 rows: one per guest, based on guests LEFT JOIN sales.

monthly_sales (month, total)
4 rows: November, December, January and March. Admissions only; no February row.

coach_summary (id, name, lessons, revenue)
3 rows: one per coach, including zero lessons.

Build order: guests + coaches → sales + lessons → views → report queries. Foreign keys must be enabled in each desktop connection. SQL definitions are in setup.sql and views.sql.

EIGHT CONNECTED TOPICS

Read, predict, then run

TOPIC 1 OF 8

Save a definition, keep the report live

A view names a SELECT stored in the database. A CTE names a result for one statement. A table made with CREATE TABLE AS stores a snapshot. Choose by how long the definition should live and whether the results must reflect changes.

Read views.sql in the kit: guest_spend and monthly_sales depend on sales; coach_summary depends on lessons and coaches. Build base tables before views, and views before reports. A view does not freeze its input or guarantee display order.

Run this rehearsal in one desktop connection. The view reflects the inserted lesson inside the transaction; ROLLBACK restores its previous result. DROP VIEW removes a definition, not its base rows. These whole-dollar practice values simplify arithmetic; they are not a general money-storage design.

Worked SQL · run this topic in desktop SQLite
SELECT * FROM coach_summary WHERE id=3;
BEGIN;
INSERT INTO lessons VALUES (5,1,3,'2026-03-10',60);
SELECT * FROM coach_summary WHERE id=3;
ROLLBACK;
SELECT * FROM coach_summary WHERE id=3;

Pause and investigate: Predict all three view outputs before running. What would a previously exported CSV show instead?

TOPIC 2 OF 8

Keep quiet guests and anonymous sales

Write the grain first: guest_spend is one row per registered guest, monthly_sales is one row per observed sale month, and sales is one row per admission purchase. A guest with no purchases still belongs in a guest report.

Start from guests and LEFT JOIN sales. COUNT(s.id) counts matched purchases; COUNT(*) counts the placeholder row too. COALESCE changes the missing aggregate to zero. Filtering the right side in WHERE can remove the guest you intended to preserve.

Anonymous admissions have no guest owner but still earn revenue. Guest totals therefore need not equal all sales. Lessons are a separate service in this small kit and are not included in monthly_sales; the official lab combines four revenue sources with UNION ALL.

Worked SQL · run this topic in desktop SQLite
SELECT * FROM guest_spend ORDER BY id;
SELECT SUM(total) AS registered_spend FROM guest_spend;
SELECT SUM(amount) AS all_admission_sales FROM sales;

Pause and investigate: Explain any difference using an identifiable source row. Would replacing UNION ALL with UNION preserve two separate identical purchases?

TOPIC 3 OF 8

Choose what a rank means

ROW_NUMBER gives each row a unique position. RANK gives peers a shared position and leaves gaps; DENSE_RANK counts distinct ordered levels. Peer equality uses every expression in the window ORDER BY.

To preserve equal-spend ties, rank by total alone. To pick exactly N names reproducibly, use ROW_NUMBER ordered by total descending and then the unique id. Adding id to RANK breaks the spend ties instead of merely stabilizing their display.

The outer ORDER BY controls presentation. The ORDER BY inside OVER controls the calculation. PARTITION BY restarts that calculation for each city without collapsing the guests.

Worked SQL · run this topic in desktop SQLite
SELECT id,name,city,total,
 RANK() OVER (PARTITION BY city ORDER BY total DESC) AS rnk,
 DENSE_RANK() OVER (PARTITION BY city ORDER BY total DESC) AS dense,
 ROW_NUMBER() OVER (PARTITION BY city ORDER BY total DESC,id) AS position
FROM guest_spend ORDER BY city,total DESC,id;

Pause and investigate: In Harbor, predict the rank assigned to Bea and Cam. Which ordering belongs to the business rule and which makes a chosen list reproducible?

TOPIC 4 OF 8

Calculate first, filter second

Window functions operate after WHERE filters the input. A window expression cannot be placed directly in WHERE. Compute the position in a CTE or subquery, then filter that named column outside.

A request for exactly two people per city is different from every person tied in the top two rank positions. ROW_NUMBER can return at most two per city, and exactly two only when a city contains at least two guests.

Worked SQL · run this topic in desktop SQLite
WITH ordered AS (
 SELECT id,name,city,total,
 ROW_NUMBER() OVER (PARTITION BY city ORDER BY total DESC,id) AS position
 FROM guest_spend
)
SELECT city,name,total,position FROM ordered
WHERE position<=2 ORDER BY city,position;

Pause and investigate: Count results per city, not just the overall result count. Would changing ROW_NUMBER to RANK meet the same request?

TOPIC 5 OF 8

Make the running-total frame explicit

SUM(amount) OVER () repeats the grand total. An ordered cumulative window can instead show progress. On one row per unique month, the usual ordered sum is straightforward.

At transaction grain, dates can tie. The default ordered frame includes peers with the same ordering value. For one addition per sale, use sale_date plus the unique id and an explicit ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW frame.

An outer sort alone cannot repair an unordered window sum. Check the intermediate totals as well as the final total; several incorrect queries share the same last number.

Worked SQL · run this topic in desktop SQLite
SELECT id,sale_date,amount,
 SUM(amount) OVER (
 ORDER BY sale_date,id
 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
 ) AS running_total
FROM sales ORDER BY sale_date,id;

Pause and investigate: Compare with SUM(amount) OVER (ORDER BY sale_date). At which pairs of sales does the default frame jump?

TOPIC 6 OF 8

Previous row is not always previous month

LAG retrieves the previous row in the window order. The first row has no predecessor and normally receives NULL. Do not silently turn that unknown comparison into a change from zero.

The rehearsal has no February admissions. Its monthly view omits February, so March compares with January. Name that output previous observed month unless you explicitly build a complete calendar and establish what a missing month means.

If source completeness confirms zero sales in February, a calendar scaffold and LEFT JOIN can supply a zero month before LAG. Missing data is not automatically zero. LEAD looks in the other direction.

Worked SQL · run this topic in desktop SQLite
WITH compared AS (
 SELECT month,total,
 LAG(month) OVER (ORDER BY month) AS previous_observed_month,
 LAG(total) OVER (ORDER BY month) AS previous_total
 FROM monthly_sales
)
SELECT *,total-previous_total AS change
FROM compared ORDER BY month;

Pause and investigate: For March, identify the two actual months being compared. Why could an accurate numeric subtraction still have a misleading label?

TOPIC 7 OF 8

Compare each lesson with its coach

GROUP BY coach_id reduces lessons to coach summaries. AVG(price) OVER (PARTITION BY coach_id) retains each lesson while adding its coach-wide average.

Leave ORDER BY out of this OVER when the benchmark should cover all that coach’s lessons. Adding ORDER BY lesson_date without a full-partition frame turns it into an accumulating benchmark.

Compute an alias in a CTE when another expression needs it. In this fixture, prices are whole dollars but averages can be fractional; ROUND the displayed comparison rather than changing the source values.

Worked SQL · run this topic in desktop SQLite
WITH compared AS (
 SELECT id,coach_id,price,
 AVG(price) OVER (PARTITION BY coach_id) AS coach_avg
 FROM lessons
)
SELECT id,coach_id,price,ROUND(coach_avg,2) AS coach_avg,
 ROUND(price-coach_avg,2) AS difference
FROM compared ORDER BY coach_id,id;

Pause and investigate: Predict how many rows remain. Does the coach with no lessons belong in this lesson-level result or in coach_summary?

TOPIC 8 OF 8

Test the whole unit of work

A lesson booking and its balance debit should succeed together. In one connection, BEGIN, perform both writes, verify affected rows and balances, then COMMIT when correct or ROLLBACK when not. A zero-row UPDATE can succeed syntactically while failing the intended task.

For the default CHECK failure used in this kit, SQLite aborts the failing statement and leaves the explicit transaction open. Earlier successful statements are still pending. Inspect the error and transaction state; do not assume every error behaves identically.

Run this successful booking rehearsal on a copy. Verify the lesson, balance and live view before COMMIT, then query again afterward. Reopen a fresh copy for the team challenges. For the deliberately failing transfer, enter each statement separately so your SQL tool does not skip the observation or rollback.

Worked SQL · run this topic in desktop SQLite
PRAGMA foreign_keys=ON;
BEGIN;
INSERT INTO lessons VALUES (5,1,3,'2026-03-10',60);
UPDATE guests SET balance=balance-60 WHERE id=1;
SELECT changes() AS guests_debited;
SELECT id,balance FROM guests WHERE id=1;
SELECT * FROM coach_summary WHERE id=3;
COMMIT;
SELECT id,balance FROM guests WHERE id=1;
SELECT * FROM coach_summary WHERE id=3;

Pause and investigate: Why does changes() need to be checked immediately after the UPDATE? What evidence would tell you to roll back rather than commit?

PREDICT · RUN ON DESKTOP · EXPLAIN

Six team investigations

Try questions 1–3 in 15–20 minutes, or all six in 35–45 minutes. Rotate writer, evidence checker and explainer. Start each question with a fresh rehearsal copy. These practice numbers are separate from official Lab 11 Q1–Q15.

Opening your saved work…

Sign in to save across devices → · Download your guest work before switching accounts.

1. The welcome report loses a guest

The manager needs every registered guest, including those with no purchases. Repair this query to return id, name, purchases and total ordered by id. Identify the missing guest and explain why guest totals do not account for all admission revenue.

SELECT g.id,g.name,COUNT(*) AS purchases,SUM(s.amount) AS total
FROM guests g JOIN sales s ON s.guest_id=g.id
GROUP BY g.id,g.name ORDER BY g.id;

2. The two-name invitation has three names

Choose exactly two guests per city for a feedback invitation. Repair the selection, retaining the output columns city, name, total and position. Count each city’s results and explain whom your tie-breaker selects. Give a different business request for which the original query would be appropriate.

WITH ranked AS (
 SELECT id,name,city,total,
 RANK() OVER (PARTITION BY city ORDER BY total DESC) AS position
 FROM guest_spend
)
SELECT city,name,total,position FROM ranked
WHERE position<=2 ORDER BY city,position,id;

3. The till total jumps before the next sale

The clerk wants one cumulative addition per admission sale. Return id, sale_date, amount and running_total in sale_date/id order. Repair the window, identify two intermediate numbers that change, and explain why the final total alone cannot validate it.

SELECT id,sale_date,amount,
 SUM(amount) OVER (ORDER BY sale_date) AS running_total
FROM sales ORDER BY sale_date,id;

4. The March headline names the wrong month

Run the query. For March, state which month LAG actually reads. Return month, total, previous_observed_month, previous_total and change with honest labels. Outline how you would compare consecutive calendar months if February was confirmed to have zero sales. Explain why filling unknown months with zero is risky.

SELECT month,total,LAG(total) OVER (ORDER BY month) AS previous_month
FROM monthly_sales ORDER BY month;

5. The benchmark changes while the coach teaches

Return each lesson id, coach_id, price and coach_avg, ordered by coach_id and id. The benchmark should be that coach’s average over every lesson in the fixture. Repair the query and name a row that exposes the error. Explain why GROUP BY alone cannot return the required grain.

SELECT id,coach_id,price,
 ROUND(AVG(price) OVER (PARTITION BY coach_id ORDER BY lesson_date,id),2) AS coach_avg
FROM lessons ORDER BY coach_id,id;

6. The error does not undo the first write

Use a fresh rehearsal copy and one SQLite connection. First record balances for ids 2 and 3. Enter the statements below one at a time; the second UPDATE should fail. Before closing the connection, record both balances and the exact error. Run ROLLBACK, record balances again, and explain what would happen without BEGIN. Do not replace ROLLBACK with COMMIT.

SELECT id,balance FROM guests WHERE id IN (2,3) ORDER BY id;
BEGIN;
UPDATE guests SET balance=balance+70 WHERE id=3;
UPDATE guests SET balance=balance-70 WHERE id=2;
-- STOP: inspect the error and both balances here.
-- Then ROLLBACK and inspect both balances again.

Notes save on this device and appear in My Work. This page does not execute or grade desktop SQL, upload your database, or submit to Canvas.

Return to your actual Lab 11

Download spirit_mountain.db from Canvas and work on a copy. Follow the handout’s exact aliases and Q1–Q15 numbering: build the view layer, apply rankings and trends, then record the before/inside/after evidence for transactions. Preserve SQL as text, results, findings, the Check the AI comparison, your client query and your own reflections in the individual report.

Milestone 3 is checked during this lab: have your team’s empty-database build and load evidence ready. After the lab, assign owners for the client reporting views and portfolio queries for Milestone 4. Your client files stay in your own project folder. This rehearsal kit does not replace those files or the assigned Spirit Mountain questions.

Official template & submission →

SQL references: views · window functions · transactions. Examples use an original fixture; Canvas controls class instructions and deadlines.