Database design is the foundation of any scalable application. In this comprehensive guide, I'll share essential design patterns, indexing strategies, and optimization techniques that will help you build high-performance database systems.
1. Normalization and Denormalization Patterns
Understanding when to normalize and when to denormalize is crucial for database performance:
1.1 Third Normal Form (3NF) Design
-- Users table
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
);
-- User profiles table
CREATE TABLE user_profiles (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
first_name VARCHAR(100),
last_name VARCHAR(100),
avatar_url VARCHAR(500),
bio TEXT
);
-- User preferences table
CREATE TABLE user_preferences (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
theme VARCHAR(50) DEFAULT 'light',
notifications_enabled BOOLEAN DEFAULT true,
language VARCHAR(10) DEFAULT 'en'
);
1.2 Strategic Denormalization
-- Denormalized order table for better read performance
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
user_email VARCHAR(255), -- Denormalized for faster queries
user_name VARCHAR(200), -- Denormalized for faster queries
total_amount DECIMAL(10,2),
status VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
2. Indexing Strategies
Proper indexing is essential for query performance:
-- Composite indexes for common query patterns
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
CREATE INDEX idx_orders_created_status ON orders(created_at, status);
-- Partial indexes for filtered queries
CREATE INDEX idx_active_users ON users(email) WHERE deleted_at IS NULL;
-- Functional indexes for computed columns
CREATE INDEX idx_user_email_lower ON users(LOWER(email));
-- Covering indexes to avoid table lookups
CREATE INDEX idx_orders_covering ON orders(user_id, status, total_amount, created_at);
3. Partitioning Strategies
Table partitioning improves performance for large datasets:
-- Range partitioning by date
CREATE TABLE orders_2024 (
CHECK (created_at >= '2024-01-01' AND created_at < '2025-01-01')
) INHERITS (orders);
CREATE TABLE orders_2023 (
CHECK (created_at >= '2023-01-01' AND created_at < '2024-01-01')
) INHERITS (orders);
-- Hash partitioning for even distribution
CREATE TABLE users_partition_0 (
CHECK (id % 4 = 0)
) INHERITS (users);
CREATE TABLE users_partition_1 (
CHECK (id % 4 = 1)
) INHERITS (users);
4. Caching Patterns
Implementing caching strategies reduces database load:
-- Redis caching implementation
const redis = require('redis')
const client = redis.createClient()
class UserCache {
static async getUser(userId) {
const cacheKey = `user:${userId}`
// Try cache first
let user = await client.get(cacheKey)
if (user) {
return JSON.parse(user)
}
// Fetch from database
user = await User.findById(userId)
if (user) {
// Cache for 1 hour
await client.setex(cacheKey, 3600, JSON.stringify(user))
}
return user
}
static async invalidateUser(userId) {
await client.del(`user:${userId}`)
}
}
5. Connection Pooling
Connection pooling improves database performance:
// PostgreSQL connection pool
const { Pool } = require('pg')
const pool = new Pool({
user: process.env.DB_USER,
host: process.env.DB_HOST,
database: process.env.DB_NAME,
password: process.env.DB_PASSWORD,
port: process.env.DB_PORT,
max: 20, // Maximum number of clients
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
})
// Usage
const getUser = async (id) => {
const client = await pool.connect()
try {
const result = await client.query('SELECT * FROM users WHERE id = $1', [id])
return result.rows[0]
} finally {
client.release()
}
}
6. Query Optimization
Optimizing queries is crucial for performance:
-- Use EXPLAIN ANALYZE to understand query performance
EXPLAIN ANALYZE
SELECT u.email, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at >= '2024-01-01'
GROUP BY u.id, u.email
HAVING COUNT(o.id) > 5
ORDER BY order_count DESC;
-- Optimized version with proper indexing
CREATE INDEX idx_users_created_at ON users(created_at);
CREATE INDEX idx_orders_user_created ON orders(user_id, created_at);
-- Use window functions for complex aggregations
SELECT
user_id,
email,
order_count,
ROW_NUMBER() OVER (ORDER BY order_count DESC) as rank
FROM (
SELECT
u.id as user_id,
u.email,
COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at >= '2024-01-01'
GROUP BY u.id, u.email
) user_stats
WHERE order_count > 5;
Conclusion
Database design patterns are essential for building scalable, high-performance applications. By implementing proper normalization, indexing, partitioning, and caching strategies, you can create database systems that handle large-scale data efficiently.
Remember to monitor query performance, use appropriate indexes, and implement caching where beneficial. Always test your design patterns with realistic data volumes to ensure they perform well in production.