SQL module
Demo
First SQL shift
Answer retail analytics questions on the seeded Postgres lab — joins, CTEs, windows — and export results.
50 minLocal lab
Do this after the window and habits lessons (through lesson 15).
You are the analyst on call for a tiny retail warehouse. Postgres is local; the website runs nothing. Work in ~/dcubes/sql-lab/shift (or reuse ~/dcubes/sql-lab if still seeded).
What you need
- Seeded DB from SQL Getting Started (5 stores, 12 products, 300 orders)
- Docker running;
psqlon your PATH
1. Confirm the lab
mkdir -p ~/dcubes/sql-lab/out
cd ~/dcubes/sql-lab
export PGPASSWORD=dcubes PGHOST=127.0.0.1 PGPORT=15432 PGUSER=dcubes PGDATABASE=retail
psql -c "SELECT count(*) FROM orders;"
count
-------
300
If empty, re-run Getting Started load steps.
2. Revenue by category (completed only)
psql -c "
SELECT p.category,
count(*) AS orders,
round(sum(o.line_total), 2) AS revenue
FROM orders o
JOIN products p ON o.sku = p.sku
WHERE o.status = 'completed'
GROUP BY p.category
ORDER BY revenue DESC;
" | tee out/q1_category_revenue.txt
You should see four categories with revenue ordered high to low.
3. Top stores by completed order count
psql -c "
SELECT s.store_id, s.store_name, s.region, count(*) AS completed_orders
FROM orders o
JOIN stores s ON o.store_id = s.store_id
WHERE o.status = 'completed'
GROUP BY s.store_id, s.store_name, s.region
ORDER BY completed_orders DESC, s.store_id;
" | tee out/q2_stores.txt
4. Repeat customers (2+ orders any status)
psql -c "
SELECT customer_id, count(*) AS orders
FROM orders
GROUP BY customer_id
HAVING count(*) >= 2
ORDER BY orders DESC, customer_id
LIMIT 15;
" | tee out/q3_repeat_customers.txt
5. CTE pipeline — completed lines with region, then bucket
psql -c "
WITH completed AS (
SELECT o.*, s.region
FROM orders o
JOIN stores s ON o.store_id = s.store_id
WHERE o.status = 'completed'
),
bucketed AS (
SELECT region,
CASE
WHEN line_total < 5 THEN 'small'
WHEN line_total < 15 THEN 'medium'
ELSE 'large'
END AS size_bucket,
line_total
FROM completed
)
SELECT region, size_bucket, count(*) AS n, round(sum(line_total), 2) AS revenue
FROM bucketed
GROUP BY region, size_bucket
ORDER BY region, size_bucket;
" | tee out/q4_region_buckets.txt
6. Window — top 3 completed lines per store
psql -c "
WITH ranked AS (
SELECT
o.store_id,
o.order_id,
o.line_total,
row_number() OVER (PARTITION BY o.store_id ORDER BY o.line_total DESC) AS rn
FROM orders o
WHERE o.status = 'completed'
)
SELECT store_id, order_id, line_total, rn
FROM ranked
WHERE rn <= 3
ORDER BY store_id, rn;
" | tee out/q5_top3_per_store.txt
7. Day-over-day with LAG
psql -c "
WITH daily AS (
SELECT order_ts::date AS d, round(sum(line_total), 2) AS revenue
FROM orders
WHERE status = 'completed'
GROUP BY 1
)
SELECT d, revenue,
lag(revenue) OVER (ORDER BY d) AS prev_revenue,
round(revenue - lag(revenue) OVER (ORDER BY d), 2) AS change
FROM daily
ORDER BY d DESC
LIMIT 10;
" | tee out/q6_dod.txt
8. Export CSV for one answer
psql -c "\copy (
SELECT p.category, round(sum(o.line_total), 2) AS revenue
FROM orders o
JOIN products p ON o.sku = p.sku
WHERE o.status = 'completed'
GROUP BY p.category
ORDER BY revenue DESC
) TO 'out/category_revenue.csv' CSV HEADER"
cat out/category_revenue.csv
You passed if
-
orderscount is 300 - Six
out/q*.txtfiles exist with sensible tables -
out/category_revenue.csvhas a header and four category rows - Top-3-per-store query returns at most 3 rows per
store_id
Common mistakes
- Forgetting
status = 'completed'on revenue questions - Filtering window
rnwithout a CTE - Writing exports to a path Postgres server cannot see —
\copyto a path under your lab is correct
Next
Next on the roadmap: Git — version the SQL you just wrote. Still guidance only, still on your machine.