
Database Management Patterns
- 327 installs
- 61 repo stars
- Updated June 13, 2026
- manutej/luxor-claude-marketplace
Apply proven database management patterns when designing schemas, migrations, indexing, pooling, and query strategies for production apps.
About
Guides Claude through database management patterns for backend services: schema design, migrations, indexing, connection pooling, transactions, and query optimization. Helps teams avoid common persistence pitfalls and implement maintainable, production-ready data layers in SaaS, API, and ecommerce builds.
- Schema and migration design patterns
- Indexing and query optimization guidance
- Connection pooling and transaction handling
- Production-safe data access conventions
- Reusable patterns for relational and app data stores
Database Management Patterns by the numbers
- 327 all-time installs (skills.sh)
- +18 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #168 of 911 Databases skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/manutej/luxor-claude-marketplace --skill database-management-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 327 |
|---|---|
| repo stars | ★ 61 |
| Last updated | June 13, 2026 |
| Repository | manutej/luxor-claude-marketplace ↗ |
What it does
Apply proven database management patterns when designing schemas, migrations, indexing, pooling, and query strategies for production apps.
Files
Database Management Patterns
A comprehensive skill for mastering database management across SQL (PostgreSQL) and NoSQL (MongoDB) systems. This skill covers schema design, indexing strategies, transaction management, replication, sharding, and performance optimization for production-grade applications.
When to Use This Skill
Use this skill when:
- Designing database schemas for new applications or refactoring existing ones
- Choosing between SQL and NoSQL databases for your use case
- Optimizing query performance with proper indexing strategies
- Implementing data consistency with transactions and ACID guarantees
- Scaling databases horizontally with sharding and replication
- Managing high-traffic applications requiring distributed databases
- Ensuring data integrity with constraints, triggers, and validation
- Troubleshooting performance issues using explain plans and query analysis
- Building fault-tolerant systems with replication and failover strategies
- Working with complex data relationships (relational) or flexible schemas (document)
Core Concepts
Database Paradigms Comparison
Relational Databases (PostgreSQL)
Strengths:
- ACID Transactions: Strong consistency guarantees
- Complex Queries: JOIN operations, subqueries, CTEs
- Data Integrity: Foreign keys, constraints, triggers
- Normalized Data: Reduced redundancy, consistent updates
- Mature Ecosystem: Rich tooling, extensions, community
Best For:
- Financial systems requiring strict consistency
- Complex relationships and data integrity requirements
- Applications with structured, well-defined schemas
- Systems requiring complex analytical queries
- Multi-step transactions across multiple tables
Document Databases (MongoDB)
Strengths:
- Flexible Schema: Easy schema evolution, polymorphic data
- Horizontal Scalability: Built-in sharding support
- JSON-Native: Natural fit for modern application development
- Embedded Documents: Denormalized data for performance
- Aggregation Framework: Powerful data processing pipeline
Best For:
- Rapidly evolving applications with changing requirements
- Content management systems with varied data structures
- Real-time analytics and event logging
- Mobile and web applications with JSON APIs
- Hierarchical or nested data structures
ACID Properties
Atomicity: All operations in a transaction succeed or fail together Consistency: Transactions bring database from one valid state to another Isolation: Concurrent transactions don't interfere with each other Durability: Committed transactions survive system failures
CAP Theorem
In distributed systems, choose two of three:
- Consistency: All nodes see the same data
- Availability: System remains operational
- Partition Tolerance: System continues despite network failures
PostgreSQL emphasizes CP (Consistency + Partition Tolerance) MongoDB can be configured for CP or AP depending on write/read concerns
PostgreSQL Patterns
Schema Design Fundamentals
Normalization Levels
First Normal Form (1NF)
- Atomic values (no arrays or lists in columns)
- Each row is unique (primary key exists)
- No repeating groups
Second Normal Form (2NF)
- Meets 1NF requirements
- All non-key attributes depend on the entire primary key
Third Normal Form (3NF)
- Meets 2NF requirements
- No transitive dependencies (non-key attributes depend only on primary key)
When to Denormalize:
- Read-heavy workloads where joins are expensive
- Frequently accessed aggregate data
- Historical snapshots that shouldn't change
- Performance-critical queries
Table Design Patterns
Primary Keys:
-- Serial auto-increment (traditional)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- UUID for distributed systems
CREATE TABLE accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Composite primary key
CREATE TABLE order_items (
order_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
quantity INTEGER NOT NULL,
price NUMERIC(10, 2) NOT NULL,
PRIMARY KEY (order_id, product_id),
FOREIGN KEY (order_id) REFERENCES orders(id),
FOREIGN KEY (product_id) REFERENCES products(id)
);Foreign Key Constraints:
-- Cascade delete: Remove child records when parent deleted
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
title VARCHAR(255) NOT NULL,
content TEXT,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Set null: Preserve child records, nullify reference
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
post_id INTEGER,
user_id INTEGER,
content TEXT NOT NULL,
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE SET NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
);
-- Restrict: Prevent deletion if child records exist
CREATE TABLE categories (
id SERIAL PRIMARY KEY,
name VARCHAR(255) UNIQUE NOT NULL
);
CREATE TABLE products (
id SERIAL PRIMARY KEY,
category_id INTEGER NOT NULL,
name VARCHAR(255) NOT NULL,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE RESTRICT
);Advanced Constraints
Check Constraints:
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price NUMERIC(10, 2) NOT NULL CHECK (price > 0),
discount_percent INTEGER CHECK (discount_percent BETWEEN 0 AND 100),
stock_quantity INTEGER NOT NULL CHECK (stock_quantity >= 0)
);
-- Table-level check constraint
CREATE TABLE date_ranges (
id SERIAL PRIMARY KEY,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
CHECK (end_date > start_date)
);Unique Constraints:
-- Single column unique
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
username VARCHAR(50) UNIQUE NOT NULL
);
-- Composite unique constraint
CREATE TABLE user_permissions (
user_id INTEGER NOT NULL,
permission_id INTEGER NOT NULL,
granted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE (user_id, permission_id),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (permission_id) REFERENCES permissions(id)
);
-- Partial unique index (unique where condition met)
CREATE UNIQUE INDEX unique_active_email
ON users (email)
WHERE active = true;Triggers and Functions
Audit Trail Pattern:
-- Audit table
CREATE TABLE audit_log (
id SERIAL PRIMARY KEY,
table_name VARCHAR(255) NOT NULL,
record_id INTEGER NOT NULL,
action VARCHAR(10) NOT NULL,
old_data JSONB,
new_data JSONB,
changed_by VARCHAR(255),
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Trigger function
CREATE OR REPLACE FUNCTION audit_trigger_function()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
INSERT INTO audit_log (table_name, record_id, action, new_data, changed_by)
VALUES (TG_TABLE_NAME, NEW.id, 'INSERT', row_to_json(NEW), current_user);
RETURN NEW;
ELSIF TG_OP = 'UPDATE' THEN
INSERT INTO audit_log (table_name, record_id, action, old_data, new_data, changed_by)
VALUES (TG_TABLE_NAME, NEW.id, 'UPDATE', row_to_json(OLD), row_to_json(NEW), current_user);
RETURN NEW;
ELSIF TG_OP = 'DELETE' THEN
INSERT INTO audit_log (table_name, record_id, action, old_data, changed_by)
VALUES (TG_TABLE_NAME, OLD.id, 'DELETE', row_to_json(OLD), current_user);
RETURN OLD;
END IF;
END;
$$ LANGUAGE plpgsql;
-- Attach trigger to table
CREATE TRIGGER users_audit_trigger
AFTER INSERT OR UPDATE OR DELETE ON users
FOR EACH ROW EXECUTE FUNCTION audit_trigger_function();Timestamp Update Pattern:
CREATE OR REPLACE FUNCTION update_modified_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TRIGGER posts_update_timestamp
BEFORE UPDATE ON posts
FOR EACH ROW EXECUTE FUNCTION update_modified_timestamp();Views and Materialized Views
Standard Views:
-- Virtual table - computed on each query
CREATE VIEW active_users_with_posts AS
SELECT
u.id,
u.username,
u.email,
COUNT(p.id) as post_count,
MAX(p.created_at) as last_post_date
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
WHERE u.active = true
GROUP BY u.id, u.username, u.email;
-- Use view like a table
SELECT * FROM active_users_with_posts WHERE post_count > 10;Materialized Views:
-- Physical table - stores computed results
CREATE MATERIALIZED VIEW user_statistics AS
SELECT
u.id,
u.username,
COUNT(DISTINCT p.id) as total_posts,
COUNT(DISTINCT c.id) as total_comments,
AVG(p.views) as avg_post_views,
MAX(p.created_at) as last_activity
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
LEFT JOIN comments c ON u.id = c.user_id
GROUP BY u.id, u.username;
-- Create index on materialized view
CREATE INDEX idx_user_stats_posts ON user_statistics(total_posts);
-- Refresh materialized view (update data)
REFRESH MATERIALIZED VIEW user_statistics;
-- Concurrent refresh (allows reads during refresh)
REFRESH MATERIALIZED VIEW CONCURRENTLY user_statistics;MongoDB Patterns
Document Modeling Strategies
Embedding vs Referencing
Embedding Pattern (Denormalization):
// One-to-few: Embed when relationship is contained
// Example: Blog post with comments
{
_id: ObjectId("..."),
title: "Database Design Patterns",
author: "John Doe",
content: "...",
published_at: ISODate("2025-01-15"),
comments: [
{
_id: ObjectId("..."),
author: "Jane Smith",
text: "Great article!",
created_at: ISODate("2025-01-16")
},
{
_id: ObjectId("..."),
author: "Bob Johnson",
text: "Very helpful, thanks!",
created_at: ISODate("2025-01-17")
}
],
tags: ["database", "design", "patterns"],
stats: {
views: 1523,
likes: 89,
shares: 23
}
}
// Benefits:
// - Single query to retrieve post with comments
// - Better read performance
// - Atomic updates to entire document
//
// Drawbacks:
// - Document size limits (16MB in MongoDB)
// - Difficult to query comments independently
// - May duplicate data if comments need to appear elsewhereReferencing Pattern (Normalization):
// One-to-many or many-to-many: Reference when relationship is unbounded
// Example: User with many posts
// Users collection
{
_id: ObjectId("507f1f77bcf86cd799439011"),
username: "john_doe",
email: "john@example.com",
profile: {
bio: "Software engineer",
avatar_url: "https://...",
location: "San Francisco"
},
created_at: ISODate("2024-01-01")
}
// Posts collection (references user)
{
_id: ObjectId("507f191e810c19729de860ea"),
user_id: ObjectId("507f1f77bcf86cd799439011"),
title: "My First Post",
content: "...",
published_at: ISODate("2025-01-15"),
comment_ids: [
ObjectId("..."),
ObjectId("...")
]
}
// Benefits:
// - No duplication of user data
// - Flexible: users can have unlimited posts
// - Easy to update user information once
//
// Drawbacks:
// - Requires multiple queries or $lookup
// - Slower read performance for joined dataHybrid Approach (Selective Denormalization):
// Store frequently accessed fields from referenced document
{
_id: ObjectId("..."),
title: "Database Patterns",
content: "...",
author: {
// Embedded: frequently accessed, rarely changes
id: ObjectId("507f1f77bcf86cd799439011"),
username: "john_doe",
avatar_url: "https://..."
},
// Reference: full user data available if needed
author_id: ObjectId("507f1f77bcf86cd799439011"),
published_at: ISODate("2025-01-15")
}
// Benefits:
// - Fast reads with embedded frequently-used data
// - Can still get full user data when needed
// - Balance between performance and flexibility
//
// Tradeoffs:
// - Need to update embedded data when user changes username/avatar
// - Slightly larger documentsSchema Design Patterns
Bucket Pattern (Time-Series Data):
// Instead of one document per measurement:
// BAD: Millions of tiny documents
{
sensor_id: "sensor_001",
timestamp: ISODate("2025-01-15T10:00:00Z"),
temperature: 72.5,
humidity: 45
}
// GOOD: Bucket documents with arrays of measurements
{
sensor_id: "sensor_001",
date: ISODate("2025-01-15"),
hour: 10,
measurements: [
{ minute: 0, temperature: 72.5, humidity: 45 },
{ minute: 1, temperature: 72.6, humidity: 45 },
{ minute: 2, temperature: 72.4, humidity: 46 },
// ... up to 60 measurements per hour
],
summary: {
count: 60,
avg_temperature: 72.5,
min_temperature: 71.8,
max_temperature: 73.2
}
}
// Benefits:
// - Reduced document count (60x fewer documents)
// - Better index efficiency
// - Pre-computed summaries
// - Easier to query by time rangesComputed Pattern (Pre-Aggregated Data):
// Store computed values to avoid expensive aggregations
{
_id: ObjectId("..."),
product_id: "PROD-123",
month: "2025-01",
total_sales: 15420.50,
units_sold: 234,
unique_customers: 187,
avg_order_value: 65.90,
top_customers: [
{ customer_id: "CUST-456", revenue: 890.50 },
{ customer_id: "CUST-789", revenue: 675.25 }
],
computed_at: ISODate("2025-02-01T00:00:00Z")
}
// Update pattern: Scheduled job or trigger updates computed valuesPolymorphic Pattern (Varied Schemas):
// Handle different product types in single collection
{
_id: ObjectId("..."),
type: "book",
name: "Database Design",
price: 49.99,
// Book-specific fields
isbn: "978-0-123456-78-9",
author: "John Smith",
pages: 456,
publisher: "Tech Books Inc"
}
{
_id: ObjectId("..."),
type: "electronics",
name: "Wireless Mouse",
price: 29.99,
// Electronics-specific fields
brand: "TechBrand",
warranty_months: 24,
specifications: {
battery_life: "6 months",
connectivity: "Bluetooth 5.0"
}
}
// Query by type
db.products.find({ type: "book", author: "John Smith" })
db.products.find({ type: "electronics", "specifications.connectivity": /Bluetooth/ })Aggregation Framework
Basic Aggregation Pipeline:
// Group by author and count posts
db.posts.aggregate([
{
$match: { published: true } // Filter stage
},
{
$group: {
_id: "$author_id",
total_posts: { $sum: 1 },
total_views: { $sum: "$views" },
avg_views: { $avg: "$views" },
latest_post: { $max: "$published_at" }
}
},
{
$sort: { total_posts: -1 } // Sort by post count
},
{
$limit: 10 // Top 10 authors
}
])Advanced Pipeline with Lookup (Join):
// Join posts with user data
db.posts.aggregate([
{
$match: {
published_at: { $gte: ISODate("2025-01-01") }
}
},
{
$lookup: {
from: "users",
localField: "author_id",
foreignField: "_id",
as: "author"
}
},
{
$unwind: "$author" // Flatten author array
},
{
$project: {
title: 1,
content: 1,
views: 1,
"author.username": 1,
"author.email": 1,
days_since_publish: {
$divide: [
{ $subtract: [new Date(), "$published_at"] },
1000 * 60 * 60 * 24
]
}
}
},
{
$sort: { views: -1 }
}
])Aggregation with Grouping and Reshaping:
// Complex aggregation: Sales analysis
db.orders.aggregate([
{
$match: {
status: "completed",
created_at: {
$gte: ISODate("2025-01-01"),
$lt: ISODate("2025-02-01")
}
}
},
{
$unwind: "$items" // Flatten order items
},
{
$group: {
_id: {
product_id: "$items.product_id",
customer_region: "$customer.region"
},
total_quantity: { $sum: "$items.quantity" },
total_revenue: { $sum: "$items.total_price" },
order_count: { $sum: 1 },
avg_order_value: { $avg: "$items.total_price" }
}
},
{
$group: {
_id: "$_id.product_id",
regions: {
$push: {
region: "$_id.customer_region",
quantity: "$total_quantity",
revenue: "$total_revenue"
}
},
total_quantity: { $sum: "$total_quantity" },
total_revenue: { $sum: "$total_revenue" }
}
},
{
$sort: { total_revenue: -1 }
}
])Indexing Strategies
PostgreSQL Indexes
B-tree Indexes (Default):
-- Single column index
CREATE INDEX idx_users_email ON users(email);
-- Composite index (order matters!)
CREATE INDEX idx_posts_author_published
ON posts(author_id, published_at);
-- Query can use index:
-- SELECT * FROM posts WHERE author_id = 123 ORDER BY published_at;
-- SELECT * FROM posts WHERE author_id = 123 AND published_at > '2025-01-01';
-- Query CANNOT fully use index:
-- SELECT * FROM posts WHERE published_at > '2025-01-01'; (only uses first column)Partial Indexes:
-- Index only active users
CREATE INDEX idx_active_users
ON users(username)
WHERE active = true;
-- Index only recent orders
CREATE INDEX idx_recent_orders
ON orders(created_at, status)
WHERE created_at > '2024-01-01';
-- Benefits: Smaller index size, faster queries on filtered dataExpression Indexes:
-- Index on lowercase email for case-insensitive search
CREATE INDEX idx_users_email_lower
ON users(LOWER(email));
-- Query that uses this index:
SELECT * FROM users WHERE LOWER(email) = 'user@example.com';
-- Index on JSONB field extraction
CREATE INDEX idx_metadata_tags
ON products((metadata->>'category'));Full-Text Search Indexes:
-- Add tsvector column for full-text search
ALTER TABLE articles
ADD COLUMN tsv_content tsvector;
-- Populate tsvector column
UPDATE articles
SET tsv_content = to_tsvector('english', title || ' ' || content);
-- Create GIN index for full-text search
CREATE INDEX idx_articles_tsv ON articles USING GIN(tsv_content);
-- Full-text search query
SELECT title, ts_rank(tsv_content, query) as rank
FROM articles, to_tsquery('english', 'database & design') query
WHERE tsv_content @@ query
ORDER BY rank DESC;
-- Trigger to auto-update tsvector
CREATE TRIGGER articles_tsv_update
BEFORE INSERT OR UPDATE ON articles
FOR EACH ROW EXECUTE FUNCTION
tsvector_update_trigger(tsv_content, 'pg_catalog.english', title, content);JSONB Indexes:
-- GIN index for JSONB containment queries
CREATE INDEX idx_products_metadata
ON products USING GIN(metadata);
-- Queries that use this index:
SELECT * FROM products WHERE metadata @> '{"color": "blue"}';
SELECT * FROM products WHERE metadata ? 'size';
-- Index on specific JSONB path
CREATE INDEX idx_products_category
ON products((metadata->>'category'));Index Monitoring:
-- Find unused indexes
SELECT
schemaname,
tablename,
indexname,
idx_scan,
idx_tup_read,
idx_tup_fetch,
pg_size_pretty(pg_relation_size(indexrelid)) as index_size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;
-- Check index usage
SELECT
relname as table_name,
indexrelname as index_name,
idx_scan as times_used,
idx_tup_read as tuples_read,
idx_tup_fetch as tuples_fetched
FROM pg_stat_user_indexes
ORDER BY idx_scan ASC;MongoDB Indexes
Single Field Indexes:
// Create index on single field
db.users.createIndex({ email: 1 }) // 1 = ascending, -1 = descending
// Unique index
db.users.createIndex({ username: 1 }, { unique: true })
// Sparse index (only index documents with the field)
db.users.createIndex({ phone_number: 1 }, { sparse: true })Compound Indexes:
// Index on multiple fields (order matters!)
db.posts.createIndex({ author_id: 1, published_at: -1 })
// Efficient queries:
// - { author_id: "123" }
// - { author_id: "123", published_at: { $gte: ... } }
// - { author_id: "123" } with sort by published_at
// Inefficient:
// - { published_at: { $gte: ... } } alone (doesn't use index efficiently)
// ESR Rule: Equality, Sort, Range
// Best compound index order:
// 1. Equality filters first
// 2. Sort fields second
// 3. Range filters last
db.orders.createIndex({
status: 1, // Equality
created_at: -1, // Sort
total_amount: 1 // Range
})Multikey Indexes (Array Fields):
// Index on array field
db.posts.createIndex({ tags: 1 })
// Document with array
{
_id: ObjectId("..."),
title: "Database Design",
tags: ["database", "mongodb", "schema"]
}
// Query that uses multikey index
db.posts.find({ tags: "mongodb" })
db.posts.find({ tags: { $in: ["database", "nosql"] } })
// Compound multikey index (max one array field)
db.posts.createIndex({ tags: 1, published_at: -1 }) // Valid
// db.posts.createIndex({ tags: 1, categories: 1 }) // Invalid if both are arraysText Indexes:
// Create text index for full-text search
db.articles.createIndex({
title: "text",
content: "text"
})
// Text search query
db.articles.find({
$text: { $search: "database design patterns" }
})
// Search with relevance score
db.articles.find(
{ $text: { $search: "database design" } },
{ score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } })
// Weighted text index (prioritize title over content)
db.articles.createIndex(
{ title: "text", content: "text" },
{ weights: { title: 10, content: 5 } }
)Geospatial Indexes:
// 2dsphere index for geographic queries
db.locations.createIndex({ coordinates: "2dsphere" })
// Document format
{
name: "Coffee Shop",
coordinates: {
type: "Point",
coordinates: [-122.4194, 37.7749] // [longitude, latitude]
}
}
// Find locations near a point
db.locations.find({
coordinates: {
$near: {
$geometry: {
type: "Point",
coordinates: [-122.4194, 37.7749]
},
$maxDistance: 1000 // meters
}
}
})Index Properties:
// TTL Index (auto-delete documents after time)
db.sessions.createIndex(
{ created_at: 1 },
{ expireAfterSeconds: 3600 } // 1 hour
)
// Partial Index (index subset of documents)
db.orders.createIndex(
{ status: 1, created_at: -1 },
{ partialFilterExpression: { status: { $eq: "pending" } } }
)
// Case-insensitive index
db.users.createIndex(
{ email: 1 },
{ collation: { locale: "en", strength: 2 } }
)
// Background index creation (doesn't block operations)
db.large_collection.createIndex(
{ field: 1 },
{ background: true }
)Index Analysis:
// Explain query execution
db.posts.find({ author_id: "123" }).explain("executionStats")
// Check index usage
db.posts.aggregate([
{ $indexStats: {} }
])
// List all indexes on collection
db.posts.getIndexes()
// Drop unused index
db.posts.dropIndex("index_name")Transactions
PostgreSQL Transaction Management
Basic Transactions:
-- Explicit transaction
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
-- or ROLLBACK; to cancel changesSavepoints (Partial Rollback):
BEGIN;
UPDATE inventory SET quantity = quantity - 10 WHERE product_id = 'PROD-123';
SAVEPOINT before_audit;
INSERT INTO audit_log (action, details) VALUES ('inventory_update', '...');
-- Oops, error in audit log
ROLLBACK TO SAVEPOINT before_audit;
-- Inventory update preserved, audit insert rolled back
-- Fix and retry
INSERT INTO audit_log (action, details) VALUES ('inventory_update', 'correct details');
COMMIT;Isolation Levels:
-- Read Uncommitted (not supported in PostgreSQL, defaults to Read Committed)
-- Read Committed (default) - sees only committed data
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- Repeatable Read - sees snapshot at transaction start
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT * FROM accounts WHERE id = 1; -- Returns balance 1000
-- Another transaction updates balance to 1500 and commits
SELECT * FROM accounts WHERE id = 1; -- Still returns 1000 (repeatable read)
COMMIT;
-- Serializable - strictest isolation, prevents all anomalies
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- If concurrent transactions would violate serializability, one aborts
COMMIT;Advisory Locks (Application-Level Locking):
-- Exclusive lock on arbitrary number
SELECT pg_advisory_lock(12345);
-- ... perform critical operation ...
SELECT pg_advisory_unlock(12345);
-- Try lock (non-blocking)
SELECT pg_try_advisory_lock(12345); -- Returns true if acquired, false otherwise
-- Session-level advisory lock (auto-released on disconnect)
SELECT pg_advisory_lock(user_id);Row-Level Locking:
-- SELECT FOR UPDATE - lock rows for update
BEGIN;
SELECT * FROM products
WHERE id = 123
FOR UPDATE; -- Locks this row
UPDATE products SET quantity = quantity - 1 WHERE id = 123;
COMMIT;
-- SELECT FOR SHARE - shared lock (allows other reads, blocks writes)
SELECT * FROM products WHERE id = 123 FOR SHARE;
-- SKIP LOCKED - skip locked rows instead of waiting
SELECT * FROM queue
WHERE processed = false
ORDER BY priority
LIMIT 10
FOR UPDATE SKIP LOCKED;MongoDB Transactions
Multi-Document Transactions:
// Transactions require replica set or sharded cluster
const session = db.getMongo().startSession()
session.startTransaction()
try {
const accountsCol = session.getDatabase("mydb").accounts
// Debit account
accountsCol.updateOne(
{ _id: "account1" },
{ $inc: { balance: -100 } },
{ session }
)
// Credit account
accountsCol.updateOne(
{ _id: "account2" },
{ $inc: { balance: 100 } },
{ session }
)
// Commit transaction
session.commitTransaction()
} catch (error) {
// Abort on error
session.abortTransaction()
throw error
} finally {
session.endSession()
}Read and Write Concerns:
// Write Concern: Acknowledgment level
db.orders.insertOne(
{ customer_id: "123", items: [...] },
{
writeConcern: {
w: "majority", // Wait for majority of replica set
j: true, // Wait for journal write
wtimeout: 5000 // Timeout after 5 seconds
}
}
)
// Read Concern: Data consistency level
db.orders.find(
{ status: "pending" }
).readConcern("majority") // Only return data acknowledged by majority
// Read Preference: Which replica to read from
db.orders.find({ ... }).readPref("secondary") // Read from secondary replicaAtomic Operations (Single Document):
// Single document updates are atomic by default
db.counters.updateOne(
{ _id: "page_views" },
{
$inc: { count: 1 },
$set: { last_updated: new Date() }
}
)
// Atomic array operations
db.posts.updateOne(
{ _id: ObjectId("...") },
{
$push: {
comments: {
$each: [{ author: "John", text: "Great!" }],
$position: 0 // Insert at beginning
}
}
}
)
// Find and modify (atomic read-modify-write)
db.queue.findOneAndUpdate(
{ status: "pending" },
{ $set: { status: "processing", processor_id: "worker-1" } },
{
sort: { priority: -1 },
returnDocument: "after" // Return updated document
}
)Replication
PostgreSQL Replication
Streaming Replication (Primary-Standby):
-- Primary server configuration (postgresql.conf)
wal_level = replica
max_wal_senders = 10
wal_keep_size = '1GB'
hot_standby = on
-- Create replication user
CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'secure_password';
-- pg_hba.conf on primary
host replication replicator standby_ip/32 md5
-- Standby server (recovery.conf or postgresql.auto.conf)
primary_conninfo = 'host=primary_ip port=5432 user=replicator password=...'
restore_command = 'cp /var/lib/postgresql/archive/%f %p'Logical Replication (Selective Replication):
-- On publisher (source)
CREATE PUBLICATION my_publication FOR TABLE users, posts;
-- or FOR ALL TABLES;
-- On subscriber (destination)
CREATE SUBSCRIPTION my_subscription
CONNECTION 'host=publisher_ip dbname=mydb user=replicator password=...'
PUBLICATION my_publication;
-- Monitor replication
SELECT * FROM pg_stat_replication;
SELECT * FROM pg_replication_slots;Failover and Promotion:
-- Promote standby to primary
pg_ctl promote -D /var/lib/postgresql/data
-- Check replication lag
SELECT
client_addr,
state,
sent_lsn,
write_lsn,
flush_lsn,
replay_lsn,
sync_state,
pg_wal_lsn_diff(sent_lsn, replay_lsn) AS lag_bytes
FROM pg_stat_replication;MongoDB Replication
Replica Set Configuration:
// Initialize replica set
rs.initiate({
_id: "myReplicaSet",
members: [
{ _id: 0, host: "mongodb1.example.com:27017", priority: 2 },
{ _id: 1, host: "mongodb2.example.com:27017", priority: 1 },
{ _id: 2, host: "mongodb3.example.com:27017", priority: 1 }
]
})
// Add member to existing replica set
rs.add("mongodb4.example.com:27017")
// Remove member
rs.remove("mongodb4.example.com:27017")
// Check replica set status
rs.status()
// Check replication lag
rs.printSecondaryReplicationInfo()Replica Set Roles:
// Priority 0 member (cannot become primary)
rs.add({
host: "analytics.example.com:27017",
priority: 0,
hidden: true // Hidden from application drivers
})
// Arbiter (voting only, no data)
rs.addArb("arbiter.example.com:27017")
// Delayed member (disaster recovery)
rs.add({
host: "delayed.example.com:27017",
priority: 0,
hidden: true,
slaveDelay: 3600 // 1 hour behind
})Read Preference Configuration:
// Application connection with read preference
const client = new MongoClient(uri, {
readPreference: "secondaryPreferred", // Try secondary, fallback to primary
readConcernLevel: "majority"
})
// Read Preference Modes:
// - primary (default): Read from primary only
// - primaryPreferred: Primary if available, else secondary
// - secondary: Read from secondary only
// - secondaryPreferred: Secondary if available, else primary
// - nearest: Read from nearest member (lowest latency)Sharding
MongoDB Sharding Architecture
Shard Key Selection:
// Good shard key characteristics:
// 1. High cardinality (many distinct values)
// 2. Even distribution
// 3. Query isolation (queries target specific shards)
// Example: User-based application
sh.shardCollection("mydb.users", { user_id: "hashed" })
// Hashed shard key: Even distribution, random data location
sh.shardCollection("mydb.events", { event_id: "hashed" })
// Range-based shard key: Ordered data, good for range queries
sh.shardCollection("mydb.logs", { timestamp: 1, server_id: 1 })
// Compound shard key
sh.shardCollection("mydb.orders", {
customer_region: 1, // Coarse grouping
order_date: 1 // Fine grouping
})Sharding Setup:
// 1. Start config servers (replica set)
mongod --configsvr --replSet configRS --port 27019
// 2. Initialize config server replica set
rs.initiate({
_id: "configRS",
configsvr: true,
members: [
{ _id: 0, host: "cfg1.example.com:27019" },
{ _id: 1, host: "cfg2.example.com:27019" },
{ _id: 2, host: "cfg3.example.com:27019" }
]
})
// 3. Start shard servers (each is a replica set)
mongod --shardsvr --replSet shard1RS --port 27018
// 4. Start mongos (query router)
mongos --configdb configRS/cfg1.example.com:27019,cfg2.example.com:27019
// 5. Add shards to cluster
sh.addShard("shard1RS/shard1-a.example.com:27018")
sh.addShard("shard2RS/shard2-a.example.com:27018")
// 6. Enable sharding on database
sh.enableSharding("mydb")
// 7. Shard collections
sh.shardCollection("mydb.users", { user_id: "hashed" })Query Targeting:
// Targeted query (includes shard key)
db.users.find({ user_id: "12345" })
// Routes to single shard
// Scatter-gather query (no shard key)
db.users.find({ email: "user@example.com" })
// Queries all shards, merges results
// Check query targeting
db.users.find({ user_id: "12345" }).explain()
// Look for "SINGLE_SHARD" vs "ALL_SHARDS"Zone Sharding (Geographic Distribution):
// Define zones for geographic sharding
sh.addShardToZone("shard1", "US")
sh.addShardToZone("shard2", "EU")
// Define zone ranges
sh.updateZoneKeyRange(
"mydb.users",
{ region: "US", user_id: MinKey },
{ region: "US", user_id: MaxKey },
"US"
)
sh.updateZoneKeyRange(
"mydb.users",
{ region: "EU", user_id: MinKey },
{ region: "EU", user_id: MaxKey },
"EU"
)
// Shard collection with zone-aware key
sh.shardCollection("mydb.users", { region: 1, user_id: 1 })PostgreSQL Horizontal Partitioning
Declarative Partitioning:
-- Range partitioning
CREATE TABLE logs (
id BIGSERIAL,
log_time TIMESTAMP NOT NULL,
message TEXT,
level VARCHAR(10)
) PARTITION BY RANGE (log_time);
-- Create partitions
CREATE TABLE logs_2025_01 PARTITION OF logs
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
CREATE TABLE logs_2025_02 PARTITION OF logs
FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');
-- List partitioning
CREATE TABLE customers (
id SERIAL,
name VARCHAR(255),
region VARCHAR(50)
) PARTITION BY LIST (region);
CREATE TABLE customers_us PARTITION OF customers
FOR VALUES IN ('US', 'CA', 'MX');
CREATE TABLE customers_eu PARTITION OF customers
FOR VALUES IN ('UK', 'DE', 'FR', 'IT');
-- Hash partitioning
CREATE TABLE events (
id BIGSERIAL,
event_type VARCHAR(50),
data JSONB
) PARTITION BY HASH (id);
CREATE TABLE events_0 PARTITION OF events
FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE events_1 PARTITION OF events
FOR VALUES WITH (MODULUS 4, REMAINDER 1);
-- ... events_2 and events_3Partition Pruning (Query Optimization):
-- Query automatically uses only relevant partition
SELECT * FROM logs
WHERE log_time BETWEEN '2025-01-15' AND '2025-01-20';
-- Only scans logs_2025_01 partition
-- Check query plan
EXPLAIN SELECT * FROM logs WHERE log_time > '2025-01-01';
-- Shows which partitions are scannedPerformance Tuning
Query Optimization Techniques
PostgreSQL Query Analysis:
-- Basic explain
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
-- Analyze with actual execution statistics
EXPLAIN ANALYZE
SELECT u.username, COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
WHERE u.active = true
GROUP BY u.id, u.username
ORDER BY post_count DESC
LIMIT 10;
-- Identify slow queries
SELECT
query,
calls,
total_exec_time,
mean_exec_time,
max_exec_time
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;
-- Table statistics
ANALYZE users; -- Update query planner statistics
-- Vacuum and analyze
VACUUM ANALYZE posts; -- Reclaim space and update statsCommon Query Patterns:
-- Avoid SELECT * (retrieve only needed columns)
-- BAD
SELECT * FROM users WHERE id = 123;
-- GOOD
SELECT id, username, email FROM users WHERE id = 123;
-- Use EXISTS instead of IN for large subqueries
-- BAD
SELECT * FROM posts WHERE author_id IN (
SELECT id FROM users WHERE active = true
);
-- GOOD
SELECT * FROM posts p WHERE EXISTS (
SELECT 1 FROM users u
WHERE u.id = p.author_id AND u.active = true
);
-- Use JOINs instead of multiple queries
-- BAD (N+1 query problem)
-- SELECT * FROM posts;
-- Then for each post: SELECT * FROM users WHERE id = post.author_id;
-- GOOD
SELECT p.*, u.username, u.email
FROM posts p
JOIN users u ON p.author_id = u.id;
-- Window functions instead of self-joins
-- Calculate running total
SELECT
order_date,
amount,
SUM(amount) OVER (ORDER BY order_date) as running_total
FROM orders;
-- Rank within groups
SELECT
category,
product_name,
sales,
RANK() OVER (PARTITION BY category ORDER BY sales DESC) as rank_in_category
FROM products;MongoDB Query Optimization:
// Use projection to limit returned fields
// BAD
db.users.find({ active: true })
// GOOD
db.users.find(
{ active: true },
{ username: 1, email: 1, _id: 0 }
)
// Use covered queries (index covers all fields)
db.users.createIndex({ username: 1, email: 1 })
db.users.find(
{ username: "john_doe" },
{ username: 1, email: 1, _id: 0 }
) // Entire query served from index
// Avoid negation operators
// BAD (cannot use index efficiently)
db.products.find({ status: { $ne: "discontinued" } })
// GOOD
db.products.find({ status: { $in: ["active", "pending", "sold"] } })
// Use $lookup sparingly (expensive operation)
// Consider embedding data instead if appropriate
// Aggregation optimization: Filter early
// BAD
db.orders.aggregate([
{ $lookup: { ... } }, // Expensive join
{ $match: { status: "completed" } } // Filter after join
])
// GOOD
db.orders.aggregate([
{ $match: { status: "completed" } }, // Filter first
{ $lookup: { ... } } // Join fewer documents
])Connection Pooling
PostgreSQL Connection Pooling:
// Using node-postgres (pg) with pool
const { Pool } = require('pg')
const pool = new Pool({
host: 'localhost',
port: 5432,
database: 'mydb',
user: 'dbuser',
password: 'secret',
max: 20, // Maximum pool size
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000
})
// Execute query
const result = await pool.query('SELECT * FROM users WHERE id = $1', [123])
// Use PgBouncer for server-side pooling
// pgbouncer.ini
// [databases]
// mydb = host=localhost port=5432 dbname=mydb
//
// [pgbouncer]
// pool_mode = transaction
// max_client_conn = 1000
// default_pool_size = 25MongoDB Connection Pooling:
// MongoClient automatically manages connection pool
const { MongoClient } = require('mongodb')
const client = new MongoClient(uri, {
maxPoolSize: 50, // Max connections
minPoolSize: 10, // Min connections
maxIdleTimeMS: 30000, // Close idle connections
waitQueueTimeoutMS: 5000 // Wait for available connection
})
await client.connect()
const db = client.db('mydb')
// Connection automatically returned to pool after useBest Practices
PostgreSQL Best Practices
1. Schema Design
- Normalize for data integrity, denormalize for performance
- Use appropriate data types (avoid TEXT for short strings)
- Define NOT NULL constraints where appropriate
- Use SERIAL or UUID for primary keys consistently
2. Indexing
- Index foreign keys for JOIN performance
- Create indexes on frequently filtered/sorted columns
- Use partial indexes for selective queries
- Monitor and remove unused indexes
- Keep composite index column count reasonable (typically ≤ 3-4)
3. Query Performance
- Use EXPLAIN ANALYZE to understand query plans
- Avoid SELECT * in application code
- Use prepared statements to prevent SQL injection
- Limit result sets with LIMIT
- Use connection pooling
4. Maintenance
- Run VACUUM regularly (or enable autovacuum)
- Update statistics with ANALYZE
- Monitor slow query log
- Set appropriate autovacuum thresholds
- Regular backup with pg_dump or WAL archiving
5. Security
- Use SSL/TLS for connections
- Implement row-level security for multi-tenant apps
- Grant minimum necessary privileges
- Use parameterized queries
- Regular security updates
MongoDB Best Practices
1. Schema Design
- Embed related data that is accessed together
- Reference data that is large or rarely accessed
- Use polymorphic pattern for varied schemas
- Limit document size to reasonable bounds (< 1-2 MB typically)
- Design for your query patterns
2. Indexing
- Index on fields used in queries and sorts
- Use compound indexes with ESR rule (Equality, Sort, Range)
- Create text indexes for full-text search
- Monitor index usage with $indexStats
- Avoid too many indexes (write performance impact)
3. Query Performance
- Use projection to limit returned fields
- Create covered queries when possible
- Filter early in aggregation pipelines
- Avoid $lookup when embedding is appropriate
- Use explain() to verify index usage
4. Scalability
- Choose appropriate shard key (high cardinality, even distribution)
- Use replica sets for high availability
- Configure appropriate read/write concerns
- Monitor chunk distribution in sharded clusters
- Use zones for geographic distribution
5. Operations
- Enable authentication and authorization
- Use TLS for client connections
- Regular backups (mongodump or filesystem snapshots)
- Monitor with MongoDB Atlas, Ops Manager, or custom tools
- Keep MongoDB version updated
Data Modeling Decision Framework
Choose PostgreSQL when:
- Strong ACID guarantees required (financial transactions)
- Complex relationships with many JOINs
- Data structure is well-defined and stable
- Need for advanced SQL features (window functions, CTEs, stored procedures)
- Compliance requirements demand strict consistency
Choose MongoDB when:
- Schema flexibility needed (rapid development, evolving requirements)
- Horizontal scalability is priority (sharding required)
- Document-oriented data (JSON/BSON native format)
- Hierarchical or nested data structures
- High write throughput with eventual consistency acceptable
Hybrid Approach:
- Use both databases for different parts of application
- PostgreSQL for transactional data (orders, payments)
- MongoDB for catalog, logs, user sessions
- Synchronize critical data between systems
Common Patterns and Anti-Patterns
PostgreSQL Anti-Patterns
❌ Storing JSON when relational fits better
-- BAD: Using JSONB for structured, queryable data
CREATE TABLE users (
id SERIAL PRIMARY KEY,
data JSONB -- { name, email, address: { street, city, state } }
);
-- GOOD: Proper normalization
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
email VARCHAR(255)
);
CREATE TABLE addresses (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
street VARCHAR(255),
city VARCHAR(100),
state VARCHAR(50)
);❌ Over-indexing
-- BAD: Index on every column "just in case"
CREATE INDEX idx1 ON users(username);
CREATE INDEX idx2 ON users(email);
CREATE INDEX idx3 ON users(created_at);
CREATE INDEX idx4 ON users(updated_at);
CREATE INDEX idx5 ON users(active);
-- Result: Slow writes, large database size
-- GOOD: Index based on actual query patterns
CREATE INDEX idx_users_email ON users(email); -- Login queries
CREATE INDEX idx_active_users_created ON users(created_at) WHERE active = true; -- Partial❌ N+1 Query Problem
-- BAD: Multiple queries in loop
SELECT * FROM posts; -- Returns 100 posts
-- Then for each post:
SELECT * FROM users WHERE id = ?; -- 100 additional queries!
-- GOOD: Single query with JOIN
SELECT p.*, u.username, u.email
FROM posts p
JOIN users u ON p.author_id = u.id;MongoDB Anti-Patterns
❌ Massive arrays in documents
// BAD: Unbounded array growth
{
_id: ObjectId("..."),
username: "popular_user",
followers: [
ObjectId("follower1"),
ObjectId("follower2"),
// ... 100,000+ follower IDs
// Document exceeds 16MB limit!
]
}
// GOOD: Separate collection with references
// users collection
{ _id: ObjectId("..."), username: "popular_user" }
// followers collection
{ _id: ObjectId("..."), user_id: ObjectId("..."), follower_id: ObjectId("...") }
db.followers.createIndex({ user_id: 1, follower_id: 1 })❌ Poor shard key selection
// BAD: Monotonically increasing shard key
sh.shardCollection("mydb.events", { _id: 1 })
// All writes go to same shard (highest _id range)
// BAD: Low cardinality shard key
sh.shardCollection("mydb.users", { country: 1 })
// Most users in few countries = uneven distribution
// GOOD: Hashed _id or compound key
sh.shardCollection("mydb.events", { _id: "hashed" }) // Even distribution
sh.shardCollection("mydb.users", { country: 1, user_id: 1 }) // Compound❌ Ignoring indexes on embedded documents
// Document structure
{
username: "john_doe",
profile: {
email: "john@example.com",
age: 30,
city: "San Francisco"
}
}
// Query on embedded field
db.users.find({ "profile.email": "john@example.com" })
// MISSING: Index on embedded field
db.users.createIndex({ "profile.email": 1 })Troubleshooting Guide
PostgreSQL Issues
Slow Queries:
-- Enable slow query logging (postgresql.conf)
-- log_min_duration_statement = 1000 # Log queries > 1 second
-- Find slow queries
SELECT
query,
calls,
total_exec_time / calls as avg_time_ms,
rows / calls as avg_rows
FROM pg_stat_statements
WHERE calls > 100
ORDER BY total_exec_time DESC
LIMIT 20;
-- Analyze specific slow query
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT ... FROM ... WHERE ...;High CPU Usage:
-- Check running queries
SELECT
pid,
now() - query_start as duration,
state,
query
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY duration DESC;
-- Terminate long-running query
SELECT pg_terminate_backend(pid);Lock Contention:
-- View locks
SELECT
locktype,
relation::regclass,
mode,
granted,
pid
FROM pg_locks
WHERE NOT granted;
-- Find blocking queries
SELECT
blocked_locks.pid AS blocked_pid,
blocking_locks.pid AS blocking_pid,
blocked_activity.query AS blocked_query,
blocking_activity.query AS blocking_query
FROM pg_locks blocked_locks
JOIN pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
JOIN pg_locks blocking_locks ON blocking_locks.locktype = blocked_locks.locktype
JOIN pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.granted AND blocking_locks.granted;MongoDB Issues
Slow Queries:
// Enable profiling
db.setProfilingLevel(1, { slowms: 100 }) // Log queries > 100ms
// View slow queries
db.system.profile.find().sort({ ts: -1 }).limit(10)
// Analyze query performance
db.collection.find({ ... }).explain("executionStats")
// Check: totalDocsExamined vs nReturned (should be close)
// Check: executionTimeMillis
// Check: indexName (should show index usage)Replication Lag:
// Check lag on secondary
rs.printSecondaryReplicationInfo()
// Check oplog size
db.getReplicationInfo()
// Increase oplog size if needed
db.adminCommand({ replSetResizeOplog: 1, size: 16384 }) // 16GBSharding Issues:
// Check chunk distribution
sh.status()
// Check balancer status
sh.getBalancerState()
sh.isBalancerRunning()
// Balance specific collection
sh.enableBalancing("mydb.mycollection")
// Check for jumbo chunks
db.chunks.find({ jumbo: true })Resources
PostgreSQL Resources
- Official Documentation: https://www.postgresql.org/docs/
- PostgreSQL Wiki: https://wiki.postgresql.org/
- Performance Tuning: https://wiki.postgresql.org/wiki/Performance_Optimization
- Explain Visualizer: https://explain.dalibo.com/
- pg_stat_statements Extension: Essential for query analysis
MongoDB Resources
- Official Documentation: https://docs.mongodb.com/
- MongoDB University: Free courses and certification
- Aggregation Framework: https://docs.mongodb.com/manual/aggregation/
- Sharding Guide: https://docs.mongodb.com/manual/sharding/
- Schema Design Patterns: https://www.mongodb.com/blog/post/building-with-patterns-a-summary
Books
- PostgreSQL: "PostgreSQL: Up and Running" by Regina Obe & Leo Hsu
- MongoDB: "MongoDB: The Definitive Guide" by Shannon Bradshaw, Eoin Brazil, Kristina Chodorow
---
Skill Version: 1.0.0 Last Updated: January 2025 Skill Category: Database Management, Data Architecture, Performance Optimization Technologies: PostgreSQL 16+, MongoDB 7+
Database Management Patterns - Detailed Examples
Comprehensive collection of production-ready examples for PostgreSQL and MongoDB database management.
Table of Contents
1. PostgreSQL Schema Design Examples 2. PostgreSQL Advanced Queries 3. PostgreSQL Performance Optimization 4. MongoDB Schema Design Examples 5. MongoDB Aggregation Examples 6. MongoDB Sharding Examples 7. Cross-Database Patterns 8. Real-World Use Cases
---
PostgreSQL Schema Design Examples
Example 1: E-Commerce Database Schema
Scenario: Design a normalized schema for an e-commerce platform with products, orders, customers, and inventory.
-- Customers table
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
phone VARCHAR(20),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT email_format CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$')
);
-- Addresses table (one customer, many addresses)
CREATE TABLE addresses (
id SERIAL PRIMARY KEY,
customer_id INTEGER NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
address_type VARCHAR(20) NOT NULL CHECK (address_type IN ('billing', 'shipping')),
street_line1 VARCHAR(255) NOT NULL,
street_line2 VARCHAR(255),
city VARCHAR(100) NOT NULL,
state VARCHAR(50) NOT NULL,
postal_code VARCHAR(20) NOT NULL,
country VARCHAR(50) NOT NULL DEFAULT 'US',
is_default BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Categories table (hierarchical)
CREATE TABLE categories (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) UNIQUE NOT NULL,
parent_id INTEGER REFERENCES categories(id),
description TEXT,
display_order INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Products table
CREATE TABLE products (
id SERIAL PRIMARY KEY,
category_id INTEGER NOT NULL REFERENCES categories(id),
sku VARCHAR(50) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
description TEXT,
price NUMERIC(10, 2) NOT NULL CHECK (price >= 0),
cost NUMERIC(10, 2) CHECK (cost >= 0),
weight_kg NUMERIC(8, 2),
dimensions JSONB, -- { length, width, height, unit }
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT price_cost CHECK (price >= cost)
);
-- Inventory table
CREATE TABLE inventory (
id SERIAL PRIMARY KEY,
product_id INTEGER NOT NULL REFERENCES products(id),
warehouse_id INTEGER NOT NULL,
quantity INTEGER NOT NULL DEFAULT 0 CHECK (quantity >= 0),
reserved_quantity INTEGER NOT NULL DEFAULT 0 CHECK (reserved_quantity >= 0),
reorder_level INTEGER DEFAULT 10,
last_restocked TIMESTAMP,
UNIQUE (product_id, warehouse_id),
CONSTRAINT available_stock CHECK (quantity >= reserved_quantity)
);
-- Orders table
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_id INTEGER NOT NULL REFERENCES customers(id),
order_number VARCHAR(50) UNIQUE NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'processing', 'shipped', 'delivered', 'cancelled')),
subtotal NUMERIC(10, 2) NOT NULL CHECK (subtotal >= 0),
tax_amount NUMERIC(10, 2) NOT NULL DEFAULT 0 CHECK (tax_amount >= 0),
shipping_amount NUMERIC(10, 2) NOT NULL DEFAULT 0 CHECK (shipping_amount >= 0),
total_amount NUMERIC(10, 2) NOT NULL CHECK (total_amount >= 0),
shipping_address_id INTEGER REFERENCES addresses(id),
billing_address_id INTEGER REFERENCES addresses(id),
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT total_calculation CHECK (
total_amount = subtotal + tax_amount + shipping_amount
)
);
-- Order items table
CREATE TABLE order_items (
id SERIAL PRIMARY KEY,
order_id INTEGER NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_id INTEGER NOT NULL REFERENCES products(id),
quantity INTEGER NOT NULL CHECK (quantity > 0),
unit_price NUMERIC(10, 2) NOT NULL CHECK (unit_price >= 0),
subtotal NUMERIC(10, 2) NOT NULL CHECK (subtotal >= 0),
discount_amount NUMERIC(10, 2) DEFAULT 0 CHECK (discount_amount >= 0),
CONSTRAINT subtotal_calculation CHECK (
subtotal = (unit_price * quantity) - discount_amount
)
);
-- Indexes for performance
CREATE INDEX idx_customers_email ON customers(email);
CREATE INDEX idx_addresses_customer ON addresses(customer_id);
CREATE INDEX idx_products_category ON products(category_id);
CREATE INDEX idx_products_sku ON products(sku);
CREATE INDEX idx_products_active ON products(is_active) WHERE is_active = true;
CREATE INDEX idx_inventory_product ON inventory(product_id);
CREATE INDEX idx_orders_customer ON orders(customer_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_created ON orders(created_at DESC);
CREATE INDEX idx_order_items_order ON order_items(order_id);
CREATE INDEX idx_order_items_product ON order_items(product_id);
-- Trigger to update updated_at timestamp
CREATE OR REPLACE FUNCTION update_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER customers_update_timestamp
BEFORE UPDATE ON customers
FOR EACH ROW EXECUTE FUNCTION update_timestamp();
CREATE TRIGGER products_update_timestamp
BEFORE UPDATE ON products
FOR EACH ROW EXECUTE FUNCTION update_timestamp();
CREATE TRIGGER orders_update_timestamp
BEFORE UPDATE ON orders
FOR EACH ROW EXECUTE FUNCTION update_timestamp();Example 2: Multi-Tenant SaaS Application
Scenario: Design schema for a multi-tenant application with row-level security.
-- Tenants table
CREATE TABLE tenants (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
subdomain VARCHAR(100) UNIQUE NOT NULL,
plan VARCHAR(50) NOT NULL CHECK (plan IN ('free', 'starter', 'professional', 'enterprise')),
settings JSONB DEFAULT '{}',
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
trial_ends_at TIMESTAMP
);
-- Users table (multi-tenant)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(50) NOT NULL CHECK (role IN ('admin', 'member', 'viewer')),
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE (tenant_id, email)
);
-- Projects table (multi-tenant)
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,
owner_id INTEGER NOT NULL REFERENCES users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Enable row-level security
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
-- Policy: Users can only see projects from their tenant
CREATE POLICY tenant_isolation_policy ON projects
FOR ALL
USING (tenant_id = current_setting('app.current_tenant_id')::INTEGER);
-- Policy: Admins can see all projects in their tenant
CREATE POLICY admin_policy ON projects
FOR ALL
USING (
tenant_id = current_setting('app.current_tenant_id')::INTEGER
AND EXISTS (
SELECT 1 FROM users
WHERE id = current_setting('app.current_user_id')::INTEGER
AND role = 'admin'
AND tenant_id = projects.tenant_id
)
);
-- Application sets tenant context before queries
-- SET app.current_tenant_id = 123;
-- SET app.current_user_id = 456;Example 3: Audit Logging with Triggers
Scenario: Track all changes to critical tables for compliance and debugging.
-- Generic audit log table
CREATE TABLE audit_log (
id BIGSERIAL PRIMARY KEY,
schema_name VARCHAR(100) NOT NULL,
table_name VARCHAR(100) NOT NULL,
operation VARCHAR(10) NOT NULL CHECK (operation IN ('INSERT', 'UPDATE', 'DELETE')),
record_id INTEGER NOT NULL,
old_data JSONB,
new_data JSONB,
changed_fields TEXT[],
user_id INTEGER,
username VARCHAR(255),
ip_address INET,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create index for efficient queries
CREATE INDEX idx_audit_table ON audit_log(table_name, record_id);
CREATE INDEX idx_audit_timestamp ON audit_log(timestamp DESC);
CREATE INDEX idx_audit_user ON audit_log(user_id);
-- Generic audit trigger function
CREATE OR REPLACE FUNCTION audit_trigger_function()
RETURNS TRIGGER AS $$
DECLARE
old_row JSONB;
new_row JSONB;
changed_fields TEXT[];
BEGIN
-- Convert rows to JSON
IF TG_OP = 'DELETE' THEN
old_row = row_to_json(OLD)::JSONB;
new_row = NULL;
ELSIF TG_OP = 'INSERT' THEN
old_row = NULL;
new_row = row_to_json(NEW)::JSONB;
ELSE -- UPDATE
old_row = row_to_json(OLD)::JSONB;
new_row = row_to_json(NEW)::JSONB;
-- Find changed fields
SELECT ARRAY_AGG(key)
INTO changed_fields
FROM jsonb_each(old_row) o
WHERE o.value IS DISTINCT FROM new_row->o.key;
END IF;
-- Insert audit record
INSERT INTO audit_log (
schema_name,
table_name,
operation,
record_id,
old_data,
new_data,
changed_fields,
username
) VALUES (
TG_TABLE_SCHEMA,
TG_TABLE_NAME,
TG_OP,
COALESCE(NEW.id, OLD.id),
old_row,
new_row,
changed_fields,
current_user
);
IF TG_OP = 'DELETE' THEN
RETURN OLD;
ELSE
RETURN NEW;
END IF;
END;
$$ LANGUAGE plpgsql;
-- Attach audit trigger to tables
CREATE TRIGGER orders_audit_trigger
AFTER INSERT OR UPDATE OR DELETE ON orders
FOR EACH ROW EXECUTE FUNCTION audit_trigger_function();
CREATE TRIGGER products_audit_trigger
AFTER INSERT OR UPDATE OR DELETE ON products
FOR EACH ROW EXECUTE FUNCTION audit_trigger_function();
-- Query audit log
SELECT
table_name,
operation,
record_id,
changed_fields,
username,
timestamp
FROM audit_log
WHERE table_name = 'orders'
AND record_id = 12345
ORDER BY timestamp DESC;Example 4: Hierarchical Data (Categories/Organization Chart)
Scenario: Store and query hierarchical organizational structure efficiently.
-- Organization table using materialized path pattern
CREATE TABLE organizations (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
parent_id INTEGER REFERENCES organizations(id),
path VARCHAR(500) NOT NULL, -- e.g., '1.5.12' for nested hierarchy
level INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Trigger to maintain path and level
CREATE OR REPLACE FUNCTION update_org_path()
RETURNS TRIGGER AS $$
DECLARE
parent_path VARCHAR(500);
parent_level INTEGER;
BEGIN
IF NEW.parent_id IS NULL THEN
-- Root node
NEW.path = NEW.id::VARCHAR;
NEW.level = 0;
ELSE
-- Get parent's path and level
SELECT path, level INTO parent_path, parent_level
FROM organizations
WHERE id = NEW.parent_id;
NEW.path = parent_path || '.' || NEW.id;
NEW.level = parent_level + 1;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER organizations_path_trigger
BEFORE INSERT OR UPDATE ON organizations
FOR EACH ROW EXECUTE FUNCTION update_org_path();
-- Efficient queries
-- Get all descendants of a node
SELECT *
FROM organizations
WHERE path LIKE (
SELECT path || '.%'
FROM organizations
WHERE id = 5
);
-- Get all ancestors of a node
SELECT *
FROM organizations
WHERE id IN (
SELECT unnest(string_to_array(
(SELECT path FROM organizations WHERE id = 12),
'.'
)::INTEGER[])
);
-- Get immediate children
SELECT *
FROM organizations
WHERE parent_id = 5;
-- Get leaf nodes (no children)
SELECT o.*
FROM organizations o
LEFT JOIN organizations c ON c.parent_id = o.id
WHERE c.id IS NULL;
-- Get depth of tree
SELECT MAX(level) as max_depth
FROM organizations;Example 5: Time-Series Data with Partitioning
Scenario: Store sensor data with automatic partitioning by month.
-- Parent table (partitioned by range)
CREATE TABLE sensor_readings (
id BIGSERIAL,
sensor_id INTEGER NOT NULL,
reading_time TIMESTAMP NOT NULL,
temperature NUMERIC(5, 2),
humidity NUMERIC(5, 2),
pressure NUMERIC(7, 2),
metadata JSONB,
PRIMARY KEY (id, reading_time)
) PARTITION BY RANGE (reading_time);
-- Create partitions for each month
CREATE TABLE sensor_readings_2025_01 PARTITION OF sensor_readings
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
CREATE TABLE sensor_readings_2025_02 PARTITION OF sensor_readings
FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');
CREATE TABLE sensor_readings_2025_03 PARTITION OF sensor_readings
FOR VALUES FROM ('2025-03-01') TO ('2025-04-01');
-- Indexes on partitions (created automatically on parent)
CREATE INDEX idx_sensor_readings_sensor_time
ON sensor_readings(sensor_id, reading_time DESC);
CREATE INDEX idx_sensor_readings_metadata
ON sensor_readings USING GIN(metadata);
-- Function to automatically create next month's partition
CREATE OR REPLACE FUNCTION create_next_partition()
RETURNS void AS $$
DECLARE
next_month DATE;
following_month DATE;
partition_name VARCHAR(100);
BEGIN
next_month := date_trunc('month', CURRENT_DATE + INTERVAL '1 month');
following_month := next_month + INTERVAL '1 month';
partition_name := 'sensor_readings_' || to_char(next_month, 'YYYY_MM');
EXECUTE format(
'CREATE TABLE IF NOT EXISTS %I PARTITION OF sensor_readings
FOR VALUES FROM (%L) TO (%L)',
partition_name,
next_month,
following_month
);
END;
$$ LANGUAGE plpgsql;
-- Schedule this function to run monthly (via cron or pg_cron extension)
-- Query benefits from partition pruning
SELECT
sensor_id,
AVG(temperature) as avg_temp,
MAX(temperature) as max_temp,
MIN(temperature) as min_temp
FROM sensor_readings
WHERE reading_time >= '2025-01-15'
AND reading_time < '2025-01-20'
AND sensor_id = 42
GROUP BY sensor_id;
-- Only scans sensor_readings_2025_01 partition!---
PostgreSQL Advanced Queries
Example 6: Window Functions for Analytics
Scenario: Calculate running totals, rankings, and moving averages.
-- Sample data: daily sales
CREATE TABLE daily_sales (
sale_date DATE NOT NULL,
product_id INTEGER NOT NULL,
category VARCHAR(50),
revenue NUMERIC(10, 2),
units_sold INTEGER,
PRIMARY KEY (sale_date, product_id)
);
-- Running total revenue by date
SELECT
sale_date,
revenue,
SUM(revenue) OVER (ORDER BY sale_date) as running_total,
AVG(revenue) OVER (ORDER BY sale_date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) as moving_avg_7day
FROM daily_sales
WHERE product_id = 100
ORDER BY sale_date;
-- Rank products by revenue within each category
SELECT
category,
product_id,
revenue,
RANK() OVER (PARTITION BY category ORDER BY revenue DESC) as rank_in_category,
PERCENT_RANK() OVER (PARTITION BY category ORDER BY revenue DESC) as percentile
FROM daily_sales
WHERE sale_date = '2025-01-15';
-- Year-over-year comparison
SELECT
DATE_TRUNC('month', sale_date) as month,
SUM(revenue) as current_revenue,
LAG(SUM(revenue), 12) OVER (ORDER BY DATE_TRUNC('month', sale_date)) as previous_year_revenue,
(SUM(revenue) - LAG(SUM(revenue), 12) OVER (ORDER BY DATE_TRUNC('month', sale_date)))
/ LAG(SUM(revenue), 12) OVER (ORDER BY DATE_TRUNC('month', sale_date)) * 100 as yoy_growth_pct
FROM daily_sales
GROUP BY DATE_TRUNC('month', sale_date)
ORDER BY month;
-- Cumulative distribution
SELECT
product_id,
revenue,
CUME_DIST() OVER (ORDER BY revenue) as cumulative_distribution,
NTILE(4) OVER (ORDER BY revenue) as quartile
FROM daily_sales
WHERE sale_date = '2025-01-15';Example 7: Common Table Expressions (CTEs) and Recursion
Scenario: Find all related records recursively and perform complex multi-step analysis.
-- Recursive CTE: Find all employees in reporting hierarchy
WITH RECURSIVE employee_hierarchy AS (
-- Base case: start with CEO
SELECT
id,
name,
manager_id,
title,
1 as level,
name::TEXT as path
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- Recursive case: find direct reports
SELECT
e.id,
e.name,
e.manager_id,
e.title,
eh.level + 1,
eh.path || ' > ' || e.name
FROM employees e
INNER JOIN employee_hierarchy eh ON e.manager_id = eh.id
)
SELECT
level,
name,
title,
path
FROM employee_hierarchy
ORDER BY level, name;
-- Multi-step analysis with CTEs
WITH
-- Step 1: Aggregate sales by customer
customer_totals AS (
SELECT
customer_id,
COUNT(*) as order_count,
SUM(total_amount) as total_spent,
MAX(created_at) as last_order_date
FROM orders
WHERE created_at >= CURRENT_DATE - INTERVAL '1 year'
GROUP BY customer_id
),
-- Step 2: Classify customers
customer_segments AS (
SELECT
customer_id,
order_count,
total_spent,
last_order_date,
CASE
WHEN total_spent >= 10000 THEN 'VIP'
WHEN total_spent >= 5000 THEN 'Premium'
WHEN total_spent >= 1000 THEN 'Regular'
ELSE 'Occasional'
END as segment,
CASE
WHEN last_order_date >= CURRENT_DATE - INTERVAL '30 days' THEN 'Active'
WHEN last_order_date >= CURRENT_DATE - INTERVAL '90 days' THEN 'At Risk'
ELSE 'Churned'
END as status
FROM customer_totals
),
-- Step 3: Calculate segment statistics
segment_stats AS (
SELECT
segment,
status,
COUNT(*) as customer_count,
AVG(total_spent) as avg_spent,
SUM(total_spent) as segment_revenue
FROM customer_segments
GROUP BY segment, status
)
-- Final output
SELECT
segment,
status,
customer_count,
ROUND(avg_spent, 2) as avg_spent,
segment_revenue,
ROUND(segment_revenue * 100.0 / SUM(segment_revenue) OVER (), 2) as pct_of_total_revenue
FROM segment_stats
ORDER BY segment_revenue DESC;Example 8: Full-Text Search with Ranking
Scenario: Implement full-text search with relevance ranking.
-- Add tsvector column for full-text search
ALTER TABLE articles
ADD COLUMN search_vector tsvector;
-- Populate search vector from title and content
UPDATE articles
SET search_vector =
setweight(to_tsvector('english', COALESCE(title, '')), 'A') ||
setweight(to_tsvector('english', COALESCE(content, '')), 'B') ||
setweight(to_tsvector('english', COALESCE(tags::text, '')), 'C');
-- Create GIN index for fast full-text search
CREATE INDEX idx_articles_search ON articles USING GIN(search_vector);
-- Trigger to keep search_vector updated
CREATE OR REPLACE FUNCTION articles_search_trigger()
RETURNS TRIGGER AS $$
BEGIN
NEW.search_vector :=
setweight(to_tsvector('english', COALESCE(NEW.title, '')), 'A') ||
setweight(to_tsvector('english', COALESCE(NEW.content, '')), 'B') ||
setweight(to_tsvector('english', COALESCE(NEW.tags::text, '')), 'C');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER articles_search_update
BEFORE INSERT OR UPDATE ON articles
FOR EACH ROW EXECUTE FUNCTION articles_search_trigger();
-- Search with ranking
SELECT
id,
title,
ts_rank(search_vector, query) as rank,
ts_headline('english', content, query, 'MaxWords=50, MinWords=25') as snippet
FROM
articles,
to_tsquery('english', 'database & (design | pattern)') query
WHERE
search_vector @@ query
ORDER BY rank DESC
LIMIT 20;
-- Advanced search with phrase matching
SELECT
title,
ts_rank_cd(search_vector, query) as rank
FROM
articles,
phraseto_tsquery('english', 'database design patterns') query
WHERE
search_vector @@ query
ORDER BY rank DESC;---
PostgreSQL Performance Optimization
Example 9: Index Optimization Strategies
-- Analyze table for query planner
ANALYZE products;
-- Check index usage statistics
SELECT
schemaname,
tablename,
indexname,
idx_scan,
idx_tup_read,
idx_tup_fetch,
pg_size_pretty(pg_relation_size(indexrelid)) as index_size
FROM pg_stat_user_indexes
ORDER BY idx_scan DESC;
-- Find unused indexes (candidates for removal)
SELECT
schemaname || '.' || tablename AS table,
indexname,
pg_size_pretty(pg_relation_size(indexrelid)) AS size,
idx_scan,
idx_tup_read
FROM pg_stat_user_indexes
WHERE idx_scan = 0
AND indexrelname NOT LIKE 'pg_toast%'
AND schemaname = 'public'
ORDER BY pg_relation_size(indexrelid) DESC;
-- Create partial index for active records only
CREATE INDEX idx_active_products
ON products(category_id, name)
WHERE is_active = true AND stock_quantity > 0;
-- Expression index for case-insensitive search
CREATE INDEX idx_products_name_lower
ON products(LOWER(name));
-- Query that uses expression index
SELECT * FROM products
WHERE LOWER(name) = LOWER('Widget Pro');
-- Covering index (index-only scan)
CREATE INDEX idx_orders_covering
ON orders(customer_id, status)
INCLUDE (total_amount, created_at);
-- This query can be served entirely from index
SELECT customer_id, status, total_amount, created_at
FROM orders
WHERE customer_id = 123 AND status = 'completed';Example 10: Query Optimization Patterns
-- BEFORE: Inefficient subquery in SELECT
SELECT
p.name,
(SELECT COUNT(*) FROM order_items oi WHERE oi.product_id = p.id) as times_ordered
FROM products p;
-- AFTER: Use LEFT JOIN with GROUP BY
SELECT
p.name,
COALESCE(COUNT(oi.id), 0) as times_ordered
FROM products p
LEFT JOIN order_items oi ON p.id = oi.product_id
GROUP BY p.id, p.name;
-- BEFORE: IN clause with large subquery
SELECT * FROM orders
WHERE customer_id IN (
SELECT id FROM customers WHERE country = 'US'
);
-- AFTER: Use EXISTS or JOIN
SELECT o.* FROM orders o
WHERE EXISTS (
SELECT 1 FROM customers c
WHERE c.id = o.customer_id AND c.country = 'US'
);
-- Or using JOIN (often faster)
SELECT o.* FROM orders o
INNER JOIN customers c ON o.customer_id = c.id
WHERE c.country = 'US';
-- BEFORE: Function in WHERE clause prevents index usage
SELECT * FROM orders
WHERE EXTRACT(YEAR FROM created_at) = 2025;
-- AFTER: Use range query
SELECT * FROM orders
WHERE created_at >= '2025-01-01'
AND created_at < '2026-01-01';
-- BEFORE: OR conditions that prevent index usage
SELECT * FROM products
WHERE category_id = 10 OR category_id = 20;
-- AFTER: Use IN clause or UNION
SELECT * FROM products
WHERE category_id IN (10, 20);
-- Materialized CTE for reuse (PostgreSQL 12+)
WITH product_stats AS MATERIALIZED (
SELECT
product_id,
COUNT(*) as order_count,
SUM(quantity) as total_quantity
FROM order_items
GROUP BY product_id
)
SELECT
p.name,
ps.order_count,
ps.total_quantity,
ps.total_quantity / ps.order_count as avg_quantity_per_order
FROM products p
INNER JOIN product_stats ps ON p.id = ps.product_id
WHERE ps.order_count > 100
ORDER BY ps.total_quantity DESC;---
MongoDB Schema Design Examples
Example 11: E-Commerce Product Catalog (Polymorphic Pattern)
Scenario: Store different product types with varying attributes in single collection.
// Book product
{
_id: ObjectId("507f1f77bcf86cd799439011"),
type: "book",
name: "Database Design Patterns",
slug: "database-design-patterns",
price: 49.99,
currency: "USD",
in_stock: true,
// Book-specific fields
isbn: "978-0-123456-78-9",
author: "John Smith",
publisher: "Tech Books Inc",
pages: 456,
publication_date: ISODate("2024-06-15"),
language: "English",
format: "Hardcover",
// Common fields
description: "Comprehensive guide to database design...",
images: [
{ url: "https://cdn.example.com/book-cover.jpg", alt: "Book cover", order: 0 },
{ url: "https://cdn.example.com/book-back.jpg", alt: "Back cover", order: 1 }
],
categories: ["Technology", "Databases", "Software Development"],
tags: ["database", "design", "sql", "nosql"],
reviews: {
average: 4.7,
count: 243
},
created_at: ISODate("2024-05-01"),
updated_at: ISODate("2025-01-10")
}
// Electronics product
{
_id: ObjectId("507f1f77bcf86cd799439012"),
type: "electronics",
name: "Wireless Noise-Cancelling Headphones",
slug: "wireless-noise-cancelling-headphones",
price: 299.99,
currency: "USD",
in_stock: true,
// Electronics-specific fields
brand: "AudioTech",
model: "AT-5000",
sku: "AUDIO-HP-5000",
warranty_months: 24,
specifications: {
battery_life: "30 hours",
bluetooth_version: "5.2",
driver_size: "40mm",
frequency_response: "20Hz - 20kHz",
weight: "250g",
colors: ["Black", "Silver", "Blue"]
},
features: [
"Active Noise Cancellation",
"Bluetooth 5.2",
"30-hour battery life",
"Quick charge (5 min = 2 hours)",
"Multipoint connection"
],
// Common fields
description: "Premium wireless headphones with...",
images: [
{ url: "https://cdn.example.com/hp-main.jpg", alt: "Main view", order: 0 },
{ url: "https://cdn.example.com/hp-side.jpg", alt: "Side view", order: 1 },
{ url: "https://cdn.example.com/hp-case.jpg", alt: "With case", order: 2 }
],
categories: ["Electronics", "Audio", "Headphones"],
tags: ["wireless", "bluetooth", "noise-cancelling", "headphones"],
reviews: {
average: 4.5,
count: 892
},
created_at: ISODate("2024-03-20"),
updated_at: ISODate("2025-01-12")
}
// Indexes for polymorphic collection
db.products.createIndex({ type: 1, slug: 1 }, { unique: true })
db.products.createIndex({ categories: 1, price: 1 })
db.products.createIndex({ tags: 1 })
db.products.createIndex({ "reviews.average": -1 })
// Type-specific indexes
db.products.createIndex({ isbn: 1 }, {
unique: true,
partialFilterExpression: { type: "book" }
})
db.products.createIndex({ sku: 1 }, {
unique: true,
partialFilterExpression: { type: "electronics" }
})
// Query books by author
db.products.find({
type: "book",
author: "John Smith"
})
// Query electronics by brand and price range
db.products.find({
type: "electronics",
brand: "AudioTech",
price: { $gte: 200, $lte: 400 }
}).sort({ "reviews.average": -1 })Example 12: Social Media Application (Embedded vs Referenced)
Scenario: Design schema for posts, comments, likes with appropriate embedding strategy.
// Users collection (separate - frequently updated, referenced by many)
{
_id: ObjectId("user1"),
username: "john_doe",
email: "john@example.com",
profile: {
full_name: "John Doe",
avatar_url: "https://cdn.example.com/avatars/john.jpg",
bio: "Software engineer and database enthusiast",
location: "San Francisco, CA",
website: "https://johndoe.dev"
},
stats: {
followers: 1523,
following: 342,
posts: 89
},
created_at: ISODate("2023-01-15"),
last_seen: ISODate("2025-01-18T10:30:00Z")
}
// Posts collection (embed comments, reference user)
{
_id: ObjectId("post1"),
// Author reference with selective denormalization
author: {
id: ObjectId("user1"),
username: "john_doe",
avatar_url: "https://cdn.example.com/avatars/john.jpg"
// Denormalize frequently accessed, rarely changing fields
},
content: {
text: "Just published a comprehensive guide to database design patterns!",
media: [
{
type: "image",
url: "https://cdn.example.com/posts/db-guide.jpg",
width: 1200,
height: 630,
alt: "Database design book cover"
}
],
links: [
{
url: "https://example.com/db-guide",
title: "Database Design Patterns",
description: "Learn advanced patterns...",
image: "https://example.com/preview.jpg"
}
]
},
// Embed comments (one-to-many, bounded, accessed together)
comments: [
{
_id: ObjectId("comment1"),
author: {
id: ObjectId("user2"),
username: "jane_smith",
avatar_url: "https://cdn.example.com/avatars/jane.jpg"
},
text: "This looks great! Can't wait to read it.",
created_at: ISODate("2025-01-15T11:00:00Z"),
likes: 12,
// Nested replies (limited depth)
replies: [
{
_id: ObjectId("reply1"),
author: {
id: ObjectId("user1"),
username: "john_doe",
avatar_url: "https://cdn.example.com/avatars/john.jpg"
},
text: "Thanks! Hope you find it useful.",
created_at: ISODate("2025-01-15T11:30:00Z"),
likes: 3
}
]
},
{
_id: ObjectId("comment2"),
author: {
id: ObjectId("user3"),
username: "bob_wilson",
avatar_url: "https://cdn.example.com/avatars/bob.jpg"
},
text: "Excellent timing! We're redesigning our database.",
created_at: ISODate("2025-01-15T14:20:00Z"),
likes: 8,
replies: []
}
],
// Stats embedded (frequently updated together)
stats: {
views: 3542,
likes: 234,
shares: 45,
comments: 2
},
// Tags for categorization
tags: ["database", "software", "tutorial"],
// Metadata
created_at: ISODate("2025-01-15T10:00:00Z"),
updated_at: ISODate("2025-01-15T14:20:00Z"),
visibility: "public", // public, followers, private
is_pinned: false
}
// Likes collection (separate - unbounded, may be millions)
{
_id: ObjectId("like1"),
post_id: ObjectId("post1"),
user_id: ObjectId("user2"),
created_at: ISODate("2025-01-15T11:00:00Z")
}
// Indexes
db.posts.createIndex({ "author.id": 1, created_at: -1 })
db.posts.createIndex({ created_at: -1 })
db.posts.createIndex({ tags: 1 })
db.posts.createIndex({ "stats.likes": -1 })
db.likes.createIndex({ post_id: 1, user_id: 1 }, { unique: true })
db.likes.createIndex({ user_id: 1, created_at: -1 })
// Query: Get user's feed (posts from people they follow)
db.posts.find({
"author.id": { $in: followingUserIds },
visibility: { $in: ["public", "followers"] }
}).sort({ created_at: -1 }).limit(20)
// Query: Check if user liked a post
db.likes.findOne({
post_id: ObjectId("post1"),
user_id: ObjectId("user2")
})
// Update: Increment like count (atomic operation)
db.posts.updateOne(
{ _id: ObjectId("post1") },
{
$inc: { "stats.likes": 1 },
$set: { updated_at: new Date() }
}
)Example 13: Time-Series Data (Bucketing Pattern)
Scenario: Store IoT sensor data efficiently using the bucket pattern.
// BAD: One document per reading (millions of tiny documents)
{
_id: ObjectId("..."),
sensor_id: "temp_sensor_001",
timestamp: ISODate("2025-01-15T10:00:00Z"),
temperature: 72.5,
humidity: 45.2
}
// GOOD: Bucket pattern - group readings by hour
{
_id: ObjectId("..."),
sensor_id: "temp_sensor_001",
date: ISODate("2025-01-15"),
hour: 10,
// Array of measurements (up to 60 for 1-minute intervals)
measurements: [
{
minute: 0,
timestamp: ISODate("2025-01-15T10:00:00Z"),
temperature: 72.5,
humidity: 45.2,
pressure: 1013.25
},
{
minute: 1,
timestamp: ISODate("2025-01-15T10:01:00Z"),
temperature: 72.6,
humidity: 45.1,
pressure: 1013.30
},
// ... up to 60 measurements
],
// Pre-computed summary statistics
summary: {
count: 60,
temperature: {
min: 71.8,
max: 73.2,
avg: 72.5,
sum: 4350.0
},
humidity: {
min: 44.5,
max: 46.1,
avg: 45.2,
sum: 2712.0
},
pressure: {
min: 1012.80,
max: 1013.90,
avg: 1013.25
}
},
metadata: {
sensor_location: "Building A - Room 101",
sensor_type: "DHT22",
firmware_version: "2.1.3"
}
}
// Indexes for efficient queries
db.sensor_data.createIndex({ sensor_id: 1, date: 1, hour: 1 }, { unique: true })
db.sensor_data.createIndex({ date: 1, hour: 1 })
db.sensor_data.createIndex({ "metadata.sensor_location": 1, date: 1 })
// Query: Get all readings for a sensor on a specific day
db.sensor_data.find({
sensor_id: "temp_sensor_001",
date: ISODate("2025-01-15")
}).sort({ hour: 1 })
// Query: Get hourly averages for a date range
db.sensor_data.aggregate([
{
$match: {
sensor_id: "temp_sensor_001",
date: {
$gte: ISODate("2025-01-01"),
$lte: ISODate("2025-01-31")
}
}
},
{
$project: {
date: 1,
hour: 1,
avg_temperature: "$summary.temperature.avg",
avg_humidity: "$summary.humidity.avg"
}
},
{
$sort: { date: 1, hour: 1 }
}
])
// Insert new measurement (update bucket)
db.sensor_data.updateOne(
{
sensor_id: "temp_sensor_001",
date: ISODate("2025-01-15"),
hour: 10
},
{
$push: {
measurements: {
minute: 30,
timestamp: ISODate("2025-01-15T10:30:00Z"),
temperature: 72.8,
humidity: 45.5,
pressure: 1013.40
}
},
$inc: {
"summary.count": 1,
"summary.temperature.sum": 72.8
},
$min: {
"summary.temperature.min": 72.8
},
$max: {
"summary.temperature.max": 72.8
}
},
{ upsert: true }
)
// Benefits:
// - 60x fewer documents
// - Better index efficiency
// - Pre-computed statistics
// - Easier time-range queries
// - Reduced disk I/O---
MongoDB Aggregation Examples
Example 14: Complex Multi-Stage Aggregation Pipeline
Scenario: Analyze e-commerce sales data with multiple transformations.
db.orders.aggregate([
// Stage 1: Filter to completed orders in date range
{
$match: {
status: "completed",
created_at: {
$gte: ISODate("2025-01-01"),
$lt: ISODate("2025-02-01")
}
}
},
// Stage 2: Unwind order items array
{
$unwind: "$items"
},
// Stage 3: Lookup product details
{
$lookup: {
from: "products",
localField: "items.product_id",
foreignField: "_id",
as: "product_info"
}
},
// Stage 4: Unwind product info (should be single doc)
{
$unwind: "$product_info"
},
// Stage 5: Group by product and calculate metrics
{
$group: {
_id: {
product_id: "$items.product_id",
product_name: "$product_info.name",
category: "$product_info.category"
},
total_quantity: { $sum: "$items.quantity" },
total_revenue: { $sum: "$items.subtotal" },
order_count: { $sum: 1 },
avg_quantity_per_order: { $avg: "$items.quantity" },
avg_price: { $avg: "$items.unit_price" },
customers: { $addToSet: "$customer_id" }
}
},
// Stage 6: Calculate unique customer count
{
$addFields: {
unique_customers: { $size: "$customers" }
}
},
// Stage 7: Group by category for summary
{
$group: {
_id: "$_id.category",
products: {
$push: {
product_id: "$_id.product_id",
product_name: "$_id.product_name",
total_quantity: "$total_quantity",
total_revenue: "$total_revenue",
unique_customers: "$unique_customers"
}
},
category_revenue: { $sum: "$total_revenue" },
category_units: { $sum: "$total_quantity" }
}
},
// Stage 8: Sort products within each category
{
$addFields: {
products: {
$slice: [
{
$sortArray: {
input: "$products",
sortBy: { total_revenue: -1 }
}
},
5 // Top 5 products per category
]
}
}
},
// Stage 9: Sort categories by revenue
{
$sort: { category_revenue: -1 }
},
// Stage 10: Format output
{
$project: {
category: "$_id",
total_revenue: {
$round: ["$category_revenue", 2]
},
total_units: "$category_units",
top_products: "$products",
_id: 0
}
}
])
// Example output:
[
{
"category": "Electronics",
"total_revenue": 125430.50,
"total_units": 1823,
"top_products": [
{
"product_id": ObjectId("..."),
"product_name": "Wireless Headphones",
"total_quantity": 234,
"total_revenue": 69882.00,
"unique_customers": 198
},
// ... 4 more products
]
},
// ... more categories
]Example 15: Aggregation with Facets (Multiple Pipelines)
Scenario: Execute multiple aggregation pipelines in parallel for a dashboard.
db.orders.aggregate([
// Common filter stage
{
$match: {
created_at: {
$gte: ISODate("2025-01-01"),
$lt: ISODate("2025-02-01")
}
}
},
// Facet: Multiple parallel aggregations
{
$facet: {
// Facet 1: Revenue by day
daily_revenue: [
{
$group: {
_id: {
$dateToString: {
format: "%Y-%m-%d",
date: "$created_at"
}
},
revenue: { $sum: "$total_amount" },
orders: { $sum: 1 }
}
},
{
$sort: { _id: 1 }
},
{
$project: {
date: "$_id",
revenue: { $round: ["$revenue", 2] },
orders: 1,
_id: 0
}
}
],
// Facet 2: Top customers
top_customers: [
{
$group: {
_id: "$customer_id",
total_spent: { $sum: "$total_amount" },
order_count: { $sum: 1 }
}
},
{
$sort: { total_spent: -1 }
},
{
$limit: 10
},
{
$lookup: {
from: "customers",
localField: "_id",
foreignField: "_id",
as: "customer"
}
},
{
$unwind: "$customer"
},
{
$project: {
customer_id: "$_id",
customer_name: "$customer.name",
email: "$customer.email",
total_spent: { $round: ["$total_spent", 2] },
order_count: 1,
_id: 0
}
}
],
// Facet 3: Status distribution
status_distribution: [
{
$group: {
_id: "$status",
count: { $sum: 1 },
total_value: { $sum: "$total_amount" }
}
},
{
$project: {
status: "$_id",
count: 1,
total_value: { $round: ["$total_value", 2] },
_id: 0
}
}
],
// Facet 4: Overall statistics
summary: [
{
$group: {
_id: null,
total_orders: { $sum: 1 },
total_revenue: { $sum: "$total_amount" },
avg_order_value: { $avg: "$total_amount" },
unique_customers: { $addToSet: "$customer_id" }
}
},
{
$project: {
total_orders: 1,
total_revenue: { $round: ["$total_revenue", 2] },
avg_order_value: { $round: ["$avg_order_value", 2] },
unique_customers: { $size: "$unique_customers" },
_id: 0
}
}
]
}
}
])---
MongoDB Sharding Examples
Example 16: Sharding Setup and Configuration
Scenario: Set up a sharded cluster with zone-aware sharding for geographic distribution.
// Step 1: Enable sharding on database
sh.enableSharding("ecommerce")
// Step 2: Choose appropriate shard key
// Option A: Hashed shard key for even distribution
sh.shardCollection("ecommerce.orders", { _id: "hashed" })
// Option B: Range-based compound key for query isolation
sh.shardCollection("ecommerce.users", {
region: 1,
user_id: 1
})
// Option C: Hashed compound key
sh.shardCollection("ecommerce.events", {
user_id: "hashed",
timestamp: 1
})
// Step 3: Create zones for geographic sharding
sh.addShardToZone("shard-us-east", "US-EAST")
sh.addShardToZone("shard-us-west", "US-WEST")
sh.addShardToZone("shard-eu", "EU")
sh.addShardToZone("shard-apac", "APAC")
// Step 4: Define zone ranges
sh.updateZoneKeyRange(
"ecommerce.users",
{ region: "US", user_id: MinKey },
{ region: "US", user_id: MaxKey },
"US-EAST"
)
sh.updateZoneKeyRange(
"ecommerce.users",
{ region: "EU", user_id: MinKey },
{ region: "EU", user_id: MaxKey },
"EU"
)
sh.updateZoneKeyRange(
"ecommerce.users",
{ region: "APAC", user_id: MinKey },
{ region: "APAC", user_id: MaxKey },
"APAC"
)
// Step 5: Monitor sharding status
sh.status()
// Step 6: Check chunk distribution
db.getSiblingDB("config").chunks.aggregate([
{
$group: {
_id: { ns: "$ns", shard: "$shard" },
count: { $sum: 1 }
}
},
{
$sort: { "_id.ns": 1, "_id.shard": 1 }
}
])
// Step 7: Enable balancer (if disabled)
sh.startBalancer()
sh.getBalancerState()
// Step 8: Check for jumbo chunks
db.getSiblingDB("config").chunks.find({ jumbo: true })
// Example: Targeted query (routes to single shard)
db.users.find({
region: "US",
email: "user@example.com"
})
// Routes only to US-EAST shard
// Example: Scatter-gather query (routes to all shards)
db.users.find({
email: "user@example.com"
})
// Routes to all shards (no shard key in query)---
Cross-Database Patterns
Example 17: Polyglot Persistence Pattern
Scenario: Use PostgreSQL for transactional data and MongoDB for product catalog.
// Application Architecture:
// - PostgreSQL: Orders, payments, inventory (ACID transactions)
// - MongoDB: Product catalog, user sessions, logs (flexible schema)
// - Sync critical data between systems
// PostgreSQL: Orders table
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
order_number VARCHAR(50) UNIQUE NOT NULL,
customer_id INTEGER NOT NULL,
status VARCHAR(20) NOT NULL,
total_amount NUMERIC(10, 2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE order_items (
id SERIAL PRIMARY KEY,
order_id INTEGER NOT NULL REFERENCES orders(id),
product_id VARCHAR(50) NOT NULL, -- References MongoDB product_id
quantity INTEGER NOT NULL,
unit_price NUMERIC(10, 2) NOT NULL,
subtotal NUMERIC(10, 2) NOT NULL
);
// MongoDB: Product catalog
{
_id: "PROD-12345",
name: "Wireless Mouse",
description: "Ergonomic wireless mouse...",
price: 29.99,
categories: ["Electronics", "Computer Accessories"],
specifications: {
battery_life: "12 months",
connectivity: "2.4GHz wireless",
dpi: "1600",
buttons: 5
},
images: [...],
inventory: {
available: 150,
reserved: 23
},
seo: {
meta_title: "...",
meta_description: "...",
keywords: [...]
}
}
// Synchronization pattern:
// 1. Application creates order in PostgreSQL (transactional)
// 2. Application reserves inventory in MongoDB
// 3. If either fails, rollback both (saga pattern)
// Application code (pseudo-code):
async function createOrder(orderData) {
const pgClient = await pgPool.connect()
const mongoSession = mongoClient.startSession()
try {
// Start PostgreSQL transaction
await pgClient.query('BEGIN')
// Start MongoDB transaction
mongoSession.startTransaction()
// 1. Create order in PostgreSQL
const orderResult = await pgClient.query(
'INSERT INTO orders (customer_id, total_amount) VALUES ($1, $2) RETURNING id',
[orderData.customer_id, orderData.total]
)
const orderId = orderResult.rows[0].id
// 2. Insert order items
for (const item of orderData.items) {
await pgClient.query(
'INSERT INTO order_items (order_id, product_id, quantity, unit_price, subtotal) VALUES ($1, $2, $3, $4, $5)',
[orderId, item.product_id, item.quantity, item.price, item.subtotal]
)
}
// 3. Reserve inventory in MongoDB
for (const item of orderData.items) {
const result = await db.products.updateOne(
{
_id: item.product_id,
"inventory.available": { $gte: item.quantity }
},
{
$inc: {
"inventory.available": -item.quantity,
"inventory.reserved": item.quantity
}
},
{ session: mongoSession }
)
if (result.modifiedCount === 0) {
throw new Error(`Insufficient inventory for product ${item.product_id}`)
}
}
// Commit both transactions
await pgClient.query('COMMIT')
await mongoSession.commitTransaction()
return { success: true, orderId }
} catch (error) {
// Rollback both transactions
await pgClient.query('ROLLBACK')
await mongoSession.abortTransaction()
throw error
} finally {
pgClient.release()
mongoSession.endSession()
}
}---
Real-World Use Cases
Example 18: Multi-Tenant SaaS Application
Scenario: Design database for a multi-tenant project management SaaS.
PostgreSQL Approach (Row-Level Security):
-- Single database, row-level isolation
CREATE TABLE tenants (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
subdomain VARCHAR(100) UNIQUE NOT NULL,
plan VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE projects (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL REFERENCES tenants(id),
name VARCHAR(255) NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE tasks (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL REFERENCES tenants(id),
project_id INTEGER NOT NULL REFERENCES projects(id),
title VARCHAR(255) NOT NULL,
status VARCHAR(50),
assignee_id INTEGER,
due_date DATE
);
-- Enable row-level security
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
-- Policy: Users can only access their tenant's data
CREATE POLICY tenant_isolation ON projects
FOR ALL
USING (tenant_id = current_setting('app.current_tenant')::INTEGER);
CREATE POLICY tenant_isolation ON tasks
FOR ALL
USING (tenant_id = current_setting('app.current_tenant')::INTEGER);
-- Application sets tenant context
SET app.current_tenant = 42;
SELECT * FROM projects; -- Only sees tenant 42's projectsMongoDB Approach (Database-per-Tenant):
// Separate database for each tenant
// tenant_42 database
{
_id: ObjectId("..."),
name: "Website Redesign",
description: "Redesign company website",
owner_id: ObjectId("..."),
members: [
{
user_id: ObjectId("..."),
role: "admin",
joined_at: ISODate("2025-01-01")
},
{
user_id: ObjectId("..."),
role: "member",
joined_at: ISODate("2025-01-05")
}
],
tasks: [
{
_id: ObjectId("..."),
title: "Create wireframes",
status: "completed",
assignee_id: ObjectId("..."),
due_date: ISODate("2025-01-15"),
completed_at: ISODate("2025-01-14")
},
{
_id: ObjectId("..."),
title: "Design homepage mockup",
status: "in_progress",
assignee_id: ObjectId("..."),
due_date: ISODate("2025-01-20")
}
],
created_at: ISODate("2024-12-01"),
updated_at: ISODate("2025-01-18")
}
// Application routing
function getTenantDatabase(tenantId) {
return mongoClient.db(`tenant_${tenantId}`)
}
const tenantDb = getTenantDatabase(42)
const projects = await tenantDb.collection('projects').find().toArray()Example 19: Real-Time Analytics Dashboard
Scenario: Build real-time analytics for e-commerce platform.
// MongoDB: Pre-aggregated metrics collection
{
_id: ObjectId("..."),
metric_type: "daily_sales",
date: ISODate("2025-01-15"),
// Pre-computed hourly breakdown
hourly_data: [
{ hour: 0, orders: 23, revenue: 1245.50, customers: 18 },
{ hour: 1, orders: 18, revenue: 987.25, customers: 15 },
// ... 24 hours
],
// Overall daily totals
totals: {
orders: 542,
revenue: 28456.75,
unique_customers: 387,
avg_order_value: 52.48,
items_sold: 1234
},
// Top products
top_products: [
{
product_id: "PROD-123",
name: "Wireless Mouse",
quantity: 89,
revenue: 2581.11
},
// ... top 10
],
// Category breakdown
by_category: [
{
category: "Electronics",
orders: 234,
revenue: 15678.50
},
// ... all categories
],
computed_at: ISODate("2025-01-16T00:05:00Z")
}
// Aggregation pipeline to compute metrics (run hourly/daily)
db.orders.aggregate([
{
$match: {
created_at: {
$gte: ISODate("2025-01-15T00:00:00Z"),
$lt: ISODate("2025-01-16T00:00:00Z")
},
status: "completed"
}
},
{
$facet: {
// Hourly breakdown
hourly: [
{
$group: {
_id: { $hour: "$created_at" },
orders: { $sum: 1 },
revenue: { $sum: "$total_amount" },
customers: { $addToSet: "$customer_id" }
}
},
{
$project: {
hour: "$_id",
orders: 1,
revenue: 1,
customers: { $size: "$customers" },
_id: 0
}
},
{
$sort: { hour: 1 }
}
],
// Overall totals
totals: [
{
$group: {
_id: null,
orders: { $sum: 1 },
revenue: { $sum: "$total_amount" },
customers: { $addToSet: "$customer_id" }
}
},
{
$project: {
orders: 1,
revenue: 1,
unique_customers: { $size: "$customers" },
avg_order_value: { $divide: ["$revenue", "$orders"] },
_id: 0
}
}
],
// Top products
top_products: [
{ $unwind: "$items" },
{
$group: {
_id: "$items.product_id",
quantity: { $sum: "$items.quantity" },
revenue: { $sum: "$items.subtotal" }
}
},
{
$lookup: {
from: "products",
localField: "_id",
foreignField: "_id",
as: "product"
}
},
{ $unwind: "$product" },
{
$project: {
product_id: "$_id",
name: "$product.name",
quantity: 1,
revenue: 1,
_id: 0
}
},
{ $sort: { revenue: -1 } },
{ $limit: 10 }
]
}
},
// Merge facets and store
{
$project: {
metric_type: { $literal: "daily_sales" },
date: { $literal: ISODate("2025-01-15") },
hourly_data: "$hourly",
totals: { $arrayElemAt: ["$totals", 0] },
top_products: "$top_products",
computed_at: { $literal: new Date() }
}
},
// Output to metrics collection
{
$merge: {
into: "daily_metrics",
whenMatched: "replace",
whenNotMatched: "insert"
}
}
])
// Dashboard query (fast - pre-computed)
db.daily_metrics.find({
metric_type: "daily_sales",
date: {
$gte: ISODate("2025-01-01"),
$lte: ISODate("2025-01-31")
}
}).sort({ date: 1 })Example 20: Event Sourcing Pattern
Scenario: Implement event sourcing for order management.
PostgreSQL Event Store:
-- Events table (append-only)
CREATE TABLE order_events (
id BIGSERIAL PRIMARY KEY,
aggregate_id UUID NOT NULL,
aggregate_type VARCHAR(50) NOT NULL,
event_type VARCHAR(100) NOT NULL,
event_data JSONB NOT NULL,
metadata JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
version INTEGER NOT NULL,
UNIQUE (aggregate_id, version)
);
CREATE INDEX idx_events_aggregate ON order_events(aggregate_id, version);
CREATE INDEX idx_events_type ON order_events(event_type, created_at);
-- Snapshots table (for performance)
CREATE TABLE order_snapshots (
aggregate_id UUID PRIMARY KEY,
state JSONB NOT NULL,
version INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Example: Order lifecycle events
INSERT INTO order_events (aggregate_id, aggregate_type, event_type, event_data, version)
VALUES
-- Event 1: Order created
(
'550e8400-e29b-41d4-a716-446655440000',
'Order',
'OrderCreated',
'{"customer_id": 123, "items": [...], "total": 99.99}'::jsonb,
1
),
-- Event 2: Payment received
(
'550e8400-e29b-41d4-a716-446655440000',
'Order',
'PaymentReceived',
'{"payment_method": "credit_card", "amount": 99.99, "transaction_id": "TXN-123"}'::jsonb,
2
),
-- Event 3: Order shipped
(
'550e8400-e29b-41d4-a716-446655440000',
'Order',
'OrderShipped',
'{"tracking_number": "TRK-456", "carrier": "UPS", "shipped_at": "2025-01-16T10:00:00Z"}'::jsonb,
3
);
-- Rebuild order state from events
WITH order_events_sorted AS (
SELECT event_type, event_data, created_at
FROM order_events
WHERE aggregate_id = '550e8400-e29b-41d4-a716-446655440000'
ORDER BY version
)
SELECT
jsonb_agg(
jsonb_build_object(
'event', event_type,
'data', event_data,
'timestamp', created_at
)
ORDER BY created_at
) as event_history
FROM order_events_sorted;
-- Create snapshot for performance
INSERT INTO order_snapshots (aggregate_id, state, version)
VALUES (
'550e8400-e29b-41d4-a716-446655440000',
'{
"customer_id": 123,
"status": "shipped",
"total": 99.99,
"payment_status": "paid",
"tracking_number": "TRK-456"
}'::jsonb,
3
)
ON CONFLICT (aggregate_id)
DO UPDATE SET
state = EXCLUDED.state,
version = EXCLUDED.version,
created_at = CURRENT_TIMESTAMP;---
Total Examples: 20+ comprehensive, production-ready examples covering:
- PostgreSQL schema design, advanced queries, performance tuning
- MongoDB document modeling, aggregation, sharding
- Cross-database patterns and real-world use cases
- Event sourcing, multi-tenancy, analytics, time-series data
These examples integrate concepts from the Context7 documentation and demonstrate practical application of database management patterns.
Database Management Patterns
Comprehensive skill for mastering database design, optimization, and management across PostgreSQL (SQL) and MongoDB (NoSQL) systems.
Overview
This skill provides production-ready patterns and best practices for:
- Schema Design: Normalization vs denormalization, relational vs document models
- Indexing: B-tree, hash, compound, partial, text, and geospatial indexes
- Transactions: ACID guarantees, isolation levels, multi-document operations
- Replication: Primary-standby, replica sets, failover strategies
- Sharding: Horizontal scaling, shard key selection, zone sharding
- Performance: Query optimization, explain plans, connection pooling
- Operations: Monitoring, troubleshooting, maintenance
Quick Reference
Database Selection Guide
| Requirement | PostgreSQL | MongoDB |
|---|---|---|
| Strong ACID transactions | ✓ Excellent | ⚠️ Limited (single replica set) |
| Complex JOINs | ✓ Excellent | ⚠️ $lookup (expensive) |
| Flexible schema | ⚠️ Requires migrations | ✓ Native support |
| Horizontal scaling | ⚠️ Manual sharding | ✓ Built-in sharding |
| JSON/Document storage | ✓ JSONB type | ✓ Native BSON |
| Nested hierarchies | ⚠️ Requires CTEs/recursion | ✓ Natural fit |
| Aggregation pipelines | ✓ Window functions, CTEs | ✓ Aggregation framework |
| Full-text search | ✓ tsvector/GIN | ✓ Text indexes |
| Geospatial queries | ✓ PostGIS extension | ✓ Native 2dsphere |
| Maturity & tooling | ✓ Very mature | ✓ Mature |
When to Use PostgreSQL
✅ Ideal for:
- Financial applications requiring strict consistency
- Complex data relationships with frequent JOINs
- Well-defined schemas that change infrequently
- Applications requiring advanced SQL features
- Strong data integrity guarantees (foreign keys, constraints)
- Multi-step transactions across multiple tables
- Regulatory compliance requiring audit trails
Example use cases:
- E-commerce order processing
- Banking and financial systems
- Inventory management
- Enterprise resource planning (ERP)
- Customer relationship management (CRM)
When to Use MongoDB
✅ Ideal for:
- Applications with evolving/flexible schemas
- Rapid prototyping and agile development
- Content management systems with varied document types
- Real-time analytics and event logging
- Mobile and web apps with JSON APIs
- Hierarchical or deeply nested data
- Applications requiring horizontal scalability
Example use cases:
- Content management systems (CMS)
- Mobile app backends
- Real-time analytics dashboards
- Product catalogs with varied attributes
- Session storage and caching
- Internet of Things (IoT) event data
Schema Design Decision Framework
PostgreSQL Schema Design
Start: What is your data structure?
│
├─ Well-defined, stable relationships?
│ └─ Use normalized tables with foreign keys
│
├─ Need for data integrity constraints?
│ └─ Use CHECK constraints, triggers, foreign keys
│
├─ Hierarchical data (categories, org charts)?
│ ├─ Shallow hierarchy → Adjacency list (parent_id)
│ └─ Deep hierarchy → Materialized path or closure table
│
├─ Temporal data (historical tracking)?
│ └─ Use temporal tables or audit log pattern
│
└─ JSON data within relational structure?
└─ Use JSONB columns for flexible attributesMongoDB Schema Design
Start: What are your access patterns?
│
├─ Data always accessed together?
│ └─ Embed documents (denormalize)
│
├─ Data accessed independently?
│ └─ Reference documents (normalize)
│
├─ One-to-few relationship (< 100 items)?
│ └─ Embed array in parent document
│
├─ One-to-many relationship (100-10,000 items)?
│ └─ Store parent reference in child documents
│
├─ One-to-squillions (unbounded)?
│ └─ Store child reference array in parent (paginate)
│
└─ Many-to-many relationship?
└─ Use intermediate collection with referencesCore Indexing Strategies
PostgreSQL Index Types
| Index Type | Use Case | Example |
|---|---|---|
| B-tree (default) | Equality, range, sorting | CREATE INDEX idx_email ON users(email) |
| Hash | Equality only | CREATE INDEX USING HASH ON sessions(token) |
| GIN | Full-text, JSONB, arrays | CREATE INDEX USING GIN ON docs(content_tsv) |
| GiST | Geometric, full-text | CREATE INDEX USING GIST ON locations(geom) |
| BRIN | Very large tables, sorted | CREATE INDEX USING BRIN ON logs(timestamp) |
| Partial | Subset of rows | CREATE INDEX ON users(email) WHERE active=true |
| Expression | Computed values | CREATE INDEX ON users(LOWER(email)) |
MongoDB Index Types
| Index Type | Use Case | Example |
|---|---|---|
| Single field | Simple queries | db.users.createIndex({ email: 1 }) |
| Compound | Multiple field queries | db.posts.createIndex({ author: 1, date: -1 }) |
| Multikey | Array fields | db.posts.createIndex({ tags: 1 }) |
| Text | Full-text search | db.articles.createIndex({ content: "text" }) |
| Geospatial | Location queries | db.places.createIndex({ loc: "2dsphere" }) |
| Hashed | Even distribution (sharding) | db.users.createIndex({ _id: "hashed" }) |
| Wildcard | Flexible schema fields | db.products.createIndex({ "$**": 1 }) |
ESR Rule for Compound Indexes (MongoDB)
Optimal compound index column order:
1. Equality filters first (exact matches) 2. Sort fields second 3. Range filters last
Example:
// Query pattern
db.orders.find({
status: "completed", // Equality
total: { $gte: 100 } // Range
}).sort({ created_at: -1 }) // Sort
// Optimal index order
db.orders.createIndex({
status: 1, // 1. Equality
created_at: -1, // 2. Sort
total: 1 // 3. Range
})Transaction Patterns
PostgreSQL Isolation Levels
| Level | Dirty Read | Non-Repeatable Read | Phantom Read | Performance |
|---|---|---|---|---|
| Read Uncommitted | Possible | Possible | Possible | Fastest |
| Read Committed (default) | Prevented | Possible | Possible | Fast |
| Repeatable Read | Prevented | Prevented | Possible | Slower |
| Serializable | Prevented | Prevented | Prevented | Slowest |
Common scenarios:
- Read Committed: Most web applications (default, good balance)
- Repeatable Read: Reports requiring consistent snapshots
- Serializable: Financial transactions requiring strict ordering
MongoDB Read/Write Concerns
Write Concern Levels:
w: 1- Acknowledge after writing to primary (fast, less durable)w: "majority"- Acknowledge after majority of replica set (slower, durable)j: true- Wait for journal write (durability guarantee)
Read Concern Levels:
local- Return latest data from node (fastest, may read rolled-back data)majority- Return data acknowledged by majority (slower, consistent)linearizable- Strongest consistency (slowest, serializable)
Recommendation:
- Critical data (payments, orders):
{ w: "majority", j: true } - Regular data:
{ w: 1 } - Analytics/reporting: Read from secondaries with
readPreference: "secondary"
Performance Optimization Checklist
PostgreSQL Performance
- [ ] Enable and configure
pg_stat_statementsextension - [ ] Set appropriate
shared_buffers(25% of RAM) - [ ] Configure
effective_cache_size(50-75% of RAM) - [ ] Enable autovacuum with appropriate thresholds
- [ ] Create indexes on foreign keys
- [ ] Use
EXPLAIN ANALYZEfor slow queries - [ ] Implement connection pooling (PgBouncer)
- [ ] Monitor long-running queries
- [ ] Partition large tables (>10M rows)
- [ ] Use prepared statements in application code
MongoDB Performance
- [ ] Create indexes matching query patterns
- [ ] Use covered queries when possible
- [ ] Enable profiling for slow queries
- [ ] Monitor index usage with
$indexStats - [ ] Choose appropriate shard key (high cardinality, even distribution)
- [ ] Configure replica set with appropriate read preferences
- [ ] Use projection to limit returned fields
- [ ] Batch operations when possible
- [ ] Monitor replication lag
- [ ] Set appropriate connection pool size
Replication & High Availability
PostgreSQL Replication Setup
Streaming Replication (Primary-Standby):
Primary Server
│
├─→ Standby 1 (synchronous)
├─→ Standby 2 (asynchronous)
└─→ Standby 3 (asynchronous)Benefits:
- Read scaling (read queries from standbys)
- High availability (automatic failover)
- Zero data loss (synchronous replication)
- Point-in-time recovery
Configuration:
- Synchronous replication: Zero data loss, slower writes
- Asynchronous replication: Faster writes, possible data loss on failure
MongoDB Replica Set
Typical 3-Node Replica Set:
Primary (writes)
│
├─→ Secondary 1 (reads, failover)
└─→ Secondary 2 (reads, failover)Benefits:
- Automatic failover (election in ~12 seconds)
- Read scaling (read from secondaries)
- Data redundancy
- Rolling upgrades without downtime
Topology Options:
- 3 data-bearing members (standard)
- 2 data + 1 arbiter (voting only, saves storage)
- 5+ members for critical systems
- Hidden members for analytics
- Delayed members for disaster recovery
Sharding Strategies
MongoDB Sharding Architectures
Range-Based Sharding:
Shard Key: timestamp
─────────────────────────────
Shard 1: 2020-01-01 to 2022-12-31
Shard 2: 2023-01-01 to 2024-12-31
Shard 3: 2025-01-01 to current- Pros: Range queries target specific shards
- Cons: Uneven distribution (recent data gets all writes)
Hashed Sharding:
Shard Key: _id (hashed)
─────────────────────────────
Shard 1: hash values 0-3333...
Shard 2: hash values 3333...-6666...
Shard 3: hash values 6666...-9999...- Pros: Even distribution
- Cons: Range queries scatter to all shards
Zone/Tag-Aware Sharding:
Geographic distribution:
─────────────────────────────
US Shard: { region: "US" }
EU Shard: { region: "EU" }
APAC Shard: { region: "APAC" }- Pros: Data locality, compliance (GDPR)
- Cons: Requires careful capacity planning
PostgreSQL Partitioning
Horizontal Partitioning (Sharding):
- Use Citus extension for distributed PostgreSQL
- Application-level sharding with multiple databases
- Foreign Data Wrappers (FDW) for federated queries
Vertical Partitioning:
- Split large tables into frequently/rarely accessed columns
- Store BLOBs in separate table
Monitoring and Observability
Key PostgreSQL Metrics
| Metric | Target | Command |
|---|---|---|
| Cache hit ratio | > 99% | SELECT * FROM pg_stat_database |
| Active connections | < max_connections | SELECT count(*) FROM pg_stat_activity |
| Deadlocks | Minimal | SELECT deadlocks FROM pg_stat_database |
| Replication lag | < 1 second | SELECT pg_wal_lsn_diff(...) |
| Bloat | < 20% | pgstattuple extension |
| Slow queries | None > 1s | pg_stat_statements |
Key MongoDB Metrics
| Metric | Target | Command |
|---|---|---|
| Replication lag | < 1 second | rs.printSecondaryReplicationInfo() |
| Index efficiency | 1:1 ratio | docs examined / docs returned |
| Connection count | < pool max | db.serverStatus().connections |
| Queue depth | < 10 | db.serverStatus().globalLock.currentQueue |
| Memory usage | < 80% | db.serverStatus().mem |
| Chunk distribution | Even | sh.status() |
Common Design Patterns
PostgreSQL Patterns
1. Audit Trail: Triggers + audit table for change history 2. Soft Delete: deleted_at column instead of DELETE 3. Optimistic Locking: Version column to detect concurrent updates 4. Event Sourcing: Immutable event log, rebuild state 5. Materialized View: Pre-computed aggregations for fast reads 6. Temporal Tables: System-versioned tables for time travel 7. Queue Pattern: FOR UPDATE SKIP LOCKED for job queues
MongoDB Patterns
1. Embedded: Store related data in single document 2. Bucketing: Group time-series into periodic buckets 3. Computed: Store pre-aggregated values 4. Subset: Store frequently accessed fields, reference full data 5. Extended Reference: Embed key fields, reference for full data 6. Approximation: Store statistical approximations for large sets 7. Outlier: Separate handling for edge cases (e.g., popular items)
Migration Strategies
SQL to NoSQL Migration
When to migrate:
- Schema changes too frequent/expensive
- Need horizontal scalability beyond single server
- Document-oriented data is natural fit
- Application primarily JSON/REST API
Approach: 1. Analyze access patterns: Understand how data is queried 2. Design document model: Embed vs reference decisions 3. Dual-write period: Write to both databases 4. Gradual read migration: Move reads collection by collection 5. Deprecate old system: After validation period
NoSQL to SQL Migration
When to migrate:
- Need for complex JOINs and relational queries
- Strong consistency requirements
- Schema has stabilized
- Advanced SQL features needed (window functions, CTEs)
Approach: 1. Normalize schema: Break documents into related tables 2. Create foreign keys: Establish relationships 3. Migrate data: Write ETL scripts 4. Validate integrity: Check constraints and references 5. Update application: Modify queries and ORM models
Security Best Practices
PostgreSQL Security
- ✅ Use SSL/TLS for all connections
- ✅ Implement row-level security (RLS) for multi-tenant apps
- ✅ Grant minimum necessary privileges (principle of least privilege)
- ✅ Use parameterized queries (prevent SQL injection)
- ✅ Enable audit logging for sensitive tables
- ✅ Rotate passwords regularly
- ✅ Encrypt sensitive columns (pgcrypto extension)
- ✅ Backup encryption for WAL archives
MongoDB Security
- ✅ Enable authentication and authorization
- ✅ Use TLS/SSL for client and replica set connections
- ✅ Implement role-based access control (RBAC)
- ✅ Enable audit logging (Enterprise feature)
- ✅ Encrypt data at rest
- ✅ Network isolation (private networks, VPNs)
- ✅ Regular backups with encryption
- ✅ Disable JavaScript execution if not needed
Troubleshooting Quick Reference
PostgreSQL Issues
| Symptom | Likely Cause | Solution |
|---|---|---|
| Slow queries | Missing index | Run EXPLAIN ANALYZE, add index |
| High CPU | Expensive queries | Check pg_stat_statements, optimize |
| Connection errors | Max connections | Increase max_connections, use pooling |
| Deadlocks | Lock ordering | Review transaction logic |
| Bloat | No vacuuming | Enable autovacuum, run manual VACUUM |
| Replication lag | Network/load | Check bandwidth, reduce write load |
MongoDB Issues
| Symptom | Likely Cause | Solution |
|---|---|---|
| Slow queries | Missing index | Run .explain(), create index |
| High memory | Working set > RAM | Add RAM, optimize queries, scale out |
| Write conflicts | Hotspot shard key | Choose better shard key |
| Replication lag | Oplog too small | Increase oplog size |
| Uneven sharding | Poor shard key | Re-shard with better key |
| Jumbo chunks | Indivisible data | Refine shard key, manual split |
Resources & Tools
PostgreSQL Tools
- pgAdmin: GUI administration
- psql: Command-line client
- pg_stat_statements: Query performance analysis
- PgBouncer: Connection pooling
- Patroni: High availability and failover
- Barman: Backup and recovery
- PostGIS: Geospatial extension
MongoDB Tools
- MongoDB Compass: GUI explorer
- mongosh: Modern shell
- MongoDB Atlas: Managed cloud service
- mongo-express: Web-based admin
- Percona Monitoring: Performance monitoring
- mongodump/mongorestore: Backup utilities
---
Quick Links:
- Full SKILL.md Documentation
- Detailed Examples
- PostgreSQL Docs: https://www.postgresql.org/docs/
- MongoDB Docs: https://docs.mongodb.com/