NoSQL
NoSQL is an umbrella term that names four different kinds of database by what they are not, which is why the choice is so often made badly. What they share is more useful than what they lack: data is placed on a node by a key you choose, so that key decides which queries are cheap, which are expensive and which are impossible. This page covers the four families, what a partition key really commits you to, what "eventually consistent" means for your users, and when a relational database is still the right answer.
- Not one thing
- Four families
- Placement
- By a key you choose
- Model for
- Queries, not entities
Four families, and the trade they all made
The label covers four data models that have almost nothing in common with each other. Picking the wrong family is a more expensive mistake than picking the wrong product inside a family, and it happens because the conversation usually starts at "we need something that scales" rather than at what shape the data is.
Key-value
A hash map you can operate. One operation: give me the value for this key. Sessions, caches, rate-limit counters, feature flags.
Document
A key-value store that can look inside the value. Nested records fetched whole, with secondary indexes on their fields.
Wide-column
Rows grouped under a partition key and sorted within it. Built for enormous write volumes and range scans inside one partition.
Graph
Nodes and edges as first-class things. The only family that answers "friends of friends of friends" without a query that grows with the depth.
What the first three share is the mechanism that makes them scale, and it is worth stating plainly because it is also the constraint. Every record is assigned to a node by hashing a key you chose when you designed the table. A request that carries that key goes to exactly one node and stays fast no matter how much data exists in total. A request that does not carry it has to ask every node and merge the answers — or, in several engines, is simply refused.
The second thing the key controls is whether the load is spread at all. Hashing distributes keys evenly only if the keys themselves are varied: partition by country and the node holding your largest market takes most of the traffic while the others idle; partition by day and every write in the system lands on one node until midnight. A hot partition does not announce itself as a hot partition — it looks like the whole cluster being slow, and adding nodes does not help, because the work was never divided.
What "eventually consistent" costs you
These systems keep several copies of every record on different nodes, which is what makes them survive a machine dying. The question every one of them has to answer is how many copies must confirm a write before the client is told it succeeded, and how many must be read before an answer is returned. Answer "all of them" and the system stops working when one node is unreachable. Answer "one" and a read can legitimately return a value that is already out of date. Most engines let you choose per operation, and that choice is a product decision wearing a configuration flag.
The famous framing — consistency, availability, partition tolerance, pick two — is true and almost useless, because networks partition rarely and the trade you actually live with is the everyday one. The more practical version adds the normal case: when the network is fine, you are still choosing between latency and consistency on every single request. Waiting for a majority of replicas is slower than asking the nearest one, and that difference is paid on every read, not only during an incident.
| Guarantee | What the user sees | What it costs |
|---|---|---|
| Eventual | A change may be missing from the next screen, then appear. Two users can briefly disagree. | Nothing extra — this is the cheapest and fastest mode, and the default in most engines. |
| Read your writes | You always see your own changes; other people's may lag behind. | Routing a user's reads to the replica that took their write, or reading from the primary for a while. |
| Quorum | A read reflects the latest acknowledged write, provided reads and writes overlap in a majority. | Latency on every operation, and unavailability when too few replicas can be reached. |
| Single-item transaction | One document or row updates atomically; two of them do not, unless the engine says so. | A modelling constraint: whatever must change together has to live in one item. |
Model for the query, not for the entity
Relational modelling stores each fact once and assembles answers with joins at read time. Without joins, that inversion has to happen at design time: you store the answer in the shape the screen needs, and you accept storing some facts more than once. An order document that carries the customer name and address alongside the line items is not a modelling failure — it is the model. The cost is that when the customer changes their name, several documents have to change, and your code is the only thing that knows which.
That gives you a useful rule for what belongs together. Duplicate a value when it is read far more often than it changes, and when a slightly stale copy is acceptable — a product name on an order line is arguably supposed to be frozen anyway. Keep a reference and pay for a second lookup when the value changes often or when staleness is a defect. And whatever must change atomically has to live inside one item, because that is the largest unit most of these engines will update as a whole.
Secondary indexes exist, and they are the place where the marketing and the mechanics diverge. An index local to a partition is cheap and consistent, because it lives beside the data it indexes. An index that spans partitions has to be maintained across nodes, so it is usually updated asynchronously — meaning a read of that index can miss a row you just wrote — and it reintroduces the cost the partition key was chosen to avoid. They are a real tool for secondary access paths, and a poor substitute for a key that fits the primary one.
This is the right shape
- The access pattern is known and narrow — fetch this thing by this id, over and over.
- Write volume is genuinely beyond one machine, and the key spreads it evenly.
- Records are self-contained: what is read together can be stored together.
- A short window of staleness is acceptable to the people using the product.
This is a relational problem
- New questions arrive that nobody anticipated, and each needs a different access path.
- Several records must change together or not at all — money, stock, bookings.
- The same fact is needed from many angles, so duplicating it multiplies the ways it can drift.
- The volume is large by intuition rather than by measurement, which is most of the time.
How this shows up in real delivery
"Schemaless" is the phrase that costs teams the most, because there is always a schema — the only question is whether it is written down and enforced, or implied by whichever code last touched the collection. After a year, a document store contains three generations of record shape, some with a field, some without, some with the field holding a different type. Every read path then needs to handle all three, and nothing tells you when you have missed one. The teams that stay sane put a version number on every document, validate on write against a schema declared in the application or in the engine, and treat a shape change as a migration with a plan.
Migrations themselves change character. There is no statement that rewrites a column across the whole table, so a shape change becomes a background job that walks every record, and it has to be resumable, rate-limited so it does not starve live traffic, and safe to run twice. Meanwhile the application reads both old and new shapes, which means the migration is not finished when the job finishes — it is finished when the compatibility code is removed, and that step is the one nobody schedules.
Finally, the honest comparison. Modern relational engines do most of what a document store was adopted for: they store and index JSON, they partition tables, they replicate, and a single primary handles write volumes that are far beyond what most products will ever produce. Running one database instead of two saves a backup story, a monitoring story, a failure mode and a set of people who understand it. Reach for a second engine when a measured requirement demands it — write volume past one machine, a graph traversal, a cache with a millisecond budget — and be able to say which of those it is. "We might need to scale" is not one of them.
Where it degrades
- Choosing a family because of a scale story rather than because of the shape of the data.
- A partition key chosen before the queries are known, which the first new screen then invalidates.
- A low-cardinality key — country, day, status — that concentrates traffic on one node.
- Scatter-gather queries added one at a time, until every read touches the whole cluster.
- Relying on last-write-wins in a product where two clients can genuinely edit the same record.
- No document version and no write-time validation, so three record shapes coexist unnoticed.
- Rebuilding joins in the application, which is the same work without the planner or the index.
- A second database adopted for one feature, and operated by nobody in particular thereafter.
When to use it
Use it when
- A known, narrow access pattern at a volume that genuinely exceeds one machine — events, telemetry, feeds.
- Records that are read and written whole and depend on nothing else — sessions, carts, device state.
- Caches, counters and queues, where a key-value store is simpler and faster than any alternative.
- Relationship traversal of unknown depth, which is what a graph engine exists to answer.
Avoid it when
- Anything needing several records to change together, unless the engine offers real multi-item transactions.
- Reporting and ad-hoc analysis, where the questions are not known and each one needs a different path.
- A product still discovering its own access patterns, since the key you must pick first is the thing still moving.
- Volume you have assumed rather than measured, where a relational database would have carried it comfortably.
Found this useful?
Share it with someone who is working on the same problem.