Schema Patterns
Production-ready schema patterns for common application requirements.
User Management
Basic Users Table
sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
display_name VARCHAR(100),
avatar_url VARCHAR(500),
bio TEXT,
is_active BOOLEAN DEFAULT TRUE,
is_verified BOOLEAN DEFAULT FALSE,
role VARCHAR(20) DEFAULT 'user',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_users_active ON users(is_active);With User Profiles
sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE user_profiles (
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
display_name VARCHAR(100),
bio TEXT,
avatar_url VARCHAR(500),
timezone VARCHAR(50) DEFAULT 'UTC',
locale VARCHAR(10) DEFAULT 'en',
date_of_birth DATE,
gender VARCHAR(20),
website VARCHAR(500),
location VARCHAR(200),
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_user_profiles_location ON user_profiles(location);Content Management (Blog/CMS)
Posts with Categories and Tags
sql
CREATE TABLE categories (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
parent_id INTEGER REFERENCES categories(id),
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE tags (
id SERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL,
slug VARCHAR(50) UNIQUE NOT NULL,
color VARCHAR(7) DEFAULT '#000000',
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
author_id INTEGER NOT NULL REFERENCES users(id),
category_id INTEGER REFERENCES categories(id),
slug VARCHAR(255) UNIQUE NOT NULL,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
excerpt TEXT,
featured_image VARCHAR(500),
status VARCHAR(20) DEFAULT 'draft',
view_count INTEGER DEFAULT 0,
meta_title VARCHAR(255),
meta_description TEXT,
published_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE post_tags (
post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE,
tag_id INTEGER REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (post_id, tag_id)
);
CREATE INDEX idx_posts_author ON posts(author_id);
CREATE INDEX idx_posts_category ON posts(category_id);
CREATE INDEX idx_posts_status ON posts(status);
CREATE INDEX idx_posts_published ON posts(published_at);
CREATE INDEX idx_posts_slug ON posts(slug);
CREATE INDEX idx_categories_parent ON categories(parent_id);Comments (Self-Referencing)
sql
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
post_id INTEGER NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
author_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
parent_id INTEGER REFERENCES comments(id) ON DELETE CASCADE,
content TEXT NOT NULL,
is_approved BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_comments_post ON comments(post_id);
CREATE INDEX idx_comments_author ON comments(author_id);
CREATE INDEX idx_comments_parent ON comments(parent_id);
CREATE INDEX idx_comments_approved ON comments(post_id, is_approved);E-Commerce
Products and Orders
sql
CREATE TABLE products (
id SERIAL PRIMARY KEY,
sku VARCHAR(100) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
slug VARCHAR(255) UNIQUE NOT NULL,
description TEXT,
short_description TEXT,
price DECIMAL(10, 2) NOT NULL,
compare_price DECIMAL(10, 2),
cost_price DECIMAL(10, 2),
quantity INTEGER DEFAULT 0,
track_inventory BOOLEAN DEFAULT TRUE,
weight DECIMAL(8, 2),
status VARCHAR(20) DEFAULT 'active',
featured BOOLEAN DEFAULT FALSE,
meta_title VARCHAR(255),
meta_description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE product_images (
id SERIAL PRIMARY KEY,
product_id INTEGER NOT NULL REFERENCES products(id) ON DELETE CASCADE,
url VARCHAR(500) NOT NULL,
alt_text VARCHAR(255),
sort_order INTEGER DEFAULT 0,
is_primary BOOLEAN DEFAULT FALSE
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
order_number VARCHAR(50) UNIQUE NOT NULL,
status VARCHAR(20) DEFAULT 'pending',
payment_status VARCHAR(20) DEFAULT 'pending',
shipping_status VARCHAR(20) DEFAULT 'pending',
subtotal DECIMAL(10, 2) NOT NULL,
tax_amount DECIMAL(10, 2) DEFAULT 0,
shipping_amount DECIMAL(10, 2) DEFAULT 0,
discount_amount DECIMAL(10, 2) DEFAULT 0,
total DECIMAL(10, 2) NOT NULL,
currency VARCHAR(3) DEFAULT 'USD',
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE order_items (
id SERIAL PRIMARY KEY,
order_id INTEGER NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_id INTEGER REFERENCES products(id),
product_name VARCHAR(255) NOT NULL,
product_sku VARCHAR(100) NOT NULL,
quantity INTEGER NOT NULL,
unit_price DECIMAL(10, 2) NOT NULL,
total_price DECIMAL(10, 2) NOT NULL
);
CREATE INDEX idx_products_status ON products(status);
CREATE INDEX idx_products_featured ON products(featured);
CREATE INDEX idx_orders_user ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_order_items_order ON order_items(order_id);Shopping Cart
sql
CREATE TABLE cart_items (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
product_id INTEGER NOT NULL REFERENCES products(id) ON DELETE CASCADE,
quantity INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, product_id)
);
CREATE INDEX idx_cart_items_user ON cart_items(user_id);Social Features
Follows and Likes
sql
CREATE TABLE follows (
follower_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
following_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (follower_id, following_id),
CHECK (follower_id != following_id)
);
CREATE TABLE likes (
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
post_id INTEGER NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, post_id)
);
CREATE INDEX idx_follows_follower ON follows(follower_id);
CREATE INDEX idx_follows_following ON follows(following_id);
CREATE INDEX idx_likes_post ON likes(post_id);Notifications
sql
CREATE TABLE notifications (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(50) NOT NULL,
title VARCHAR(255) NOT NULL,
body TEXT,
action_url VARCHAR(500),
actor_id INTEGER REFERENCES users(id),
is_read BOOLEAN DEFAULT FALSE,
read_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_notifications_user ON notifications(user_id);
CREATE INDEX idx_notifications_unread ON notifications(user_id, is_read);
CREATE INDEX idx_notifications_created ON notifications(created_at);Multi-Tenant (SaaS)
Tenant Isolation
sql
CREATE TABLE tenants (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
slug VARCHAR(100) UNIQUE NOT NULL,
domain VARCHAR(255) UNIQUE,
plan VARCHAR(20) DEFAULT 'free',
is_active BOOLEAN DEFAULT TRUE,
settings JSONB DEFAULT '{}',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE tenant_users (
tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role VARCHAR(20) DEFAULT 'member',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (tenant_id, user_id)
);
-- Add tenant_id to all tenant-scoped tables
CREATE TABLE projects (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_projects_tenant ON projects(tenant_id);
CREATE INDEX idx_tenant_users_tenant ON tenant_users(tenant_id);Audit Logging
Change Tracking
sql
CREATE TABLE audit_logs (
id BIGSERIAL PRIMARY KEY,
table_name VARCHAR(100) NOT NULL,
record_id INTEGER NOT NULL,
action VARCHAR(10) NOT NULL,
old_values JSONB,
new_values JSONB,
changed_by INTEGER REFERENCES users(id),
ip_address VARCHAR(45),
user_agent TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_audit_logs_table ON audit_logs(table_name);
CREATE INDEX idx_audit_logs_record ON audit_logs(table_name, record_id);
CREATE INDEX idx_audit_logs_created ON audit_logs(created_at);Using Enums
PostgreSQL Native Enums
sql
CREATE TYPE user_role AS ENUM ('admin', 'moderator', 'user', 'guest');
CREATE TYPE post_status AS ENUM ('draft', 'review', 'published', 'archived');
CREATE TYPE order_status AS ENUM ('pending', 'processing', 'shipped', 'delivered', 'cancelled');
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
role user_role DEFAULT 'user'
);
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
status post_status DEFAULT 'draft'
);MySQL Inline Enums
sql
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL,
role ENUM('admin', 'moderator', 'user', 'guest') DEFAULT 'user'
);Soft Deletes
sql
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT,
deleted_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Query active posts only
-- SELECT * FROM posts WHERE deleted_at IS NULL;Full-Text Search
sql
CREATE TABLE articles (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
search_vector tsvector,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_articles_search ON articles USING GIN(search_vector);
-- Update search vector (via trigger or application)
-- UPDATE articles SET search_vector = to_tsvector('english', title || ' ' || content);