Choosing SQL vs NoSQL
The Fundamental Tradeoff
Every system design interview eventually requires a database choice. The decision between SQL and NoSQL is not about which is "better" — it is about which fits your specific requirements.
Decision Matrix
| Factor | SQL (Relational) | NoSQL (Non-relational) |
|---|---|---|
| ACID Compliance | Full ACID guarantees | BASE model (eventual consistency) |
| Schema | Fixed, predefined schema | Flexible, dynamic schema |
| Relationships | Native JOIN support | Denormalized, embedded documents |
| Scaling | Vertical (scale up) | Horizontal (scale out) |
| Query Language | Standardized SQL | Varies (MQL, CQL, DynamoDB API) |
| Data Structure | Tables with rows/columns | Documents, key-value, wide-column, graph |
When to Choose SQL
Choose SQL when:
- Data integrity is critical: Financial transactions, inventory management, order processing
- Complex relationships exist: Multiple entities with many-to-many relationships
- Ad-hoc queries are needed: Business analysts need flexible reporting
- ACID compliance is non-negotiable: Banking, payment systems
When to Choose NoSQL
Choose NoSQL when:
- Massive scale is required: Millions of reads/writes per second
- Schema flexibility matters: Data structure evolves rapidly
- Low latency at scale: Real-time bidding, gaming leaderboards
- Simple access patterns: Key-value lookups, document retrieval
Database Technology Selection Guide
PostgreSQL
- Best for: Complex queries, ACID transactions, geospatial data
- Use when: E-commerce, ERP systems, content management
- Strengths: JSON support, full ACID, rich indexing, extensions ecosystem
- Example: Order management system with complex joins across users, products, and orders
MySQL
- Best for: Read-heavy workloads, web applications, replication
- Use when: CMS, e-commerce platforms, SaaS applications
- Strengths: Mature ecosystem, fast reads, widespread tooling
- Example: Product catalog with frequent reads and occasional writes
MongoDB
- Best for: Document storage, rapid prototyping, content management
- Use when: User profiles, product catalogs with varying attributes, event logging
- Strengths: Flexible schema, horizontal scaling, rich query language
- Example: Social media posts with varying metadata, comments, and media
DynamoDB
- Best for: Key-value access patterns, serverless applications, gaming
- Use when: Session storage, shopping carts, user state, IoT data
- Strengths: Single-digit millisecond latency, auto-scaling, fully managed
- Example: Real-time gaming leaderboard with millions of concurrent users
Cassandra
- Best for: Write-heavy workloads, time-series data, high availability
- Use when: IoT sensor data, event logging, audit trails
- Strengths: Linear scalability, no single point of failure, tunable consistency
- Example: Smart home device telemetry collecting millions of readings per second
Redis
- Best for: Caching, session management, real-time analytics
- Use when: Rate limiting, leaderboards, pub/sub messaging
- Strengths: In-memory speed, data structures, persistence options
- Example: Session cache for authentication tokens and user preferences
Real-World Database Choices by System
E-commerce Platform
- Product catalog: MongoDB (flexible attributes per product category)
- Orders/transactions: PostgreSQL (ACID for financial data)
- Session/cache: Redis (fast session retrieval)
- Search: Elasticsearch (full-text product search)
Chat Application
- Messages: Cassandra (write-heavy, time-series pattern)
- User profiles: PostgreSQL (relational, structured)
- Online status: Redis (real-time presence tracking)
- Message search: Elasticsearch (full-text search across messages)
Analytics Pipeline
- Raw events: Cassandra or DynamoDB (high write throughput)
- Aggregated data: PostgreSQL (complex analytical queries)
- Real-time dashboards: Redis (precomputed aggregations)
- Historical data: Columnar stores like Redshift or BigQuery
Schema Design for Scale
Vertical Partitioning
Vertical partitioning splits a table by columns, placing different column groups in separate tables or databases. This reduces I/O by only loading relevant data.
When to use:
- Tables with many columns but queries typically access only a subset
- Large text/blob columns mixed with frequently accessed data
- Security requirements where different teams access different columns
Example: User table vertical partitioning
Original table:
users(user_id, name, email, password_hash, bio, profile_image_url, preferences_json)
Partitioned into:
user_auth(user_id, email, password_hash) -- Auth service accesses
user_profile(user_id, name, bio, profile_image_url) -- Profile service accesses
user_preferences(user_id, preferences_json) -- Preference service accesses
Horizontal Partitioning (Sharding)
Horizontal partitioning splits rows across multiple database instances. Each shard contains a subset of the total data.
Sharding Strategies:
Hash-based Sharding
shard_id = hash(user_id) % number_of_shards
- Pros: Even data distribution, simple implementation
- Cons: Difficult to add/remove shards, range queries require scatter-gather
- Use when: Access patterns are uniform, data grows linearly
Range-based Sharding
if (user_id < 1000000) -> shard_0
if (user_id < 2000000) -> shard_1
...
- Pros: Range queries efficient, easy to understand
- Cons: Hotspots if access is skewed (new users write to latest shard)
- Use when: Range queries are common, data has natural ordering
Directory-based Sharding
lookup_table = {user_id -> shard_id}
- Pros: Flexible rebalancing, any sharding key
- Cons: Lookup table is a bottleneck, single point of failure
- Use when: Rebalancing is frequent, complex routing needed
Normalization vs Denormalization
Normalization (3NF) eliminates redundancy:
orders(order_id, user_id, product_id, quantity)
users(user_id, name, email)
products(product_id, name, price)
Denormalization adds redundancy for read performance:
orders(order_id, user_id, user_name, product_id, product_name, product_price, quantity)
When to denormalize:
- Read-heavy systems where JOINs are expensive
- Data that rarely changes (product names, user names)
- When latency requirements are sub-millisecond
When to normalize:
- Write-heavy systems where updates are frequent
- Data integrity is critical
- Storage is a concern
Indexing Strategy
B-Tree Index: Default for most databases. Good for equality and range queries.
CREATE INDEX idx_orders_user_id ON orders(user_id);
Composite Index: For multi-column queries.
CREATE INDEX idx_orders_user_date ON orders(user_id, order_date DESC);
- Column order matters: put high-selectivity columns first
Covering Index: Includes all columns needed by a query.
CREATE INDEX idx_orders_covering ON orders(user_id, order_date) INCLUDE (total_amount, status);
- Query never hits the actual table (index-only scan)
Partial Index: Indexes only a subset of rows.
CREATE INDEX idx_orders_pending ON orders(order_date) WHERE status = 'pending';
- Smaller index, faster maintenance, lower storage
Schema Design Anti-Patterns
- Over-normalization: Too many JOINs for simple queries
- Under-normalization: Data inconsistency when updates propagate
- Missing foreign keys: Orphaned records accumulate
- UUID as primary key: Index fragmentation, slower inserts vs auto-increment
- Large JSON columns: Cannot be indexed efficiently, full table scans
- God table: Single table with 100+ columns serving multiple services
Scaling Strategy
Read Scaling: Replication
Primary-Replica Architecture
One primary handles writes; replicas handle reads. Most production systems use this pattern.
┌─────────────┐
│ Primary │ ← writes
│ (master) │
└──────┬──────┘
│ WAL/binlog
┌────────────┼────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Replica1 │ │ Replica2 │ │ Replica3 │
└──────────┘ └──────────┘ └──────────┘
↑ ↑ ↑
└────────────┼────────────┘
reads distributed
Synchronous vs Asynchronous Replication:
| Type | Consistency | Latency | Use Case |
|---|---|---|---|
| Synchronous | Strong | Higher | Financial systems |
| Asynchronous | Eventual | Lower | Social media, analytics |
| Semi-synchronous | Balanced | Medium | E-commerce |
Read-after-write consistency problem:
- Write to primary, read from replica that hasn't synced yet
- Solutions: Read from primary for recent writes, session stickiness, version vectors
Write Scaling: Sharding
When to shard:
- Single database cannot handle write throughput
- Data exceeds single machine storage
- Latency requirements demand data locality
Sharding implementation layers:
Application Layer: App determines shard for each query.
def get_shard(user_id):
return user_id % NUM_SHARDS
def get_connection(user_id):
shard = get_shard(user_id)
return connections[shard]
Proxy Layer: Middleware handles routing (Vitess, ProxySQL).
Database Layer: Native sharding (Cassandra, DynamoDB, CockroachDB).
Resharding challenges:
- Adding shards requires data migration
- Downtime during rebalancing (mitigate with consistent hashing)
- Temporary inconsistency during migration
Connection Pooling
Problem: Each database connection costs 5-10MB memory. 10,000 connections = 50-100GB RAM.
Solution: Connection pool
Pool Configuration:
- Min connections: 10
- Max connections: 100
- Connection timeout: 30s
- Idle timeout: 600s
Tools: PgBouncer (PostgreSQL), ProxySQL (MySQL), HikariCP (Java)
Best practices:
- Pool size = (2 * CPU cores) + effective_spindle_count
- Monitor active vs idle connections
- Set connection timeout to fail fast
- Use separate pools for read/write
Caching Layer Integration
Cache-aside pattern with database:
1. Check cache for key
2. If HIT → return cached value
3. If MISS → query database → store in cache → return value
4. On write → update database → invalidate cache
Cache invalidation strategies:
- TTL-based: Cache expires after N seconds
- Event-based: Invalidate on write (more complex, more accurate)
- Version-based: Cache key includes version number
Database Scaling Anti-Patterns
- Premature sharding: Sharding before hitting actual limits adds enormous complexity
- Cross-shard joins: Expensive scatter-gather queries across shards
- Hotspot shards: Uneven data distribution from poor shard key selection
- No connection pooling: Exhausting database connections under load
- Missing read replicas: All reads hitting primary database
- Over-caching: Caching data that changes frequently or is rarely accessed
Practice Problems
Design a scalable Database Design (HLD) system. Cover high-level architecture, data model, and API design.
Solution
// Complete system design:
// - Functional + Non-functional requirements
// - Capacity estimation
// - Data model (SQL/NoSQL choice)
// - API endpoints
// - Component architecture
// - Scaling strategy
// - Monitoring & reliabilityHow would you scale Database Design (HLD) to handle 10x the current load? Identify bottlenecks and solutions.
Solution
// Scaling approach:
// 1. Load balancing
// 2. Database sharding/replication
// 3. Cache layer (Redis)
// 4. CDN for static assets
// 5. Async processing (queues)
// 6. Microservices decompositionAnalyze potential failure modes for Database Design (HLD) and design mitigation strategies.
Solution
// Failure mitigation:
// 1. Redundancy (multi-AZ)
// 2. Circuit breakers
// 3. Retry with backoff
// 4. Dead letter queues
// 5. Health checks
// 6. Graceful degradationQuiz
1. You are designing an e-commerce platform that requires ACID transactions for orders but flexible schemas for product catalogs. What database combination is most appropriate?
2. When is hash-based sharding preferred over range-based sharding?
3. What problem does a connection pool solve?
4. In a primary-replica setup, what consistency issue can occur with read-after-write?
5. When should you denormalize a database schema?
Flashcards
Question
What is the difference between vertical and horizontal partitioning?
Click to reveal answer
Answer
Vertical partitioning splits tables by columns (different column groups in separate tables). Horizontal partitioning (sharding) splits rows across multiple database instances.
Question
When would you choose Cassandra over PostgreSQL?
Click to reveal answer
Answer
Cassandra excels at write-heavy workloads with linear scalability and no single point of failure. Choose it for time-series data, IoT telemetry, or when you need multi-datacenter replication. PostgreSQL is better for complex queries with ACID guarantees.
Question
What is a composite index and why does column order matter?
Click to reveal answer
Answer
A composite index covers multiple columns. Column order matters because queries can only use the index efficiently if they filter by the leftmost columns first. An index on (user_id, order_date) supports queries filtering by user_id alone or user_id + order_date, but not order_date alone.
Question
What are the three layers where sharding can be implemented?
Click to reveal answer
Answer
1) Application layer: App determines shard for each query. 2) Proxy layer: Middleware handles routing (Vitess, ProxySQL). 3) Database layer: Native sharding (Cassandra, DynamoDB).
Question
What is the cache-aside pattern?
Click to reveal answer
Answer
1) Check cache for key. 2) If HIT, return cached value. 3) If MISS, query database, store in cache, return value. 4) On write, update database, invalidate cache.
Revision Notes
Key Takeaways
- 1.SQL for ACID, complex relationships, and ad-hoc queries; NoSQL for scale, flexibility, and simple access patterns
- 2.Denormalize for read performance; normalize for data integrity
- 3.Shard only when necessary — it adds enormous complexity
- 4.Always use connection pooling in production
- 5.Read replicas solve most read scaling needs before sharding becomes necessary
- 6.Index design is critical — a missing index can cause 1000x performance degradation
Interview Tips
- •Start by asking about data volume, read/write ratio, and consistency requirements before recommending a database
- •Mention specific databases (PostgreSQL, DynamoDB) rather than just saying SQL or NoSQL
- •Draw the sharding diagram showing how data distributes across shards
- •Discuss the tradeoffs of your choice — interviewers want to see you understand downsides
- •If asked about a specific system, name the databases you would use for different components and justify each choice
Cheat Sheet
Database Design Cheat Sheet
SQL vs NoSQL Decision Flow
- Need ACID? → SQL
- Need flexible schema? → NoSQL
- Complex relationships with JOINs? → SQL
- Massive write throughput? → NoSQL (Cassandra/DynamoDB)
- Simple key-value access? → NoSQL (Redis/DynamoDB)
Scaling Checklist
- Read replicas for read scaling
- Connection pooling (min: 10, max: 100+)
- Sharding when single DB limits reached
- Caching layer (cache-aside pattern)
- Index strategy for common queries
Anti-Patterns to Mention
- Premature sharding
- N+1 queries
- Missing indexes on foreign keys
- Over-normalization causing excessive JOINs
- Cross-shard joins
- Hotspot shards from poor key selection
Database Choices by System
| System | Database | Reason |
|---|---|---|
| E-commerce orders | PostgreSQL | ACID for transactions |
| Product catalog | MongoDB | Flexible product attributes |
| Session store | Redis | Sub-millisecond reads |
| Chat messages | Cassandra | Write-heavy time-series |
| Search | Elasticsearch | Full-text search |
| Gaming leaderboard | Redis/DynamoDB | Low latency at scale |