Capstone Project and PostgreSQL Interview Preparation
Design a practical schema, implement common queries, validate performance, and review core interview concepts.
Capstone: Design a Course Platform Database
Build a small course platform schema with users, courses, enrollments, lessons, and progress. Define primary and foreign keys, constraints, useful timestamps, and indexes around expected access patterns. Think in terms of entities and relationships first, then write queries that answer product questions such as most-enrolled courses or a user's course progress.
A useful capstone includes schema creation, seed data, read queries, updates inside transactions, a migration, and a performance review using EXPLAIN. The goal is not merely to make SQL run; it is to make the data model enforce the domain's important invariants.
CREATE TABLE courses (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
slug TEXT UNIQUE NOT NULL,
title TEXT NOT NULL,
published BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE enrollments (
user_id BIGINT NOT NULL REFERENCES users(id),
course_id BIGINT NOT NULL REFERENCES courses(id),
enrolled_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, course_id)
);
CREATE INDEX idx_enrollments_course
ON enrollments (course_id);users table already exists. The composite primary key prevents duplicate enrollments, while the course index can help course-centric lookups and counts.- Model entities and relationships before writing queries.
- Use constraints to encode important invariants.
- Index based on expected access patterns and verify with plans.
Which key prevents the same user from enrolling in the same course more than once?
Capstone: Reporting Queries
Write queries that aggregate enrollments, calculate course popularity, and show a user's progress. Ensure counts reflect the intended grain and that outer joins preserve courses with zero enrollments when needed. Add deterministic ordering and pagination for user-facing lists.
Test queries with edge cases: empty tables, missing relationships, duplicate-looking values, NULLs, and large result sets. A query that works for one sample row may still be logically wrong or slow at scale.
SELECT c.id, c.title, COUNT(e.user_id) AS enrollment_count
FROM courses AS c
LEFT JOIN enrollments AS e ON e.course_id = c.id
GROUP BY c.id, c.title
ORDER BY enrollment_count DESC, c.id
LIMIT 20;- Use LEFT JOIN when zero-related-row entities must remain in results.
- COUNT(column) excludes NULL, allowing zero counts for unmatched rows.
- Test empty and edge-case data, not only happy paths.
Why use COUNT(e.user_id) instead of COUNT(*) in this LEFT JOIN aggregation?
Capstone: Performance and Production Checklist
Measure the query with representative data and inspect its plan. Consider indexes on foreign keys and filters, but avoid adding indexes without evidence. Confirm that the application uses bound parameters, a bounded connection pool, transaction handling, and least-privilege roles. Ensure backups can be restored and schema migrations can be deployed safely.
A production-ready database is not just a schema. It also requires monitoring, access control, backup and recovery procedures, capacity planning, and a clear operational owner. Document assumptions so future developers can safely change the model.
EXPLAIN (ANALYZE, BUFFERS)
SELECT c.id, c.title, COUNT(e.user_id) AS enrollment_count
FROM courses AS c
LEFT JOIN enrollments AS e ON e.course_id = c.id
GROUP BY c.id, c.title
ORDER BY enrollment_count DESC, c.id
LIMIT 20;- Performance work should start with measurement.
- Production readiness includes security, backups, monitoring, and migrations.
- Document operational assumptions and recovery procedures.
What is the best first step when a query is slow?