Skip to content
Back to all concepts
DatabaseData ModelingArchitecture

SQL vs NoSQL: The Database Guide

Complete database selection guide covering relational vs non-relational systems, ACID vs BASE, when to use PostgreSQL vs MongoDB vs Redis vs Neo4j, and a workload-based selection framework that separates data model, transactions, and scaling.

By ScaleWikiUpdated 4 min readBeginner

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

sql
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

sql
-- 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

sql
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

sql
-- 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

sql
-- 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

javascript
// 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

python
# 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

cypher
// 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:

  1. It's not either/or: "Modern apps often use both - PostgreSQL for transactions, Redis for cache, Cassandra for events..."

  2. Start with use case: "For banking? SQL (ACID critical). For social feed? NoSQL (scale critical)..."

  3. Separate dimensions: "Data model, transaction isolation, replication, and partitioning are independent decisions. Evaluate the specific engine."

  4. Use evidence: "Document the required query and invariant, then show how the chosen database satisfies both."

  5. Schema evolution: "SQL: rigid schema, migrations. NoSQL: flexible, but must handle multiple versions in code..."

  6. Query patterns: "SQL excels at ad-hoc queries. NoSQL requires known access patterns upfront..."

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.