| name | database-schema-documentation |
| description | Document database schemas, ERD diagrams, table relationships, indexes, and constraints. Use when documenting database schema, creating ERD diagrams, or writing table documentation. |
Database Schema Documentation
Overview
Create comprehensive database schema documentation including entity relationship diagrams (ERD), table definitions, indexes, constraints, and data dictionaries.
When to Use
- Database schema documentation
- ERD (Entity Relationship Diagrams)
- Data dictionary creation
- Table relationship documentation
- Index and constraint documentation
- Migration documentation
- Database design specs
Schema Documentation Template
# Database Schema Documentation
**Database:** PostgreSQL 14.x
**Version:** 2.0
**Last Updated:** 2025-01-15
**Schema Version:** 20250115120000
## Overview
This database supports an e-commerce application with user management, product catalog, orders, and payment processing.
## Entity Relationship Diagram
```mermaid
erDiagram
users ||--o{ orders : places
users ||--o{ addresses : has
users ||--o{ payment_methods : has
orders ||--|{ order_items : contains
orders ||--|| payments : has
products ||--o{ order_items : includes
products }o--|| categories : belongs_to
products ||--o{ product_images : has
products ||--o{ inventory : tracks
users {
uuid id PK
string email UK
string password_hash
string name
timestamp created_at
timestamp updated_at
}
orders {
uuid id PK
uuid user_id FK
string status
decimal total_amount
timestamp created_at
timestamp updated_at
}
order_items {
uuid id PK
uuid order_id FK
uuid product_id FK
int quantity
decimal price
}
products {
uuid id PK
string name
text description
decimal price
uuid category_id FK
boolean active
}
Tables
users
Stores user account information.
Columns:
| Column | Type | Null | Default | Description |
|---|---|---|---|---|
| id | uuid | NO | gen_random_uuid() | Primary key |
| varchar(255) | NO | - | User email (unique) | |
| password_hash | varchar(255) | NO | - | bcrypt hashed password |
| name | varchar(255) | NO | - | User's full name |
| email_verified | boolean | NO | false | Email verification status |
| two_factor_enabled | boolean | NO | false | 2FA enabled flag |
| two_factor_secret | varchar(32) | YES | - | TOTP secret |
| created_at | timestamp | NO | now() | Record creation time |
| updated_at | timestamp | NO | now() | Last update time |
| deleted_at | timestamp | YES | - | Soft delete timestamp |
| last_login_at | timestamp | YES | - | Last login timestamp |
Indexes:
CREATE UNIQUE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_created_at ON users(created_at);
CREATE INDEX idx_users_deleted_at ON users(deleted_at) WHERE deleted_at IS NULL;
Constraints:
ALTER TABLE users
ADD CONSTRAINT users_email_format
CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$');
ALTER TABLE users
ADD CONSTRAINT users_name_length
CHECK (length(name) >= 2);
Triggers:
-- Update updated_at timestamp
CREATE TRIGGER update_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
Sample Data:
INSERT INTO users (email, password_hash, name, email_verified)
VALUES
('john@example.com', '$2b$12$...', 'John Doe', true),
('jane@example.com', '$2b$12$...', 'Jane Smith', true);
products
Stores product catalog information.
Columns:
| Column | Type | Null | Default | Description |
|---|---|---|---|---|
| id | uuid | NO | gen_random_uuid() | Primary key |
| name | varchar(255) | NO | - | Product name |
| slug | varchar(255) | NO | - | URL-friendly name (unique) |
| description | text | YES | - | Product description |
| price | decimal(10,2) | NO | - | Product price in USD |
| compare_at_price | decimal(10,2) | YES | - | Original price (for sales) |
| sku | varchar(100) | NO | - | Stock keeping unit (unique) |
| category_id | uuid | NO | - | Foreign key to categories |
| brand | varchar(100) | YES | - | Product brand |
| active | boolean | NO | true | Product visibility |
| featured | boolean | NO | false | Featured product flag |
| metadata | jsonb | YES | - | Additional product metadata |
| created_at | timestamp | NO | now() | Record creation time |
| updated_at | timestamp | NO | now() | Last update time |
Indexes:
CREATE UNIQUE INDEX idx_products_slug ON products(slug);
CREATE UNIQUE INDEX idx_products_sku ON products(sku);
CREATE INDEX idx_products_category_id ON products(category_id);
CREATE INDEX idx_products_active ON products(active);
CREATE INDEX idx_products_featured ON products(featured) WHERE featured = true;
CREATE INDEX idx_products_metadata ON products USING gin(metadata);
Foreign Keys:
ALTER TABLE products
ADD CONSTRAINT fk_products_category
FOREIGN KEY (category_id)
REFERENCES categories(id)
ON DELETE RESTRICT;
Full-Text Search:
-- Add full-text search column
ALTER TABLE products ADD COLUMN search_vector tsvector;
-- Create full-text index
CREATE INDEX idx_products_search ON products USING gin(search_vector);
-- Trigger to update search vector
CREATE TRIGGER products_search_vector_update
BEFORE INSERT OR UPDATE ON products
FOR EACH ROW
EXECUTE FUNCTION
tsvector_update_trigger(
search_vector, 'pg_catalog.english',
name, description, brand
);
orders
Stores customer orders.
Columns:
| Column | Type | Null | Default | Description |
|---|---|---|---|---|
| id | uuid | NO | gen_random_uuid() | Primary key |
| order_number | varchar(20) | NO | - | Human-readable order ID (unique) |
| user_id | uuid | NO | - | Foreign key to users |
| status | varchar(20) | NO | 'pending' | Order status |
| subtotal | decimal(10,2) | NO | - | Items subtotal |
| tax | decimal(10,2) | NO | 0 | Tax amount |
| shipping | decimal(10,2) | NO | 0 | Shipping cost |
| total | decimal(10,2) | NO | - | Total amount |
| currency | char(3) | NO | 'USD' | Currency code |
| notes | text | YES | - | Order notes |
| shipping_address | jsonb | NO | - | Shipping address |
| billing_address | jsonb | NO | - | Billing address |
| created_at | timestamp | NO | now() | Order creation time |
| updated_at | timestamp | NO | now() | Last update time |
| confirmed_at | timestamp | YES | - | Order confirmation time |
| shipped_at | timestamp | YES | - | Shipping time |
| delivered_at | timestamp | YES | - | Delivery time |
| cancelled_at | timestamp | YES | - | Cancellation time |
Indexes:
CREATE UNIQUE INDEX idx_orders_order_number ON orders(order_number);
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_created_at ON orders(created_at);
Constraints:
ALTER TABLE orders
ADD CONSTRAINT orders_status_check
CHECK (status IN ('pending', 'confirmed', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded'));
ALTER TABLE orders
ADD CONSTRAINT orders_total_positive
CHECK (total >= 0);
Computed Columns:
-- Total is computed from subtotal + tax + shipping
ALTER TABLE orders
ADD CONSTRAINT orders_total_computation
CHECK (total = subtotal + tax + shipping);
order_items
Line items for each order.
Columns:
| Column | Type | Null | Default | Description |
|---|---|---|---|---|
| id | uuid | NO | gen_random_uuid() | Primary key |
| order_id | uuid | NO | - | Foreign key to orders |
| product_id | uuid | NO | - | Foreign key to products |
| product_snapshot | jsonb | NO | - | Product data at order time |
| quantity | int | NO | - | Quantity ordered |
| unit_price | decimal(10,2) | NO | - | Price per unit |
| subtotal | decimal(10,2) | NO | - | Line item total |
| created_at | timestamp | NO | now() | Record creation time |
Indexes:
CREATE INDEX idx_order_items_order_id ON order_items(order_id);
CREATE INDEX idx_order_items_product_id ON order_items(product_id);
Foreign Keys:
ALTER TABLE order_items
ADD CONSTRAINT fk_order_items_order
FOREIGN KEY (order_id)
REFERENCES orders(id)
ON DELETE CASCADE;
ALTER TABLE order_items
ADD CONSTRAINT fk_order_items_product
FOREIGN KEY (product_id)
REFERENCES products(id)
ON DELETE RESTRICT;
Constraints:
ALTER TABLE order_items
ADD CONSTRAINT order_items_quantity_positive
CHECK (quantity > 0);
ALTER TABLE order_items
ADD CONSTRAINT order_items_subtotal_computation
CHECK (subtotal = quantity * unit_price);
Views
active_products_view
Shows only active products with category information.
CREATE VIEW active_products_view AS
SELECT
p.id,
p.name,
p.slug,
p.description,
p.price,
p.compare_at_price,
p.sku,
p.brand,
c.name as category_name,
c.slug as category_slug,
(SELECT COUNT(*) FROM order_items oi WHERE oi.product_id = p.id) as times_ordered,
(SELECT AVG(rating) FROM product_reviews pr WHERE pr.product_id = p.id) as avg_rating
FROM products p
JOIN categories c ON p.category_id = c.id
WHERE p.active = true;
user_order_summary
Aggregated order statistics per user.
CREATE MATERIALIZED VIEW user_order_summary AS
SELECT
u.id as user_id,
u.email,
u.name,
COUNT(o.id) as total_orders,
SUM(o.total) as total_spent,
AVG(o.total) as average_order_value,
MAX(o.created_at) as last_order_date,
MIN(o.created_at) as first_order_date
FROM users u
LEFT JOIN orders o ON u.id = o.user_id AND o.status != 'cancelled'
GROUP BY u.id, u.email, u.name;
-- Refresh strategy
CREATE INDEX idx_user_order_summary_user_id ON user_order_summary(user_id);
REFRESH MATERIALIZED VIEW CONCURRENTLY user_order_summary;
Functions
calculate_order_total
Calculates order total with tax and shipping.
CREATE OR REPLACE FUNCTION calculate_order_total(
p_subtotal decimal,
p_tax_rate decimal,
p_shipping decimal
)
RETURNS decimal AS $$
BEGIN
RETURN ROUND((p_subtotal * (1 + p_tax_rate) + p_shipping)::numeric, 2);
END;
$$ LANGUAGE plpgsql IMMUTABLE;
update_updated_at_column
Trigger function to automatically update updated_at timestamp.
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Data Dictionary
Enum Types
-- Order status values
CREATE TYPE order_status AS ENUM (
'pending',
'confirmed',
'processing',
'shipped',
'delivered',
'cancelled',
'refunded'
);
-- Payment status values
CREATE TYPE payment_status AS ENUM (
'pending',
'processing',
'succeeded',
'failed',
'refunded'
);
JSONB Structures
shipping_address format
{
"street": "123 Main St",
"street2": "Apt 4B",
"city": "New York",
"state": "NY",
"postalCode": "10001",
"country": "US"
}
product_snapshot format
{
"name": "Product Name",
"sku": "PROD-123",
"price": 99.99,
"image": "https://cdn.example.com/product.jpg"
}
Migrations
Migration: 20250115120000_add_two_factor_auth
-- Up
ALTER TABLE users ADD COLUMN two_factor_enabled BOOLEAN DEFAULT FALSE;
ALTER TABLE users ADD COLUMN two_factor_secret VARCHAR(32);
CREATE TABLE two_factor_backup_codes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
code_hash VARCHAR(255) NOT NULL,
used_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_2fa_backup_codes_user_id ON two_factor_backup_codes(user_id);
-- Down
DROP TABLE two_factor_backup_codes;
ALTER TABLE users DROP COLUMN two_factor_secret;
ALTER TABLE users DROP COLUMN two_factor_enabled;
Performance Optimization
Recommended Indexes
-- Frequently queried columns
CREATE INDEX CONCURRENTLY idx_users_email_verified ON users(email_verified);
CREATE INDEX CONCURRENTLY idx_products_price ON products(price);
CREATE INDEX CONCURRENTLY idx_orders_user_status ON orders(user_id, status);
-- Composite indexes for common queries
CREATE INDEX CONCURRENTLY idx_products_category_active
ON products(category_id, active)
WHERE active = true;
CREATE INDEX CONCURRENTLY idx_orders_user_created
ON orders(user_id, created_at DESC);
Query Optimization
-- EXPLAIN ANALYZE for slow queries
EXPLAIN ANALYZE
SELECT p.*, c.name as category_name
FROM products p
JOIN categories c ON p.category_id = c.id
WHERE p.active = true
ORDER BY p.created_at DESC
LIMIT 20;
-- Add covering index if needed
CREATE INDEX idx_products_active_created
ON products(active, created_at DESC)
INCLUDE (name, price, slug);
Backup & Recovery
Backup Schedule
- Full Backup: Daily at 2 AM UTC
- Incremental Backup: Every 6 hours
- WAL Archiving: Continuous
- Retention: 30 days
Backup Commands
# Full backup
pg_dump -h localhost -U postgres -Fc database_name > backup.dump
# Restore
pg_restore -h localhost -U postgres -d database_name backup.dump
# Backup specific tables
pg_dump -h localhost -U postgres -t users -t orders database_name > tables.sql
Data Retention Policy
| Table | Retention | Archive Strategy |
|---|---|---|
| users | Indefinite | Soft delete after 2 years inactive |
| orders | 7 years | Move to archive after 2 years |
| order_items | 7 years | Move to archive with orders |
| logs | 90 days | Delete after retention period |
## Best Practices
### ✅ DO
- Document all tables and columns
- Create ERD diagrams
- Document indexes and constraints
- Include sample data
- Document foreign key relationships
- Show JSONB field structures
- Document triggers and functions
- Include migration scripts
- Specify data types precisely
- Document performance considerations
### ❌ DON'T
- Skip constraint documentation
- Forget to version schema changes
- Ignore performance implications
- Skip index documentation
- Forget to document enum values
## Resources
- [PostgreSQL Documentation](https://www.postgresql.org/docs/)
- [dbdiagram.io](https://dbdiagram.io/) - ERD tool
- [SchemaSpy](https://schemaspy.org/) - Schema documentation generator
- [Mermaid ERD Syntax](https://mermaid.js.org/syntax/entityRelationshipDiagram.html)