This mini project asks you to design a schema for one of three domains: a blog, a small shop, or a booking system. Pick one that interests you. The goal is a normalised, constrained schema you could hand to an application developer.
Pick a domain
Blog – authors, posts, comments, tags, optional categories.
Shop – customers, products, categories, orders, line items.
Booking – customers, services or rooms, time slots, appointments, optional staff.
Do not build all three unless you want the practice. Depth on one beats shallow coverage of three.
Requirements (all domains)
- At least four tables with clear relationships
- Primary keys and foreign keys with sensible ON DELETE behaviour
- At least one UNIQUE constraint (email, slug, or slot uniqueness)
- At least one CHECK or ENUM for status/rating/range
- Indexes on columns you will filter or join on
created_at(andupdated_atwhere updates happen) on main entities
Example starter: shop
Extend the shop model from earlier tutorials or start fresh:
CREATE DATABASE mini_shop;
USE mini_shop;
CREATE TABLE categories (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE,
slug VARCHAR(100) NOT NULL UNIQUE
);
CREATE TABLE products (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
category_id INT UNSIGNED NULL,
name VARCHAR(200) NOT NULL,
slug VARCHAR(200) NOT NULL UNIQUE,
price DECIMAL(10, 2) NOT NULL,
stock INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_products_category
FOREIGN KEY (category_id) REFERENCES categories(id)
ON DELETE SET NULL,
CONSTRAINT chk_price CHECK (price >= 0),
CONSTRAINT chk_stock CHECK (stock >= 0)
);
CREATE TABLE customers (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE orders (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
customer_id INT UNSIGNED NOT NULL,
status ENUM('pending','paid','shipped','cancelled') NOT NULL DEFAULT 'pending',
placed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_orders_customer
FOREIGN KEY (customer_id) REFERENCES customers(id)
ON DELETE RESTRICT
);
CREATE INDEX idx_orders_customer ON orders (customer_id);
CREATE INDEX idx_orders_placed_at ON orders (placed_at);
CREATE TABLE order_items (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
order_id INT UNSIGNED NOT NULL,
product_id INT UNSIGNED NOT NULL,
quantity INT UNSIGNED NOT NULL,
unit_price DECIMAL(10, 2) NOT NULL,
CONSTRAINT fk_items_order
FOREIGN KEY (order_id) REFERENCES orders(id)
ON DELETE CASCADE,
CONSTRAINT fk_items_product
FOREIGN KEY (product_id) REFERENCES products(id)
ON DELETE RESTRICT,
CONSTRAINT chk_qty CHECK (quantity > 0)
);
Example starter: blog
CREATE TABLE authors (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE
);
CREATE TABLE posts (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
author_id INT UNSIGNED NOT NULL,
title VARCHAR(200) NOT NULL,
slug VARCHAR(200) NOT NULL UNIQUE,
body TEXT NOT NULL,
status ENUM('draft','published') NOT NULL DEFAULT 'draft',
published_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (author_id) REFERENCES authors(id)
);
CREATE TABLE tags (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE
);
CREATE TABLE post_tags (
post_id INT UNSIGNED NOT NULL,
tag_id INT UNSIGNED NOT NULL,
PRIMARY KEY (post_id, tag_id),
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
);
CREATE TABLE comments (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
post_id INT UNSIGNED NOT NULL,
author_name VARCHAR(100) NOT NULL,
body TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE
);
Example starter: booking
CREATE TABLE rooms (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
capacity INT UNSIGNED NOT NULL,
CONSTRAINT chk_capacity CHECK (capacity > 0)
);
CREATE TABLE customers (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL
);
CREATE TABLE appointments (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
room_id INT UNSIGNED NOT NULL,
customer_id INT UNSIGNED NOT NULL,
starts_at DATETIME NOT NULL,
ends_at DATETIME NOT NULL,
status ENUM('booked','cancelled','completed') NOT NULL DEFAULT 'booked',
CONSTRAINT fk_appt_room FOREIGN KEY (room_id) REFERENCES rooms(id),
CONSTRAINT fk_appt_customer FOREIGN KEY (customer_id) REFERENCES customers(id),
CONSTRAINT chk_times CHECK (ends_at > starts_at),
UNIQUE KEY uq_room_slot (room_id, starts_at)
);
Seed sample data
Insert enough rows to make reporting interesting – at least ten customers, twenty orders (or posts, or appointments), spread across dates and statuses. Fabricated data is fine.
Deliverables
- ER sketch or bullet list of entities and relationships
- Complete
CREATE TABLEscript - Short note on any deliberate denormalisation
- Seed data script
The next tutorial writes reporting queries against this schema – revenue, activity, and “find the gaps” questions.

