This project is the culmination of everything you've learned. You'll build a database that mirrors real-world platforms like Medium, Ghost, or WordPress. By the end, you'll have a high-quality portfolio piece that demonstrates your ability to:
Model complex business relationships
Write clean, production-grade
Optimize queries for performance and scale
Project Overview
Our goal is to build the backend database for a professional blogging platform.
Requirements:
Users: Secure storage for authors and admins.
Posts: Articles with SEO-friendly slugs and publishing workflows.
Comments: Reader engagement with moderation states.
Categories & Tags: Two ways to organize content (One-to-Many and Many-to-Many).
Blog Architecture Flow
How a Blog Post is Createdflowchart
Rendering diagram…
Step 1: Design the Schema (ER Diagram)
A professional design starts with a clear map of how tables connect.
Blog Database Schema
Entity Relationship (Mermaid)
Blog Database ER DiagramerDiagram
Rendering diagram…
Step 2: Create the Database & Tables
Run these commands in your psql shell.
sql
-- 1. Create the DatabaseCREATE DATABASE blog_db;\c blog_db-- 2. Users TableCREATE TABLE users ( id SERIAL PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255
In a real app, these queries would power the Homepage and Admin Dashboard.
4.1 Latest Published Posts (Homepage Feed)
sql
SELECT posts.title, users.username AS author, categories.name AS category, posts.published_atFROM postsJOIN users ON posts.user_id = users.idJOIN categories ON posts.category_id = categories.idWHERE posts
4.2 Post Engagement (Comment Count)
sql
SELECT posts.title, COUNT(comments.id) AS total_commentsFROM postsLEFT JOIN comments ON posts.id = comments.post_idGROUP BY posts.id, posts.titleORDER BY total_comments DESC;
4.3 Author Statistics
sql
SELECT users.username, COUNT(posts.id) AS post_count, MAX(posts.published_at) AS latest_postFROM usersLEFT JOIN posts ON users.id = posts.user_id AND posts.published = trueGROUP BY users
4.4 Tag Search
sql
-- Find all posts with the tag 'sql'SELECT posts.titleFROM postsJOIN post_tags ON posts.id = post_tags.post_idJOIN tags ON post_tags.tag_id = tags.idWHERE tags.name = 'sql';
Step 5: Production Optimization (Indexes)
sql
-- Speed up looking up posts by their URL slugCREATE INDEX idx_posts_slug ON posts(slug);-- Speed up filtering by published dateCREATE INDEX idx_posts_published ON posts(published_at) WHERE published = true;-- Speed up comment lookups by postCREATE INDEX idx_comments_post_id ON comments(post_id);-- Speed up tag lookupsCREATE INDEX idx_post_tags_post_id ON
Final Challenge
To truly test your skills, try to write these queries on your own:
Draft List: Find all posts by 'Alice' that are NOT yet published
Admin Audit: Show all comments with the username of the commenter and the post title
Content Strategy: Which category has the most published posts?
Window Function: Rank posts by comment count within each category
CTE: Find authors whose posts have above-average comment counts
What You've Accomplished
Final Note
You have completed the SQL Fundamentals course! You now have the foundational skills to build the backend of almost any modern application. Keep practicing, keep building, and stay curious.
)
NOT NULL
,
role VARCHAR(20) DEFAULT 'author' CHECK (role IN ('author', 'admin')),
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 3. Categories Table
CREATE TABLE categories (
id SERIAL PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
slug VARCHAR(100) UNIQUE NOT NULL
);
-- 4. Posts Table
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR(300) NOT NULL,
slug VARCHAR(300) UNIQUE NOT NULL,
content TEXT NOT NULL,
user_id INTEGER NOT NULL REFERENCES users(id),
category_id INTEGER REFERENCES categories(id),
published BOOLEAN DEFAULT false,
published_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 5. Comments Table
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
content TEXT NOT NULL,
post_id INTEGER NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id),
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected')),
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 6. Tags & Junction Table (Many-to-Many)
CREATE TABLE tags (
id SERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL
);
CREATE TABLE post_tags (
post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE,
tag_id INTEGER REFERENCES tags(id) ON DELETE CASCADE,