The Most Important Decision
Every system starts with: "How do we store data?" Wrong choice: - Slow queries - Expensive rewrites - Scalability bottlenecks - Engineering frustration Right choice: - Natural data model - Fast queries - Easy scaling - Happy team
The Two Worlds
1. Relational (SQL)
Examples: PostgreSQL, MySQL, Oracle, SQL Server
Philosophy: Structure first, flexibility later Data model: Tables with rows/columns Schema: Strict (defined upfront) Relationships: Foreign keys + JOINs Query language: SQL Scaling: Single-node, replicas, sharding, or distributed SQL depending on product Transactions: Isolation and durability depend on engine and configuration
2. Non-Relational (NoSQL)
Examples: MongoDB, Cassandra, Redis, Neo4j
Philosophy: Flexibility first, structure optional Data model: Varies (documents, key-value, graphs, columns) Schema: Product-dependent; flexible does not mean unvalidated Relationships: Embedded or denormalized Query language: Varies by database Scaling: Primarily horizontal Consistency: Product- and operation-specific, from eventual to strongly consistent
SQL Databases In-Depth
Strengths
✅ ACID Transactions
BEGIN TRANSACTION; UPDATE accounts SET balance = balance - 100 WHERE id = 1; UPDATE accounts SET balance = balance + 100 WHERE id = 2; COMMIT; -- Guaranteed: Both happen or neither happens
✅ Complex Queries
-- Find top customers who bought products in multiple categories
SELECT
c.name,
COUNT(DISTINCT p.category) AS categories_purchased,
SUM(oi.quantity * oi.unit_price) AS total_spent
FROM customers c
JOIN orders o ON c.id = o.customer_id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
GROUP BY c.id
HAVING COUNT(DISTINCT p.category) >= 3
ORDER BY total_spent DESC
LIMIT 10;
✅ Data Integrity
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
age INTEGER CHECK (age >= 18),
created_at TIMESTAMP DEFAULT NOW()
);
-- Database enforces:
-- - Unique emails
-- - Non-null emails
-- - Age >= 18
-- - Auto-generated IDs
Weaknesses
❌ Rigid Schema
-- Adding a column requires migration ALTER TABLE users ADD COLUMN phone VARCHAR(20); -- Locking and rewrite behavior depend on the database, version, and default. -- Plan and test the migration; adding a nullable column need not rewrite every row.
❌ Vertical Scaling Limits
One node has finite compute, memory, storage, and I/O capacity. The relevant limit depends on the product, hardware, and workload. Replicas, partitioning, middleware, or distributed SQL may extend capacity.
❌ JOIN Performance
-- 5-way JOIN on large tables = slow SELECT * FROM table1 JOIN table2 ON ... JOIN table3 ON ... JOIN table4 ON ... JOIN table5 ON ... WHERE ...; -- Time: can be seconds on large datasets
NoSQL Families
A. Document Stores (MongoDB, CouchDB)
Model: JSON-like documents
// User document
{
"_id": "user123",
"name": "Alice",
"email": "alice@example.com",
"address": {
"street": "123 Main St",
"city": "NYC"
},
"orders": [
{ "id": "order1", "total": 99.99, "date": "2024-01-01" },
{ "id": "order2", "total": 149.99, "date": "2024-01-15" }
]
}
// Query
db.users.find({ "address.city": "NYC", "orders.total": { $gt: 100 } })
When to use:
- Flexible schemas
- Nested/hierarchical data
- Rapid prototyping
- Content management
Example: Blog platform with varying article structures
B. Key-Value Stores (Redis, DynamoDB)
Model: Simple key → value mapping
# Redis
redis.set("user:123:name", "Alice")
redis.set("user:123:email", "alice@example.com")
# Retrieve
name = redis.get("user:123:name")
# Atomic operations
redis.incr("page:views")
redis.expire("session:abc", 3600) # TTL
When to use:
- Caching
- Session storage
- Real-time counters
- Simple lookups
- Sub-millisecond latency required
Example: Session store for web app
C. Wide-Column Stores (Cassandra, HBase)
Model: Rows with dynamic columns
Column Family: user_events Row Key: user_123 Column: event_2024-01-01-10:00:00 → "login" Column: event_2024-01-01-10:05:23 → "click_button" Column: event_2024-01-01-10:10:15 → "purchase" ... Column: event_2024-01-15-09:30:00 → "logout" // Bound partition size and hotspot risk; a theoretical limit is not a target.
When to use:
- Time-series data
- Massive write throughput
- Log aggregation
- IoT sensor data
Example: Netflix viewing history (billions of events)
D. Graph Databases (Neo4j, Neptune)
Model: Nodes + relationships
// Social network (Alice:Person)-[:FRIEND]->(Bob:Person)-[:FRIEND]->(Carol:Person) (Alice)-[:BOUGHT]->(Product:Laptop) (Bob)-[:BOUGHT]->(Product) // Query: Friends who bought same product MATCH (me:Person)-[:FRIEND]-(friend)-[:BOUGHT]->(product) WHERE me.name = 'Alice' AND (me)-[:BOUGHT]-> (product) RETURN friend.name, product.name
When to use:
- Social networks
- Recommendation engines
- Fraud detection
- Knowledge graphs
Example: LinkedIn "People you may know"
ACID vs BASE
ACID (transaction properties)
Atomicity: All or nothing Consistency: Valid state always Isolation: Transactions don't interfere Durability: Committed = persisted Example: Bank transfer: $100 from A to B Either both happen or neither Database never shows inconsistent state
BASE (an availability-oriented design shorthand)
Basically Available: System responds even if some nodes down Soft state: State may change over time Eventually consistent: Reads may be stale, but will converge Example: Twitter follower count Might show 1,000 on one server, 1,001 on another Eventually both show 1,001 Acceptable trade-off for speed
Decision Framework
Choose SQL when:
✅ Structured data with clear relationships
E-commerce: - Users, Orders, Products, Inventory - Clear foreign keys - Need JOINs (order → order_items → products)
✅ Data integrity is critical
Banking, Healthcare, Payroll: - Cannot lose transactions - Must maintain constraints - ACID guarantees required
✅ Complex queries required
Analytics, Reporting: - Multi-table aggregations - Ad-hoc queries - Business intelligence
✅ The engine fits the workload
- Required queries meet measured latency targets - Transaction and durability settings meet business invariants - Growth and recovery plans are operationally manageable
Choose NoSQL when:
✅ Flexible/evolving schema
Content platforms, catalogs: - Product attributes vary - User profiles with custom fields - Rapid feature development
✅ Massive scale required
Social media, IoT, logs: - Billions of records - Petabytes of data - Horizontal scaling needed
✅ High write throughput
Time-series, events, metrics: - 100k+ writes/second - Append-only workload - Eventual consistency OK
✅ Geographic distribution
Global apps: - Low-latency worldwide - Multi-region replication - Partition tolerance critical
✅ Specific access patterns
Key-value: Cache, sessions (Redis) Graph: Social networks (Neo4j) Document: CMS, catalogs (MongoDB) Wide-column: Time-series (Cassandra)
A workload example, not a company blueprint
Consider a small marketplace. Orders need atomic inventory changes and an auditable payment state. Catalog items have varying attributes. Search needs ranking and typo tolerance. These are three requirements, not proof that three databases are immediately necessary.
Start by testing whether one relational store, including its JSON and search facilities, meets the requirements. Add a dedicated search index or document store only when a measured limitation justifies the extra replication, recovery, and operational work. Never treat an unsupported claim that a famous company “migrated from SQL to NoSQL” as a selection rule.
Polyglot Persistence
Modern approach: Use multiple databases for different needs.
Example: E-commerce platform
PostgreSQL: - Orders, payments, inventory - ACID for financial accuracy Redis: - Shopping cart (session data) - Product catalog cache - Real-time inventory count Elasticsearch: - Product search - Faceted navigation - Autocomplete Cassandra: - User click streams - Product view history - Recommendation training data Neo4j: - Product recommendations - "Customers also bought..." - Fraud detection rules
Migration Considerations
SQL → NoSQL
Reasons:
- Measured bottlenecks that the chosen alternative addresses
- Need horizontal scaling
- Schema too rigid
- Geographic distribution required
Challenges:
1. No JOINs → Denormalize data 2. Different transaction boundaries → Verify supported atomic operations 3. Different query patterns → Rethink access patterns 4. Data migration → Dual-write period
Strategy:
1. Identify bounded contexts 2. Migrate one service/table at a time 3. Use durable change capture or an outbox with reconciliation during transition 4. Validate data consistency 5. Gradual traffic shift
NoSQL → SQL
Reasons:
- Need complex queries
- ACID transactions required
- Data model stabilized
- JOIN performance needed
Example: Early startup with MongoDB
Phase 1: MongoDB (rapid prototyping) Phase 2: PostgreSQL (after PMF, stable schema) Reason: Complex reporting needs emerged
How to compare performance fairly
A table of invented milliseconds or requests per second cannot select a database. Run candidates on representative data with equivalent durability, replication, indexes, and concurrency. Include:
- Successful operations per second at the required p95/p99 latency and error target.
- The most expensive queries, not just a primary-key lookup.
- Storage and index growth, compaction or vacuum work, and recovery time.
- A node failure and a replica lag scenario.
- The same business operation: one denormalized read and a ten-query reconstruction are not interchangeable benchmarks.
Keep the dataset, configuration, hardware, and workload script with the results so the comparison can be repeated.
Interview Tips 💡
When discussing SQL vs NoSQL in interviews:
-
It's not either/or: "Modern apps often use both - PostgreSQL for transactions, Redis for cache, Cassandra for events..."
-
Start with use case: "For banking? SQL (ACID critical). For social feed? NoSQL (scale critical)..."
-
Separate dimensions: "Data model, transaction isolation, replication, and partitioning are independent decisions. Evaluate the specific engine."
-
Use evidence: "Document the required query and invariant, then show how the chosen database satisfies both."
-
Schema evolution: "SQL: rigid schema, migrations. NoSQL: flexible, but must handle multiple versions in code..."
-
Query patterns: "SQL excels at ad-hoc queries. NoSQL requires known access patterns upfront..."
Related Concepts
- ACID vs BASE — Consistency models
- CAP Theorem — Distributed system trade-offs
- Database Sharding — Horizontal partitioning
- Replication — Data redundancy
- Indexing — Query optimization
Primary references
PostgreSQL JSON types demonstrates that relational storage can include documents. MongoDB transactions demonstrates that a document database can support multi-document transactions. CockroachDB replication provides a concrete distributed SQL design. Use these capabilities as questions for evaluation rather than equating SQL with one machine or NoSQL with eventual consistency.
Keep reading
ACID and BASE: Choose Guarantees per Operation
Distinguish transaction isolation, invariant preservation and replica freshness through a worked inventory example.
Database Replication
The process of copying and maintaining database objects in multiple databases to improve reliability, fault-tolerance, and accessibility.
Database Sharding
How to split a massive database across multiple servers. Horizontal scaling strategies, challenges (Joins, ACID), and real-world algorithms used by Instagram, Vitess, and CockroachDB.
Distributed Transactions
Managing data consistency across multiple services where a single operation must either fully succeed or fully fail. Deep dive into SAGA, 2PC, and modern patterns.
Graph Databases
Complete guide to graph databases treating relationships as first-class citizens, covering property graphs, Cypher query language, graph algorithms, and production implementations in Neo4j, Amazon Neptune powering social networks, fraud detection, and recommendation systems.
Consistent Hashing
How to add/remove servers without moving every single key. The ring, virtual nodes, placement and migration tradeoffs.