Core concepts
NoSQL
"Not only SQL" — an umbrella term for databases that don't use the traditional relational, table-and-JOIN model.
# document (MongoDB), key-value (Redis), wide-column (Cassandra), graph (Neo4j)
💡 Not a single technology — the different NoSQL families solve genuinely different problems, not one problem four ways.
schema-less
Records don't need to follow a predefined, fixed structure — different documents in the same collection can have different fields.
{ "name": "Alice", "role": "admin" }
{ "name": "Bob", "role": "user", "team": "infra" } // extra field, no problem
💡 Flexibility shifts validation responsibility to the application layer — the database won't stop you from writing inconsistent data.
CAP theorem
A distributed system can only guarantee two of three properties at once: Consistency, Availability, and Partition tolerance.
# during a network partition, a system must choose:
# stay consistent (reject some requests) or stay available (risk stale reads)
💡 Since network partitions are unavoidable in practice, the real-world choice is almost always CP vs. AP.
eventual consistency
A guarantee that all replicas will converge to the same value eventually, but reads immediately after a write may return stale data.
# write to replica A succeeds instantly;
# a read from replica B a moment later might still return the old value
💡 A deliberate trade-off for higher availability and lower write latency — not a bug, a design choice.
horizontal scaling
Handling more load by adding more machines (nodes) to a cluster, rather than making one machine bigger.
# 3 nodes not enough? add a 4th — no downtime, no bigger single server needed
💡 The main reason many NoSQL databases exist — they're designed from the ground up to scale this way, unlike most traditional RDBMSs.
Document stores
document
A single self-contained record, usually stored as JSON/BSON, that can nest arrays and objects instead of splitting data across tables.
{
"_id": "u123",
"name": "Alice",
"addresses": [{ "city": "Kyiv" }, { "city": "Lviv" }]
}
💡 The nested addresses array would require a separate table and a JOIN in a relational model.
collection
A group of related documents — the document-database rough equivalent of a table, but without an enforced shared schema.
db.users.insertOne({ name: "Alice" })
💡 Documents within one collection commonly represent the same kind of entity, even without a schema forcing it.
embedding
Nesting related data directly inside a parent document instead of storing it in a separate collection and referencing it.
{ "order_id": "o1", "items": [{ "sku": "abc", "qty": 2 }] }
💡 Great for data that's always read together; bad for data that grows unbounded or is shared across many parents.
referencing
Storing an ID that points to a document in another collection, instead of embedding the full related data.
{ "order_id": "o1", "customer_id": "c42" }
💡 The document-database analogue of a foreign key — but there's no enforced referential integrity by default.
aggregation pipeline
A sequence of processing stages (filter, group, sort, reshape) applied to documents to compute a result — the document-store answer to complex SQL queries.
db.orders.aggregate([
{ $match: { status: "paid" } },
{ $group: { _id: "$customer_id", total: { $sum: "$amount" } } }
])
💡 Each stage passes its output to the next, similar in spirit to piping shell commands together.
Key-value stores
key-value store
The simplest NoSQL model — every piece of data is a value retrieved by a unique key, with no query language beyond "get this key".
SET session:abc123 "user_id=42"
GET session:abc123
💡 Extremely fast lookups by design, at the cost of no complex queries — you can't ask "find all sessions for user 42" without a secondary index.
TTL (key expiry)
Setting a key to automatically delete itself after a specified duration — built into most key-value stores.
SET session:abc123 "..." EX 3600 # expires in 1 hour
💡 The standard mechanism behind session storage, rate-limit counters, and cache entries.
in-memory store
A database that keeps all (or most) data in RAM rather than on disk, trading durability for extreme speed.
# Redis, Memcached — sub-millisecond reads
💡 Many in-memory stores offer optional persistence (periodic snapshots or a write-ahead log) so a restart doesn't lose everything.
cache-aside pattern
The application checks the cache first; on a miss, it reads from the primary database and writes the result into the cache for next time.
value = cache.get(key)
if value is None:
value = db.query(key)
cache.set(key, value, ttl=300)
💡 The most common way key-value stores get used alongside a "real" database, rather than replacing it.
Wide-column stores
wide-column store
A model where each row can have a different, dynamic set of columns, grouped into column families — built for massive write throughput.
# Cassandra, HBase, Bigtable
💡 Designed for write-heavy workloads at huge scale — think time-series and event-logging use cases.
partition key
The field that determines which physical node in the cluster stores a given row — the single most important design decision in a wide-column schema.
# rows partitioned by user_id land on the same node, making per-user queries fast
💡 A poorly chosen partition key causes a "hot partition" — one node overloaded while others sit idle.
column family
A grouping of related columns stored together on disk — designed around how the data is queried, not just how it's logically related.
# a "profile" column family and a separate "activity" column family for the same entity
💡 Schema design here is query-first: you model the table around the questions you'll ask it, not the entity relationships.
tombstone
A marker written in place of a deleted value, since distributed wide-column stores can't just erase data across every replica instantly.
# a delete doesn't remove data immediately -- it writes a tombstone that gets cleaned up later
💡 Too many tombstones accumulating (from frequent deletes) is a well-known Cassandra performance problem.
Graph databases
graph database
Stores data as nodes (entities) and edges (relationships) explicitly, optimized for traversing connections rather than joining tables.
# Neo4j, Amazon Neptune
💡 Shines when the interesting question is about relationships themselves — "friends of friends", fraud rings, recommendation paths.
node (graph)
A single entity in a graph database — a person, product, or place — that can carry its own properties.
CREATE (a:Person { name: "Alice" })
💡 Conceptually similar to a row, but relationships to other nodes are first-class, not a separate join table.
edge / relationship
A directed, typed connection between two nodes, which can itself carry properties (e.g. "since 2020").
MATCH (a:Person), (b:Person)
WHERE a.name = "Alice" AND b.name = "Bob"
CREATE (a)-[:FOLLOWS { since: 2020 }]->(b)
💡 Traversing edges is typically O(1) per hop in a graph database, versus an expensive JOIN in a relational one.
traversal
Walking from node to node along edges to answer a query — the graph-database equivalent of a JOIN-heavy relational query.
MATCH (a:Person)-[:FOLLOWS*1..2]->(b:Person)
RETURN b
💡 A multi-hop traversal that would require several expensive JOINs relationally is often a single fast query in a graph database.
Distributed operations
sharding
Splitting a large dataset across multiple servers (shards), each holding a subset of the data.
# users A-M on shard 1, users N-Z on shard 2
💡 Distinct from replication — sharding splits data for scale; replication copies data for redundancy. Many systems do both.
replica set
A group of database nodes holding copies of the same data, for redundancy and read scaling — one primary, the rest secondaries.
# MongoDB replica set: 1 primary (handles writes) + 2 secondaries (replicate + serve reads)
💡 If the primary fails, the replica set automatically elects a new one — this is what "automatic failover" means in practice.
quorum
The minimum number of nodes that must acknowledge a read or write for it to be considered successful.
# write quorum of 2 out of 3 replicas = tolerates 1 node being down
💡 Tuning read/write quorum is how you trade off consistency, availability, and latency in practice.
denormalization
Deliberately duplicating data across documents/rows to avoid expensive joins at read time — the opposite of relational normalization.
{ "order_id": "o1", "customer_name": "Alice" }
// customer_name duplicated here instead of joined from a customers table
💡 A core NoSQL design principle: optimize for how data is read, and accept the cost of keeping duplicates in sync on writes.