SQL and NoSQL represent two fundamentally different approaches to storing and querying data. SQL databases have powered applications since the 1970s. NoSQL databases emerged in the 2000s to solve specific scalability and flexibility problems that relational databases struggled with at web scale. Choosing the wrong one can cause major pain later — this guide covers every major dimension.
At a glance
| SQL | NoSQL | |
|---|---|---|
| Data model | Tables with rows and columns | Documents, key-value, wide-column, graph |
| Schema | Fixed schema (defined upfront) | Flexible schema (dynamic) |
| Query language | SQL (standardised) | Database-specific APIs |
| Transactions | Full ACID guarantees | Varies (BASE to full ACID) |
| Scalability | Vertical (scale up) + read replicas | Horizontal (scale out natively) |
| Relationships | Foreign keys, JOINs | Typically embedded or application-handled |
| Consistency | Strong consistency by default | Eventual consistency common |
| Best for | Structured data, complex queries, transactions | Unstructured data, high throughput, flexibility |
| Examples | PostgreSQL, MySQL, SQLite, SQL Server | MongoDB, Redis, Cassandra, DynamoDB |
| Learning curve | SQL is universal and transferable | Each database has its own paradigm |
What is SQL (relational)?
SQL databases store data in tables — rows and columns, like a spreadsheet, but with strict types and relationships enforced at the schema level.
-- Create a users table
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
-- Create an orders table with a foreign key
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INT REFERENCES users(id) ON DELETE CASCADE,
total NUMERIC(10,2) NOT NULL,
status TEXT DEFAULT 'pending'
);
-- Query with a JOIN
SELECT u.name, COUNT(o.id) AS order_count, SUM(o.total) AS total_spent
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.name
ORDER BY total_spent DESC;
Core properties (ACID):
- Atomicity — a transaction either fully completes or fully rolls back
- Consistency — data always moves from one valid state to another
- Isolation — concurrent transactions don't interfere with each other
- Durability — committed data survives crashes
What is NoSQL (non-relational)?
NoSQL is an umbrella term for databases that don't use the relational model. There are four main types:
1. Document stores (MongoDB, Firestore, CouchDB)
Store data as JSON-like documents. Great for hierarchical, semi-structured data.
// MongoDB document
{
_id: ObjectId("..."),
email: "alice@example.com",
name: "Alice",
orders: [
{ total: 99.99, status: "delivered", items: ["book", "pen"] },
{ total: 24.50, status: "pending", items: ["notebook"] }
],
address: {
city: "London",
country: "UK"
}
}
No JOINs needed — related data is embedded in one document. A single read gets everything you need for a user profile page.
2. Key-value stores (Redis, DynamoDB, Memcached)
The simplest model — a distributed hash map.
# Redis key-value
SET session:abc123 '{"userId":42,"role":"admin"}' EX 3600
GET session:abc123
ZADD leaderboard 1500 "alice"
ZRANGE leaderboard 0 9 REV WITHSCORES
Extremely fast for lookups by key. Used for caching, sessions, rate limiting.
3. Wide-column stores (Cassandra, HBase, Bigtable)
Tables with rows and columns, but columns are dynamic per row. Optimised for massive write throughput.
-- Cassandra CQL
CREATE TABLE messages_by_conversation (
conversation_id UUID,
created_at TIMESTAMP,
sender_id UUID,
body TEXT,
PRIMARY KEY (conversation_id, created_at)
) WITH CLUSTERING ORDER BY (created_at DESC);
Queries must include the partition key. No JOINs. Designed for time-series, IoT, messaging at massive scale.
4. Graph databases (Neo4j, Amazon Neptune)
Store data as nodes and edges. Perfect for relationship-heavy data.
-- Neo4j Cypher
MATCH (alice:User {name: "Alice"})-[:FOLLOWS]->(u:User)
WHERE NOT (alice)-[:FOLLOWS]->(u)-[:BLOCKED]->(alice)
RETURN u.name AS suggestion
ORDER BY u.follower_count DESC
LIMIT 10
ACID vs BASE
| Property | ACID (SQL) | BASE (NoSQL) |
|---|---|---|
| Consistency | Strong — guaranteed after commit | Eventual — converges over time |
| Availability | May sacrifice availability for consistency | Prioritises availability |
| Isolation | Transactions isolated (configurable level) | Often no transactions across documents |
| Durability | Durable on commit | Usually durable (configurable) |
| Partition tolerance | Typically CP (Consistent + Partition-tolerant) | Typically AP (Available + Partition-tolerant) |
| Suitable for | Financial, medical, e-commerce | Social, IoT, analytics, caching |
BASE = Basically Available, Soft-state, Eventually consistent.
Schema: fixed vs flexible
SQL: schema-first
You define the schema before inserting data. Changing it requires migrations.
-- Adding a column to a 50M-row table needs careful migration
ALTER TABLE users ADD COLUMN phone TEXT;
-- In production: use expand-contract pattern to avoid locking
Upside: the schema is your documentation. Invalid data cannot enter the database.
Downside: schema changes require planning and migration scripts.
NoSQL: schema-last (or schema-optional)
Documents don't need to match a fixed structure.
// These two documents can coexist in the same collection
{ _id: 1, name: "Alice", email: "alice@example.com" }
{ _id: 2, name: "Bob", phone: "+44 7700 000000", tags: ["vip"] }
Upside: rapid iteration; no migration needed when adding fields.
Downside: inconsistent data; validation must happen at the application layer.
Performance comparison
| Scenario | SQL | NoSQL |
|---|---|---|
| Simple key lookup | Fast (indexed) | Extremely fast (key-value O(1)) |
| Complex JOIN queries | Excellent (optimiser) | Poor (no native JOINs) |
| Write throughput (millions/sec) | Limited by ACID overhead | Excellent (Cassandra, DynamoDB) |
| Aggregation / analytics | Excellent (GROUP BY, window functions) | Varies (MongoDB aggregation pipeline) |
| Full-text search | Limited (basic LIKE, pg full-text) | Built-in (Elasticsearch, MongoDB Atlas) |
| Time-series data | Good with partitioning | Excellent (Cassandra, InfluxDB) |
| Graph traversal | Slow (recursive CTEs) | Excellent (Neo4j, Neptune) |
| Caching | Not designed for it | Excellent (Redis microsecond latency) |
Scalability
SQL scales vertically by default. Add more CPU and RAM to the database server. Read replicas help with read-heavy workloads. Horizontal sharding is possible but complex (PlanetScale, Citus for PostgreSQL).
NoSQL scales horizontally by design. Add nodes to a cluster. DynamoDB and Cassandra handle petabytes across hundreds of nodes with no single point of failure.
SQL typical scaling: NoSQL typical scaling:
┌─────────────────┐ ┌────┐ ┌────┐ ┌────┐
│ Primary (RW) │ │ N1 │ │ N2 │ │ N3 │
│ 16 CPU / 64GB │ └────┘ └────┘ └────┘
└────────┬────────┘ add nodes as you grow
│ replication
┌────────┴────────┐
│ Replica (R) │
└─────────────────┘
Data model design: embedded vs normalised
One of the biggest SQL vs NoSQL decisions is where to handle relationships.
SQL: normalise, then JOIN
-- 3NF normalised schema
users(id, email, name)
products(id, name, price)
orders(id, user_id, created_at)
order_items(id, order_id, product_id, quantity)
-- Reconstitute with JOIN
SELECT o.id, u.name, p.name AS product, oi.quantity
FROM orders o
JOIN users u ON u.id = o.user_id
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE o.id = 42;
MongoDB: embed for read performance
// Embed order items inside the order document
{
_id: ObjectId("..."),
user: { id: "u42", name: "Alice" },
items: [
{ product: "Book", qty: 2, price: 12.99 },
{ product: "Pen", qty: 5, price: 1.49 }
],
total: 33.43,
created_at: ISODate("2025-01-15")
}
// Single document read — no JOIN needed
db.orders.findOne({ _id: orderId })
Rule of thumb:
- Embed when the nested data is always read together and doesn't change independently
- Reference (foreign key /
$lookup) when data is shared across many parents or grows unboundedly
Transactions
SQL transactions
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- If anything fails here, both updates roll back automatically
COMMIT;
MongoDB multi-document transactions (v4.0+)
const session = client.startSession();
try {
session.startTransaction();
await accounts.updateOne({ _id: 1 }, { $inc: { balance: -100 } }, { session });
await accounts.updateOne({ _id: 2 }, { $inc: { balance: 100 } }, { session });
await session.commitTransaction();
} catch (err) {
await session.abortTransaction();
} finally {
session.endSession();
}
MongoDB supports multi-document transactions, but they carry performance overhead. The preferred MongoDB pattern is to design your schema so transactions are unnecessary — embed related data in one document.
Cassandra: no multi-row transactions
Cassandra supports lightweight transactions (compare-and-set via Paxos) for single partition operations, but has no general multi-row transactions. Design your data model around this constraint.
When SQL wins
| Situation | Why SQL |
|---|---|
| Financial data (banking, payments) | ACID guarantees; no lost transactions |
| Complex relationships (ERP, CRM) | JOINs are efficient; normalisation prevents anomalies |
| Reporting and analytics | GROUP BY, window functions, CTEs, subqueries |
| Regulatory / compliance | Audit trails; referential integrity enforced by DB |
| Team knows SQL | SQL is universal — hire any backend engineer |
| Unknown query patterns | SQL's flexibility handles ad-hoc queries well |
| Medium data scale (up to ~1TB) | PostgreSQL handles this comfortably |
When NoSQL wins
| Situation | Why NoSQL |
|---|---|
| Massive write throughput | Cassandra, DynamoDB handle millions of writes/sec |
| Document storage (CMS, catalogs) | MongoDB embeds nested data naturally |
| Caching / sessions | Redis gives microsecond latency |
| Real-time features (chat, pub/sub) | Redis Pub/Sub, Firestore listeners |
| Flexible schema (early startup) | Add fields without migrations |
| Global, multi-region | DynamoDB Global Tables, Cassandra multi-DC |
| Graph data (social network, fraud detection) | Neo4j traverses deep relationships efficiently |
| Time-series / IoT | Cassandra, InfluxDB handle high-frequency writes |
Popular databases side-by-side
| Database | Type | ACID | Best for |
|---|---|---|---|
| PostgreSQL | Relational | Full | General-purpose, JSON, analytics |
| MySQL / MariaDB | Relational | Full | Web apps, WordPress, e-commerce |
| SQLite | Relational | Full | Embedded, mobile, development |
| SQL Server | Relational | Full | Microsoft ecosystems, enterprise |
| MongoDB | Document | Multi-doc (v4+) | CMS, product catalogs, user profiles |
| Redis | Key-value | Partial (Lua) | Cache, sessions, leaderboards, pub/sub |
| Cassandra | Wide-column | LWT only | Time-series, IoT, messaging at scale |
| DynamoDB | Key-value + doc | Per-partition | AWS serverless, global scale |
| Elasticsearch | Document | No | Full-text search, log analytics |
| Neo4j | Graph | Full | Social graphs, fraud detection |
| InfluxDB | Time-series | No | Metrics, monitoring, IoT |
| Firebase / Firestore | Document | Per-document | Mobile apps, real-time sync |
SQL vs NoSQL: full comparison
| Dimension | SQL | NoSQL |
|---|---|---|
| Data model | Tables, rows, columns | Documents, key-value, wide-column, graph |
| Schema | Rigid, enforced | Flexible, optional |
| Query language | SQL (ISO standard) | Database-specific |
| JOINs | Native, optimised | Rare (embed data instead) |
| Transactions | Full multi-table ACID | Varies widely |
| Consistency | Strong | Eventual (usually) |
| Write scalability | Vertical + sharding (complex) | Horizontal (built-in) |
| Read scalability | Read replicas | Horizontal + eventual consistency |
| Schema migration | Required, tooling mature | Not needed (or optional) |
| Developer experience | Universal SQL knowledge | Learn each DB separately |
| Cost at scale | Expensive (large vertical) | Cheaper (commodity nodes) |
| Data integrity | Enforced by DB | Application responsibility |
| Indexing | B-tree, GIN, GiST, etc. | Varies (compound, sparse, TTL) |
| Maturity | 50+ years | 15–20 years |
| Managed cloud | RDS, Cloud SQL, Supabase, Neon | DynamoDB, MongoDB Atlas, Upstash |
| Open source | PostgreSQL, MySQL, MariaDB | MongoDB (SSPL), Redis, Cassandra |
The "both" approach
Modern applications commonly use both:
┌─────────────────────────────────────────┐
│ Your Application │
└────────────────────┬────────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌─────────┐ ┌───────────┐ ┌──────────┐
│PostgreSQL│ │ Redis │ │ Elastic │
│ (primary │ │ (cache + │ │ search │
│ store) │ │ sessions)│ │ (search) │
└─────────┘ └───────────┘ └──────────┘
Example: an e-commerce platform might use:
- PostgreSQL for orders, inventory, and payments (ACID required)
- Redis for sessions, cart data, and product page caching
- Elasticsearch for product search with fuzzy matching
- MongoDB for product reviews with varying attributes
Common mistakes
| Mistake | Why it's a problem | Fix |
|---|---|---|
| Using NoSQL to avoid learning SQL | SQL is essential; you'll hit limitations fast | Learn SQL regardless |
| Storing everything in one MongoDB collection | Becomes a chaos of inconsistent documents | Use schema validation; separate collections |
JOINing in NoSQL with $lookup for every query |
Slow; defeats the purpose of document storage | Redesign schema to embed |
| Using SQL for a hot-path cache | Unnecessary load; RDBMS not optimised for it | Add Redis for caching layer |
| No transactions in MongoDB when you need them | Data corruption on failure | Use sessions+transactions or redesign schema |
| Horizontal sharding SQL prematurely | Massive complexity before you need it | Start vertical; shard when proven necessary |
| Assuming NoSQL = schemaless = no validation | Application receives garbage data | Add JSON Schema validation (MongoDB) or Zod |
| Picking Cassandra for a small project | Complex ops, limited query flexibility | Use PostgreSQL until you need Cassandra scale |
Decision guide
Use SQL (PostgreSQL) when:
- You have relational data with many-to-many relationships
- You need complex queries you can't predict upfront
- You're building anything financial or requiring audit trails
- Your team knows SQL
- Data fits on one server (up to a few TB)
Use a document store (MongoDB) when:
- Your data is naturally hierarchical (nested objects)
- Schema varies per record (product catalog with different attributes)
- You're prototyping and need schema flexibility
- You want a single-document read for your main entity
Use a key-value store (Redis) when:
- You need sub-millisecond latency for lookups
- Data is ephemeral (sessions, caches, rate limiting)
- You need pub/sub, sorted sets, or atomic counters
Use a wide-column store (Cassandra / DynamoDB) when:
- You need millions of writes per second
- Data is time-series or event-driven
- You need global, multi-region distribution
Use a graph database (Neo4j) when:
- The data is fundamentally relational in complex ways (friends-of-friends, fraud rings)
- You need deep traversals (more than 3 hops are slow in SQL)
Default choice: PostgreSQL. It handles 90% of use cases, supports JSONB for document-style storage, has excellent horizontal read scaling via replicas, and has a massive ecosystem. Add Redis for caching. Add Elasticsearch for search. Reach for specialised NoSQL only when you have a concrete need.
FAQ
Q: Can NoSQL databases be ACID compliant?
Yes. MongoDB supports multi-document ACID transactions (v4.0+). FaunaDB (now Fauna) was designed with ACID from the start. DynamoDB supports transactions within a table. However, most NoSQL databases make trade-offs — they default to eventual consistency because it enables higher availability and write throughput.
Q: Is SQL dead?
No. PostgreSQL is one of the fastest-growing databases in 2025. SQL skills are arguably more in demand than ever — including for querying Snowflake, BigQuery, Redshift, and even MongoDB (via Atlas SQL). The "NoSQL killed SQL" narrative from the 2010s didn't materialise.
Q: Can PostgreSQL replace MongoDB?
For most use cases, yes. PostgreSQL's JSONB column type supports indexing, querying, and storing arbitrary JSON documents. pg_jsonschema adds schema validation. For true MongoDB feature parity at massive document scale (>100M documents, complex aggregations), MongoDB still has advantages. But for most teams, PostgreSQL + JSONB avoids the operational complexity of running two databases.
Q: Does Google / Netflix / Amazon use SQL or NoSQL?
Both. Google built Spanner (distributed SQL, global ACID). Netflix uses Cassandra for billions of events. Amazon uses DynamoDB for Prime Day traffic (hundreds of millions of calls). Every hyperscaler runs dozens of database systems tailored to specific use cases. The lesson: there is no single right answer.
Q: What about NewSQL databases?
NewSQL databases (CockroachDB, TiDB, Spanner, YugabyteDB, PlanetScale) offer SQL semantics with horizontal scalability. They're a middle ground: you write standard SQL, but the database distributes data across nodes like a NoSQL system. Good choice when you need PostgreSQL semantics at Cassandra scale.
Q: When should I switch from SQL to NoSQL?
Concrete signals to consider migrating:
- Your PostgreSQL server consistently hits CPU/memory limits and vertical scaling is no longer cost-effective
- You're doing more than ~100k writes/second on a single table with no relief from partitioning
- Your data is fundamentally non-relational (e.g., sensor readings with unpredictable fields, social graph traversals beyond 3 hops)
- You need global multi-region active-active writes
Don't migrate speculatively. Start with PostgreSQL.