NoSQL is an umbrella term for databases that do not store data as related tables queried with SQL joins. It covers four main families: key-value, document, wide-column and graph stores. They have little in common with each other beyond that. Each one gives up part of the relational model to do one thing well, so the useful question is never "SQL or NoSQL?" but "which data model fits the way this application reads and writes?"
The name is usually expanded as "Not Only SQL". That reading was applied after the term caught on around 2009, and it is the more accurate one today: several of these systems now have SQL-like query languages, and most relational databases now store JSON.
The examples below all model the same thing, a shopping application with users, carts and orders, so you can see what each family is good at.
The four families at a glance
| Family | Data model | Typical lookups | Examples |
|---|---|---|---|
| Key-value | Opaque value behind a unique key | Get, set and delete by exact key | Redis, Valkey, Amazon DynamoDB, etcd |
| Document | JSON-like documents grouped in collections | By key, by any indexed field, nested fields | MongoDB, Couchbase, Amazon DocumentDB, Firestore |
| Wide-column | Rows grouped into partitions, sorted inside each partition | By partition key, then a range within the partition | Apache Cassandra, ScyllaDB, Apache HBase, Google Bigtable |
| Graph | Nodes and the relationships between them | Traversals: "who is connected to what, how many hops away" | Neo4j, Amazon Neptune, Memgraph |
Key-value stores
A key-value store is a dictionary that survives a restart and can be shared between servers. You hand it a key and it returns the value. It does not look inside the value, so you cannot ask "which carts contain product 42?" unless you maintain a second key for that yourself.
That restriction is what makes the model fast and easy to distribute: every operation touches one key, so keys can be spread across machines by hashing.
Good fits: sessions, caches, rate limiters, feature flags, shopping carts, leaderboards.
A cart in Redis (the same commands work in Valkey), stored as a hash that expires after a day of inactivity:
redis-cli HSET cart:user:1001 sku:8841 2 sku:1207 1
redis-cli EXPIRE cart:user:1001 86400
redis-cli HGETALL cart:user:1001
1) "sku:8841"
2) "2"
3) "sku:1207"
4) "1"
Things to know before choosing one:
- Redis keeps the whole dataset in memory. Persistence (snapshots and an append-only file) is configurable, and the default settings can lose the most recent writes in a crash. Treat it as a cache unless you have deliberately configured and tested durability.
- Redis changed its licence in 2024 from BSD to source-available terms, which led to the BSD-licensed Valkey fork under the Linux Foundation. Redis later added AGPLv3 as an option. Check which one your platform or cloud provider actually ships.
- DynamoDB is a disk-based key-value and document store with a hard limit of 400 KB per item.
Document databases
A document database stores each record as a self-contained JSON-like document. Fields can be nested, can hold arrays, and can differ from one document to the next. Unlike a key-value store, the database understands the structure, so you can index and query any field.
Good fits: product catalogues where every category has different attributes, content management, user profiles, event payloads, and any aggregate that the application always loads and saves as a whole.
An order in MongoDB, using the mongosh shell:
db.orders.insertOne({
_id: 5001,
userId: 1001,
status: "paid",
placedAt: ISODate("2026-10-01T09:30:00Z"),
items: [
{ sku: "8841", name: "USB-C cable", qty: 2, price: 9.50 },
{ sku: "1207", name: "Laptop stand", qty: 1, price: 34.00 }
],
shipTo: { city: "Leeds", postcode: "LS1 4AP" }
});
db.orders.createIndex({ userId: 1, placedAt: -1 });
db.orders.find(
{ userId: 1001, "items.sku": "8841" },
{ status: 1, placedAt: 1 }
);
The order and its line items arrive in one read with no join. The cost is duplication: the product name is copied into every order that contains it. That is correct for an order, which should record the name at the time of purchase, and wrong for data that must change everywhere at once.
Things to know:
- "Schemaless" means the database does not enforce a schema by default. Your application still depends on one. MongoDB can enforce it with
$jsonSchemavalidators, and you should use them once the shape settles. - A MongoDB document is limited to 16 MB. An array that grows without bound (comments on a post, events on a device) will eventually hit it and gets slower long before then. Move such items to their own collection.
- MongoDB has supported multi-document ACID transactions since 4.0 (replica sets) and 4.2 (sharded clusters). They work, but a design that needs them on every request is a sign the data is relational.
- MongoDB is licensed under the SSPL, which the Open Source Initiative does not recognise as an open-source licence.
Wide-column stores
A wide-column store looks like a table, but it behaves like a sorted map spread across many machines. Each row belongs to a partition, chosen by hashing the partition key, and rows inside a partition are stored sorted by a clustering key. Reads that name the partition and take a slice of it are fast at almost any data size. Reads that do not name the partition have to ask every node.
Do not confuse this family with columnar analytics databases such as ClickHouse or Amazon Redshift, which store each column separately to make aggregations fast. The 2016-era label "column-oriented" blurred the two; they solve different problems.
Good fits: very high write rates and data that is naturally keyed and time-ordered, such as order history per user, sensor readings per device, messages per conversation and audit logs.
Order history in Cassandra's CQL:
CREATE TABLE shop.orders_by_user (
user_id bigint,
placed_at timestamp,
order_id bigint,
status text,
total decimal,
PRIMARY KEY ((user_id), placed_at, order_id)
) WITH CLUSTERING ORDER BY (placed_at DESC, order_id DESC);
SELECT order_id, status, total
FROM shop.orders_by_user
WHERE user_id = 1001
LIMIT 10;
Things to know:
- You design the table for the query, not for the entity. To look orders up by status as well, you create a second table keyed by status and write to both. There are no joins.
- A query that filters on a non-key column is rejected unless you add
ALLOW FILTERING, which scans the cluster. Seeing that keyword in application code almost always means the table design is wrong. - Consistency is chosen per query (
ONE,QUORUM,ALLand others). Lower levels are faster and can return stale data. - Cassandra 4 and later do not run natively on Windows. Use Linux, WSL2 or a container.
Graph databases
A graph database stores nodes and the relationships between them as first-class records. Following a relationship is a direct pointer hop rather than a join computed at query time, so queries that walk many hops stay fast where the equivalent chain of SQL self-joins becomes slow and unreadable.
Good fits: recommendations, fraud rings, social graphs, permission hierarchies, network and dependency maps.
"Customers who bought this also bought" in Neo4j's Cypher:
CREATE (u1:User {id: 1001}), (u2:User {id: 1002}),
(cable:Product {sku: "8841", name: "USB-C cable"}),
(stand:Product {sku: "1207", name: "Laptop stand"}),
(u1)-[:BOUGHT]->(cable),
(u2)-[:BOUGHT]->(cable),
(u2)-[:BOUGHT]->(stand);
MATCH (:User {id: 1001})-[:BOUGHT]->(p:Product)<-[:BOUGHT]-(other:User)-[:BOUGHT]->(rec:Product)
WHERE rec <> p
RETURN rec.name AS recommendation, count(*) AS score
ORDER BY score DESC;
recommendation score
"Laptop stand" 1
Things to know: graph databases are weaker at whole-dataset aggregation ("total revenue by month") than relational or columnar systems, and a few very highly connected nodes can dominate traversal time. Property-graph queries are now standardised as GQL (ISO/IEC 39075, published in 2024), which is closely modelled on Cypher.
Related specialised stores
Three other categories are often grouped under NoSQL:
- Search engines (Elasticsearch, OpenSearch) for full-text relevance ranking and log exploration.
- Time-series databases (InfluxDB, Prometheus, TimescaleDB as a PostgreSQL extension) for metrics with timestamps, retention policies and downsampling.
- Vector databases and vector indexes (Qdrant, Milvus, pgvector in PostgreSQL) for similarity search over embeddings.
What you give up
Each family removes something a relational database gives you for free:
- Joins and ad hoc queries. If you cannot list your queries in advance, a key-value or wide-column store will hurt.
- Constraints. Foreign keys, unique constraints across collections and check constraints mostly move into application code.
- Cross-record transactions. Available in some products, with limits and a performance cost.
- Immediate consistency. Many distributed stores default to eventual consistency. Read the defaults; do not assume.
Common mistakes
- Choosing NoSQL for "scale" when the data fits comfortably on one relational server. Most applications never outgrow that.
- Modelling a document or wide-column store like a normalised SQL schema, then issuing several queries per page to stitch the data back together.
- Using a cache as the only copy of data that matters.
- Ignoring that PostgreSQL (
jsonb), MySQL (JSON) and SQL Server all store and index JSON. If you need flexible attributes on a few tables, a JSON column in the database you already run is usually the smaller change.
Further reading
Get the weekly commit
New database deep dives every week.
