Cơ sở dữ liệu NoSQLNoSQL Databases
Thuộc bộ kiến thức Backend Roadmap.
Tổng quan
NoSQL (“Not Only SQL”) là thuật ngữ chung cho các database không dùng mô hình relational truyền thống gồm bảng, hàng, và schema cứng nhắc liên kết qua foreign key và join. Thay vào đó, mỗi họ NoSQL được tối ưu cho một hình dạng dữ liệu và pattern truy cập cụ thể: tra cứu đơn giản theo key, document lồng nhau, hàng thưa (wide row), hay đồ thị các thực thể liên kết.
Các database NoSQL xuất hiện vào giữa đến cuối những năm 2000, xuất phát từ nhu cầu của các công ty vận hành ở quy mô mà một server relational đơn lẻ khó đáp ứng. Google (Bigtable, 2006), Amazon (bài báo Dynamo, 2007) và Facebook (Cassandra, 2008) đều công bố hoặc open-source các hệ thống được xây để xử lý workload phân tán khổng lồ với throughput cao, nơi horizontal scaling (thêm nhiều máy commodity) quan trọng hơn khả năng truy vấn phong phú và các đảm bảo mạnh của một relational database (RDBMS).
Đánh đổi cốt lõi của NoSQL: nó nới lỏng một số thứ mà relational database đảm bảo — schema cố định, join nhiều bảng, và thường là ACID transaction mạnh — để đổi lấy sự linh hoạt, horizontal scalability, và hiệu năng trên các pattern truy cập cụ thể. NoSQL không phải là thứ thay thế SQL; nó là một bộ công cụ chuyên biệt mà bạn dùng khi mô hình relational không phù hợp.
Ghi chú này giả định bạn đã quen với Relational Databases và liên kết tới Database Design & Scaling cùng Caching.
Kiến thức nền tảng
Vì sao NoSQL xuất hiện
Relational database là kho lưu trữ đa dụng tuyệt vời, nhưng nhiều yếu tố đã bộc lộ giới hạn của chúng ở quy mô lớn:
- Khối lượng và tốc độ ở quy mô web — mạng xã hội, IoT, log, và clickstream tạo ra hàng tỷ lượt write khó phục vụ từ một node primary duy nhất.
- Horizontal scaling — RDBMS truyền thống scale lên (máy lớn hơn). Sharding một relational database với join và transaction xuyên shard rất khó. Các hệ NoSQL được thiết kế để scale ra trên nhiều node ngay từ đầu.
- Sự linh hoạt về schema — phát triển agile thay đổi hình dạng dữ liệu liên tục. Schema cứng và migration làm chậm team; dữ liệu bán cấu trúc (JSON) phù hợp tự nhiên với nhiều tính năng sản phẩm.
- Pattern truy cập chuyên biệt — một cache cần tra cứu key trong microsecond; một recommendation engine cần duyệt graph; một feed cần đọc timeline của một user thật nhanh. Lưu trữ relational “một cỡ cho tất cả” không phải lúc nào cũng tối ưu.
- Chi phí — scale ra trên phần cứng commodity hoặc dịch vụ cloud managed có thể rẻ hơn so với việc mua server relational ngày càng lớn.
Đánh đổi giữa SQL và NoSQL
| Khía cạnh | Relational (SQL) | NoSQL |
|---|---|---|
| Data model | Bảng, hàng, cột; đã normalize | Key-value, document, wide-column, graph |
| Schema | Cố định, bắt buộc (schema-on-write) | Linh hoạt / động (thường schema-on-read) |
| Ngôn ngữ truy vấn | SQL (chuẩn hóa) | API/DSL riêng theo sản phẩm (một số giống SQL) |
| Join | Hạng nhất, mạnh mẽ | Hạn chế hoặc không có; ưu tiên denormalization |
| Transaction | ACID mạnh xuyên hàng/bảng | Thường hạn chế; tùy sản phẩm |
| Scaling | Chủ yếu vertical; sharding khó | Horizontal scaling là mục tiêu thiết kế hạng nhất |
| Consistency | Mạnh theo mặc định | Điều chỉnh được; thường eventual consistency |
| Phù hợp nhất cho | Truy vấn phức tạp, quan hệ, tính toàn vẹn | Throughput cao, big data, hình dạng linh hoạt/chuyên biệt |
Khi nào chọn cái nào
Chọn relational database khi:
- Dữ liệu có cấu trúc chặt và quan hệ quan trọng (đơn hàng, hóa đơn, kế toán).
- Bạn cần ACID transaction xuyên nhiều hàng/nhiều bảng (chuyển tiền, tồn kho).
- Truy vấn của bạn tùy biến và đa dạng — bạn chưa biết trước tất cả pattern truy cập.
- Tính toàn vẹn dữ liệu và constraint là tối quan trọng.
Chọn NoSQL khi:
- Bạn cần scale write/read theo chiều ngang vượt quá khả năng của một node.
- Pattern truy cập đã biết trước và đơn giản (tra cứu theo key, đọc một document).
- Dữ liệu bán cấu trúc, phân cấp, hoặc schema thay đổi thường xuyên.
- Bạn có hình dạng chuyên biệt: caching (key-value), catalog (document), time-series/event log (wide-column), dữ liệu nặng về quan hệ (graph).
Hầu hết hệ thống thực tế đều polyglot: PostgreSQL cho dữ liệu transactional cốt lõi, Redis cho caching/session, Elasticsearch cho search, và có thể thêm một graph hoặc document store cho một tính năng cụ thể.
CAP theorem và các mô hình consistency
CAP theorem phát biểu rằng một kho dữ liệu phân tán chỉ có thể đảm bảo tối đa hai trong ba thuộc tính sau khi xảy ra network partition:
- Consistency (C) — mọi lượt read nhận được kết quả write mới nhất (hoặc lỗi).
- Availability (A) — mọi request nhận được phản hồi (không lỗi), nhưng không đảm bảo đó là dữ liệu mới nhất.
- Partition tolerance (P) — hệ thống vẫn hoạt động dù mạng bị chia cắt giữa các node.
Vì network partition là không thể tránh trong mọi hệ phân tán, P không phải lựa chọn tùy ý — bạn buộc phải chịu được partition. Lựa chọn thực sự khi partition xảy ra là C hay A:
- Hệ CP (ví dụ HBase, MongoDB ở cấu hình mặc định, Zookeeper) ưu tiên consistency: có thể từ chối request để tránh trả về dữ liệu cũ.
- Hệ AP (ví dụ Cassandra, DynamoDB, Riak) ưu tiên availability: vẫn cho write và hòa giải (reconcile) sau, chấp nhận inconsistency tạm thời.
Các database hiện đại thường cho phép điều chỉnh theo từng operation (xem quorum read/write bên dưới), nên “CP vs AP” là một dải liên tục chứ không phải nhãn cố định.
Các mô hình consistency:
- Strong consistency — sau khi một write hoàn tất, mọi read tiếp theo đều thấy nó. Dễ suy luận nhất; đánh đổi bằng latency/availability.
- Eventual consistency — các replica sẽ hội tụ “cuối cùng” nếu ngừng write; một read có thể tạm thời trả về dữ liệu cũ. Tuyệt cho availability và throughput.
- Read-your-writes / causal / monotonic — các mô hình trung gian cho các đảm bảo hữu ích mà không cần strong consistency đầy đủ.
ACID vs BASE
| ACID (RDBMS điển hình) | BASE (NoSQL điển hình) | |
|---|---|---|
| Viết tắt của | Atomicity, Consistency, Isolation, Durability | Basically Available, Soft state, Eventual consistency |
| Mục tiêu | Tính đúng đắn và an toàn | Availability và scale |
| Consistency | Mạnh, tức thì | Eventual (hội tụ theo thời gian) |
| Transaction | Nhiều lệnh, tất-cả-hoặc-không | Thường single-item; hạn chế multi-item |
| Trạng thái | Luôn hợp lệ sau mỗi transaction | Có thể inconsistent tạm thời |
| Đánh đổi | Latency/availability lấy tính đúng đắn | Tính đúng đắn (ngắn hạn) lấy availability |
Lưu ý: ranh giới đang mờ dần. MongoDB hỗ trợ ACID transaction đa document; DynamoDB hỗ trợ transaction; Google Spanner và CockroachDB cung cấp strong consistency phân tán. Hãy chọn dựa trên đảm bảo thực tế mà sản phẩm cung cấp, không phải dựa trên cái nhãn.
Khái niệm chính
Bốn họ NoSQL nhìn nhanh
| Họ | Data model | Truy vấn theo | Sản phẩm tiêu biểu | Use case điển hình |
|---|---|---|---|---|
| Key-value | Value dạng “hộp đen” dưới một key duy nhất | Chỉ theo key | Redis, Memcached, DynamoDB, Riak | Cache, session, feature flag, counter |
| Document | Document JSON/BSON tự mô tả | Key + truy vấn theo field | MongoDB, CouchDB, Firestore | Catalog, user profile, CMS, dữ liệu app |
| Wide-column | Hàng với cột động, gom theo column family | Partition key + clustering | Cassandra, ScyllaDB, HBase, Bigtable | Time-series, event log, feed ở quy mô lớn |
| Graph | Node + edge kèm property | Traversal / pattern match | Neo4j, Dgraph, Amazon Neptune | Social graph, fraud, recommendation |
Key-value store
Mô hình đơn giản nhất: một hash map khổng lồ, phân tán. Bạn lưu một value (database coi như bytes “hộp đen”) dưới một key duy nhất, và lấy lại theo key đó. Cực nhanh và dễ scale, nhưng thường bạn không thể truy vấn theo nội dung của value.
Sản phẩm: Redis (in-memory, nhiều cấu trúc dữ liệu phong phú, tùy chọn persistence), Memcached (cache in-memory, không persistence), DynamoDB (managed, bền, scale được), Riak (phân tán, AP, kiểu Dynamo).
Ví dụ (Redis):
# Lưu session kèm thời gian hết hạn
SET session:abc123 '{"userId":42,"role":"admin"}' EX 3600
GET session:abc123
# Counter atomic cho rate limiting
INCR api:calls:user:42
EXPIRE api:calls:user:42 60
# Bảng xếp hạng dùng sorted set
ZADD leaderboard 1500 "player:alice"
ZADD leaderboard 2300 "player:bob"
ZREVRANGE leaderboard 0 9 WITHSCORES # top 10
Ngoài string thuần, Redis còn cung cấp list, set, sorted set, hash, stream, và pub/sub — đó là lý do nó được dùng vượt xa caching đơn thuần. Xem Caching.
Document store
Lưu các document — bản ghi tự mô tả, lồng nhau ở dạng JSON hoặc BSON. Document được gom vào các collection. Không có schema cố định, nên các document khác nhau trong cùng collection có thể có field khác nhau. Bạn có thể truy vấn trên bất kỳ field nào, tạo index, và cập nhật từng phần lồng nhau của document.
Sản phẩm: MongoDB (phổ biến nhất; truy vấn phong phú, aggregation pipeline, transaction), CouchDB (API HTTP/JSON, đồng thời đa phiên bản, đồng bộ offline), Firebase/Firestore (managed, listener real-time, ưu tiên mobile).
Ví dụ (MongoDB):
// Một document sản phẩm — dữ liệu liên quan được embed, không join
db.products.insertOne({
_id: "SKU-1001",
name: "Mechanical Keyboard",
price: 89.99,
tags: ["electronics", "peripherals"],
specs: { switches: "brown", layout: "TKL", backlight: true },
reviews: [
{ user: "alice", rating: 5, text: "Great feel" },
{ user: "bob", rating: 4, text: "A bit loud" }
]
});
// Truy vấn theo field lồng nhau + phần tử trong array
db.products.find({
"specs.switches": "brown",
tags: "peripherals",
price: { $lt: 100 }
});
// Aggregation: rating trung bình mỗi sản phẩm
db.products.aggregate([
{ $unwind: "$reviews" },
{ $group: { _id: "$_id", avgRating: { $avg: "$reviews.rating" } } }
]);
Việc embed dữ liệu liên quan (reviews nằm trong product) tránh được join và cho phép đọc trọn sản phẩm trong một truy vấn — một ý tưởng modeling cốt lõi của NoSQL.
Wide-column store
Còn gọi là column-family store. Dữ liệu được tổ chức thành các hàng định danh bởi partition key, và mỗi hàng có thể có tập cột động, lớn. Các hàng được phân bố qua các node theo partition key; trong một partition, hàng được sắp xếp theo clustering column. Mô hình này được xây cho throughput write khổng lồ và read dự đoán được trên các partition đã biết — nhưng bạn phải thiết kế bảng quanh các truy vấn của mình, vì nó không hỗ trợ truy vấn tùy biến và join.
Sản phẩm: Cassandra (AP, consistency điều chỉnh được, không có single point of failure), ScyllaDB (tương thích Cassandra, viết bằng C++, hiệu năng cao hơn), HBase (chạy trên HDFS, CP, kiểu Bigtable), Google Bigtable (managed, vận hành Search/Maps/Analytics).
Ví dụ (Cassandra CQL):
-- Thiết kế bảng cho truy vấn: "các event gần đây của một device, mới nhất trước"
CREATE TABLE events_by_device (
device_id text,
event_time timestamp,
event_type text,
payload text,
PRIMARY KEY (device_id, event_time)
) WITH CLUSTERING ORDER BY (event_time DESC);
-- Write
INSERT INTO events_by_device (device_id, event_time, event_type, payload)
VALUES ('dev-42', '2026-07-19T10:00:00Z', 'temp', '{"c":21.5}');
-- Read: hiệu quả, chỉ chạm một partition, đã được sắp xếp sẵn
SELECT * FROM events_by_device
WHERE device_id = 'dev-42'
LIMIT 100;
Partition key (device_id) quyết định node nào giữ dữ liệu; clustering column (event_time) quyết định thứ tự sắp xếp bên trong. Chọn partition key tốt (để tránh “hot partition” và partition phình vô hạn) là kỹ năng trung tâm.
Graph database
Mô hình hóa dữ liệu thành các node (thực thể) nối với nhau bởi các edge (quan hệ), cả hai đều có thể mang property. Chúng vượt trội khi quan hệ là trọng tâm — duyệt “bạn của bạn”, tìm đường ngắn nhất, hoặc phát hiện các vòng tài khoản fraud liên kết — những truy vấn sẽ cần rất nhiều join tốn kém trong SQL.
Sản phẩm: Neo4j (phổ biến nhất; ngôn ngữ truy vấn Cypher), Dgraph (phân tán, GraphQL-native), Amazon Neptune (managed; hỗ trợ Gremlin và SPARQL).
Ví dụ (Neo4j Cypher):
// Tạo người và một mối quan hệ bạn bè
CREATE (alice:Person {name: 'Alice'})
CREATE (bob:Person {name: 'Bob'})
CREATE (carol:Person {name: 'Carol'})
CREATE (alice)-[:FRIENDS_WITH]->(bob)
CREATE (bob)-[:FRIENDS_WITH]->(carol);
// Bạn-của-bạn của Alice mà cô ấy chưa là bạn
MATCH (alice:Person {name: 'Alice'})-[:FRIENDS_WITH]->(:Person)
-[:FRIENDS_WITH]->(fof:Person)
WHERE NOT (alice)-[:FRIENDS_WITH]->(fof) AND fof <> alice
RETURN DISTINCT fof.name;
Chi phí traversal phụ thuộc vào số quan hệ đi qua, không phải tổng kích thước dataset — nên truy vấn quan hệ sâu vẫn nhanh dù dữ liệu tăng lên.
Các kho dữ liệu chuyên biệt
Ngoài bốn họ kinh điển, một số kho được xây cho mục đích riêng cũng rất phổ biến trong backend:
- Search engine — Elasticsearch, OpenSearch, Apache Solr. Xây trên inverted index cho full-text search, chấm điểm relevance, faceting, và analytics. Thường ghép với một database chính (search trên một bản sao của dữ liệu).
- Time-series database (TSDB) — InfluxDB, TimescaleDB (một extension của PostgreSQL), Prometheus. Tối ưu cho metric có timestamp: ingest cao, downsampling, retention policy, truy vấn theo cửa sổ thời gian.
- Kho analytics / OLAP dạng columnar — ClickHouse, Apache Druid, Apache Pinot. Lưu dữ liệu theo cột để aggregation nhanh trên hàng tỷ dòng; phục vụ dashboard và analytics, không phải write transactional.
- Vector database — Pinecone, Milvus, Weaviate, Qdrant, pgvector. Lưu embedding chiều cao và hỗ trợ tìm kiếm tương đồng (nearest-neighbor) — xương sống của semantic search và Retrieval-Augmented Generation (RAG). Xem AI for Backend.
Data modeling cho NoSQL
Modeling cho NoSQL khác biệt về bản chất so với modeling relational. Trong SQL bạn normalize trước rồi để join ghép lại dữ liệu. Trong NoSQL bạn mô hình quanh các pattern truy cập và thường denormalize mạnh tay.
- Thiết kế hướng access-pattern — liệt kê mọi truy vấn ứng dụng cần trước khi thiết kế bảng/collection. Cấu trúc dữ liệu sao cho mỗi truy vấn chạm một partition/document/key. Bạn tối ưu cho read, không phải cho cấu trúc gọn gàng.
- Denormalization — nhân bản dữ liệu để một lần read trả về mọi thứ cần thiết (ví dụ lưu tên tác giả trong mỗi bài post, không chỉ author ID). Đánh đổi storage và độ phức tạp khi write để lấy tốc độ read. Giữ các bản sao đồng bộ trở thành việc của ứng dụng.
- Embedding vs referencing (document) — embed dữ liệu được đọc cùng nhau và thuộc sở hữu của cha (line item của đơn hàng nằm trong đơn hàng); reference dữ liệu lớn, dùng chung, hoặc cập nhật độc lập (một user được nhiều post tham chiếu).
- Aggregate — mô hình quanh đơn vị bạn đọc và ghi như một khối. Một document/hàng lý tưởng nên khớp với một thao tác nghiệp vụ.
Single-table design (DynamoDB): Một pattern nâng cao nổi tiếng, trong đó nhiều loại entity nằm trong một bảng, phân biệt bằng partition key (PK) và sort key (SK) được ghép khéo léo. Điều này cho phép một truy vấn lấy về các item liên quan (ví dụ một user và tất cả đơn hàng của họ) mà không cần join.
| PK | SK | Attributes |
|---|---|---|
USER#42 | PROFILE#42 | name, email |
USER#42 | ORDER#1001 | total, status, date |
USER#42 | ORDER#1002 | total, status, date |
# Một truy vấn trả về profile CỦA user VÀ tất cả đơn hàng của họ:
Query where PK = "USER#42"
# Lọc chỉ lấy đơn hàng:
Query where PK = "USER#42" AND begins_with(SK, "ORDER#")
Nguyên tắc thiết kế: vì DynamoDB không có join và tính phí theo từng request, bạn đặt các item liên quan cùng một partition key để đọc chúng trong một truy vấn hiệu quả.
So sánh giữa các họ
| Key-value | Document | Wide-column | Graph | |
|---|---|---|---|---|
| Data model | Key → value hộp đen | Collection các document JSON | Hàng có partition, cột động | Node + edge + property |
| Khả năng truy vấn | Chỉ theo key | Truy vấn phong phú trên mọi field | Partition/clustering key; ad hoc hạn chế | Traversal, pattern matching |
| Consistency | Điều chỉnh được (thường eventual) | Điều chỉnh; MongoDB mặc định mạnh trên primary | Điều chỉnh (Cassandra quorum) | Thường mạnh (Neo4j ACID) |
| Scaling | Horizontal xuất sắc | Tốt (sharding) | Horizontal xuất sắc | Khó phân tán hơn |
| Use case tốt nhất | Cache, session, counter | Dữ liệu app, catalog, profile | Time-series, log, feed quy mô lớn | Quan hệ, social, fraud |
| Điểm yếu | Truy vấn theo value | Join xuyên document | Truy vấn ad hoc, join | Write cực lớn, sharding |
Best Practices
- Mô hình quanh pattern truy cập, không quanh entity. Liệt kê các truy vấn read/write trước, rồi thiết kế storage sao cho mỗi cái là một operation hiệu quả duy nhất. Đây là điều ngược với normalization relational.
- Đừng ép mọi thứ vào một database. Dùng polyglot persistence: relational cho dữ liệu transactional cốt lõi, key-value cho caching, search engine cho text, và một kho chuyên biệt chỉ khi nó thắng rõ rệt.
- Hiểu nhu cầu consistency theo từng operation. Dùng strong consistency ở nơi tính đúng đắn quan trọng (thanh toán), eventual consistency ở nơi availability/latency quan trọng (feed, đếm view). Nhiều sản phẩm cho phép điều chỉnh theo từng truy vấn (ví dụ Cassandra
QUORUM, DynamoDB read strongly-consistent). - Chọn partition/shard key cẩn thận. Key kém tạo ra hot partition (tải không đều) hoặc partition không giới hạn (phình vô hạn). Nhắm tới key có cardinality cao, phân bố đều.
- Denormalize có chủ đích, và tự chịu trách nhiệm về consistency. Nhân bản dữ liệu tăng tốc read nhưng bạn phải giữ các bản sao đồng bộ khi write — thường qua logic ứng dụng, job nền, hoặc luồng change-data-capture.
- Đặt TTL và retention. Với cache, session, và time-series, dùng expiry/retention policy để dữ liệu không phình vô hạn.
- Index cho truy vấn của bạn, nhưng đừng mù quáng. Index tăng tốc read nhưng làm chậm write và tốn storage. Chỉ index các field bạn thực sự filter/sort.
- Nắm rõ giới hạn transaction. Nếu cần atomicity đa item, hãy xác minh sản phẩm hỗ trợ (MongoDB multi-doc transaction, DynamoDB
TransactWriteItems) và hiểu chi phí. - Lên kế hoạch cho lỗi và hội tụ eventual. Trong hệ AP, thiết kế write idempotent và cơ chế resolve xung đột (last-write-wins, vector clock, CRDT) để các replica reconcile đúng.
- Benchmark với dữ liệu và pattern truy cập thực tế. Hiệu năng NoSQL phụ thuộc rất nhiều vào key và hình dạng truy vấn; hãy test ở quy mô đại diện trước khi cam kết.
Tài liệu tham khảo
Part of the Backend Roadmap knowledge base.
Overview
NoSQL (“Not Only SQL”) is an umbrella term for databases that do not use the traditional relational model of tables, rows, and rigid schemas connected by foreign keys and joins. Instead, each NoSQL family optimizes for a specific data shape and access pattern: simple lookups by key, nested documents, wide sparse rows, or connected graphs.
NoSQL databases emerged in the mid-to-late 2000s, driven by companies operating at a scale that single relational servers struggled with. Google (Bigtable, 2006), Amazon (Dynamo paper, 2007), and Facebook (Cassandra, 2008) each published or open-sourced systems built to handle massive, distributed, high-throughput workloads where horizontal scaling (adding more commodity machines) mattered more than the rich querying and strong guarantees of a relational database (RDBMS).
The core trade NoSQL makes: it relaxes some of what relational databases guarantee — a fixed schema, multi-table joins, and often strong ACID transactions — in exchange for flexibility, horizontal scalability, and performance on specific access patterns. NoSQL is not a replacement for SQL; it is a set of specialized tools you reach for when the relational model is a poor fit.
This note assumes familiarity with Relational Databases and connects to Database Design & Scaling and Caching.
Fundamentals
Why NoSQL emerged
Relational databases are excellent general-purpose stores, but several forces exposed their limits at scale:
- Web-scale volume and velocity — social networks, IoT, logs, and clickstreams generate billions of writes that are hard to serve from a single primary node.
- Horizontal scaling — RDBMS traditionally scale up (bigger machines). Sharding a relational database with cross-shard joins and transactions is painful. NoSQL systems were designed to scale out across many nodes from the start.
- Schema flexibility — agile development changes data shapes constantly. Rigid schemas and migrations slow teams down; semi-structured data (JSON) is a natural fit for many product features.
- Specialized access patterns — a cache needs microsecond key lookups; a recommendation engine needs graph traversals; a feed needs to read one user’s timeline fast. One-size-fits-all relational storage is not always optimal.
- Cost — scaling out on commodity hardware or managed cloud services can be cheaper than ever-larger relational servers.
SQL vs NoSQL trade-offs
| Aspect | Relational (SQL) | NoSQL |
|---|---|---|
| Data model | Tables, rows, columns; normalized | Key-value, document, wide-column, graph |
| Schema | Fixed, enforced (schema-on-write) | Flexible / dynamic (often schema-on-read) |
| Query language | SQL (standardized) | Product-specific APIs/DSLs (some SQL-like) |
| Joins | First-class, powerful | Limited or none; prefer denormalization |
| Transactions | Strong ACID across rows/tables | Often limited; varies by product |
| Scaling | Primarily vertical; sharding is hard | Horizontal scaling is a first-class design goal |
| Consistency | Strong by default | Tunable; often eventual consistency |
| Best for | Complex queries, relationships, integrity | High throughput, big data, flexible/specialized shapes |
When to choose which
Reach for a relational database when:
- Your data is highly structured and relationships matter (orders, invoices, accounting).
- You need multi-row/multi-table ACID transactions (money movement, inventory).
- Your queries are ad hoc and varied — you don’t know all access patterns up front.
- Data integrity and constraints are critical.
Reach for NoSQL when:
- You need to scale writes/reads horizontally beyond what one node handles.
- Your access patterns are known and simple (lookup by key, read a document).
- Your data is semi-structured, hierarchical, or the schema changes often.
- You have a specialized shape: caching (key-value), catalogs (document), time-series/event logs (wide-column), relationship-heavy data (graph).
Most real systems are polyglot: PostgreSQL for core transactional data, Redis for caching/sessions, Elasticsearch for search, and perhaps a graph or document store for a specific feature.
CAP theorem and consistency models
The CAP theorem states that a distributed data store can guarantee at most two of the following three properties when a network partition occurs:
- Consistency (C) — every read receives the most recent write (or an error).
- Availability (A) — every request receives a (non-error) response, without guaranteeing it has the latest write.
- Partition tolerance (P) — the system keeps operating despite network splits between nodes.
Since network partitions are unavoidable in any distributed system, P is not optional — you must tolerate partitions. The real choice during a partition is C vs A:
- CP systems (e.g., HBase, MongoDB in default configs, Zookeeper) prefer consistency: they may reject requests to avoid returning stale data.
- AP systems (e.g., Cassandra, DynamoDB, Riak) prefer availability: they stay writable and reconcile later, accepting temporary inconsistency.
Modern databases often make this tunable per operation (see quorum reads/writes below), so “CP vs AP” is a spectrum, not a fixed label.
Consistency models:
- Strong consistency — after a write completes, all subsequent reads see it. Simplest to reason about; costs latency/availability.
- Eventual consistency — replicas converge “eventually” if writes stop; a read may briefly return stale data. Great for availability and throughput.
- Read-your-writes / causal / monotonic — intermediate models that give useful guarantees without full strong consistency.
ACID vs BASE
| ACID (typical RDBMS) | BASE (typical NoSQL) | |
|---|---|---|
| Stands for | Atomicity, Consistency, Isolation, Durability | Basically Available, Soft state, Eventual consistency |
| Goal | Correctness and safety | Availability and scale |
| Consistency | Strong, immediate | Eventual (converges over time) |
| Transactions | Multi-statement, all-or-nothing | Often single-item; limited multi-item |
| State | Always valid after each transaction | May be temporarily inconsistent |
| Trade-off | Latency/availability for correctness | Correctness (short-term) for availability |
Note: the line is blurring. MongoDB supports multi-document ACID transactions; DynamoDB supports transactions; Google Spanner and CockroachDB offer distributed strong consistency. Choose based on the actual guarantees a product offers, not the label.
Key Concepts
The four families at a glance
| Family | Data model | Query by | Representative products | Typical use case |
|---|---|---|---|---|
| Key-value | Opaque value under a unique key | Key only | Redis, Memcached, DynamoDB, Riak | Cache, sessions, feature flags, counters |
| Document | Self-describing JSON/BSON documents | Key + rich field queries | MongoDB, CouchDB, Firestore | Catalogs, user profiles, CMS, app data |
| Wide-column | Rows with dynamic columns, grouped in families | Partition key + clustering | Cassandra, ScyllaDB, HBase, Bigtable | Time-series, event logs, feeds at scale |
| Graph | Nodes + edges with properties | Traversals / pattern match | Neo4j, Dgraph, Amazon Neptune | Social graphs, fraud, recommendations |
Key-value stores
The simplest model: a giant, distributed hash map. You store a value (which the database treats as opaque bytes) under a unique key, and retrieve it by that key. Extremely fast and easy to scale, but you generally cannot query by the value’s contents.
Products: Redis (in-memory, rich data structures, optional persistence), Memcached (in-memory cache, no persistence), DynamoDB (managed, durable, scalable), Riak (distributed, AP, Dynamo-style).
Example (Redis):
# Session store with expiry
SET session:abc123 '{"userId":42,"role":"admin"}' EX 3600
GET session:abc123
# Atomic counter for rate limiting
INCR api:calls:user:42
EXPIRE api:calls:user:42 60
# Leaderboard using a sorted set
ZADD leaderboard 1500 "player:alice"
ZADD leaderboard 2300 "player:bob"
ZREVRANGE leaderboard 0 9 WITHSCORES # top 10
Beyond plain strings, Redis provides lists, sets, sorted sets, hashes, streams, and pub/sub — which is why it’s used far beyond simple caching. See Caching.
Document stores
Store documents — self-describing, nested records in JSON or BSON. Documents are grouped into collections. There is no fixed schema, so different documents in a collection can have different fields. You can query on any field, index fields, and update nested parts of a document.
Products: MongoDB (most popular; rich queries, aggregation pipeline, transactions), CouchDB (HTTP/JSON API, multi-version concurrency, offline sync), Firebase/Firestore (managed, real-time listeners, mobile-first).
Example (MongoDB):
// A product document — related data embedded, not joined
db.products.insertOne({
_id: "SKU-1001",
name: "Mechanical Keyboard",
price: 89.99,
tags: ["electronics", "peripherals"],
specs: { switches: "brown", layout: "TKL", backlight: true },
reviews: [
{ user: "alice", rating: 5, text: "Great feel" },
{ user: "bob", rating: 4, text: "A bit loud" }
]
});
// Query by nested field + array membership
db.products.find({
"specs.switches": "brown",
tags: "peripherals",
price: { $lt: 100 }
});
// Aggregation: average rating per product
db.products.aggregate([
{ $unwind: "$reviews" },
{ $group: { _id: "$_id", avgRating: { $avg: "$reviews.rating" } } }
]);
Embedding related data (reviews inside the product) avoids joins and lets you read the whole product in one query — a core NoSQL modeling idea.
Wide-column stores
Also called column-family stores. Data is organized into rows identified by a partition key, and each row can have a large, dynamic set of columns. Rows are distributed across nodes by partition key; within a partition, rows are sorted by clustering columns. This model is built for massive write throughput and predictable reads on known partitions — but you must design tables around your queries, since ad hoc querying and joins are not supported.
Products: Cassandra (AP, tunable consistency, no single point of failure), ScyllaDB (Cassandra-compatible, C++, higher performance), HBase (on HDFS, CP, Bigtable-style), Google Bigtable (managed, powers Search/Maps/Analytics).
Example (Cassandra CQL):
-- Model the table for the query: "recent events for a device, newest first"
CREATE TABLE events_by_device (
device_id text,
event_time timestamp,
event_type text,
payload text,
PRIMARY KEY (device_id, event_time)
) WITH CLUSTERING ORDER BY (event_time DESC);
-- Write
INSERT INTO events_by_device (device_id, event_time, event_type, payload)
VALUES ('dev-42', '2026-07-19T10:00:00Z', 'temp', '{"c":21.5}');
-- Read: efficient, hits one partition, already sorted
SELECT * FROM events_by_device
WHERE device_id = 'dev-42'
LIMIT 100;
The partition key (device_id) determines which node holds the data; the clustering column (event_time) determines sort order within it. Choosing partition keys well (to avoid “hot partitions” and unbounded partition growth) is the central skill.
Graph databases
Model data as nodes (entities) connected by edges (relationships), both of which can hold properties. They excel when relationships are the point — traversing “friends of friends”, finding shortest paths, or detecting rings of connected fraud accounts — queries that would require many expensive joins in SQL.
Products: Neo4j (most popular; Cypher query language), Dgraph (distributed, GraphQL-native), Amazon Neptune (managed; supports Gremlin and SPARQL).
Example (Neo4j Cypher):
// Create people and a friendship
CREATE (alice:Person {name: 'Alice'})
CREATE (bob:Person {name: 'Bob'})
CREATE (carol:Person {name: 'Carol'})
CREATE (alice)-[:FRIENDS_WITH]->(bob)
CREATE (bob)-[:FRIENDS_WITH]->(carol);
// Friends-of-friends of Alice she isn't already friends with
MATCH (alice:Person {name: 'Alice'})-[:FRIENDS_WITH]->(:Person)
-[:FRIENDS_WITH]->(fof:Person)
WHERE NOT (alice)-[:FRIENDS_WITH]->(fof) AND fof <> alice
RETURN DISTINCT fof.name;
Traversal cost depends on the number of relationships walked, not the total dataset size — so deep relationship queries stay fast even as data grows.
Specialized data stores
Beyond the four classic families, several purpose-built stores are common in backends:
- Search engines — Elasticsearch, OpenSearch, Apache Solr. Built on inverted indexes for full-text search, relevance scoring, faceting, and analytics. Often paired with a primary database (search over a mirror of the data).
- Time-series databases (TSDB) — InfluxDB, TimescaleDB (a PostgreSQL extension), Prometheus. Optimized for timestamped metrics: high ingest, downsampling, retention policies, time-window queries.
- Analytics / OLAP columnar stores — ClickHouse, Apache Druid, Apache Pinot. Store data by column for fast aggregations over billions of rows; power dashboards and analytics, not transactional writes.
- Vector databases — Pinecone, Milvus, Weaviate, Qdrant, pgvector. Store high-dimensional embeddings and support similarity (nearest-neighbor) search — the backbone of semantic search and Retrieval-Augmented Generation (RAG). See AI for Backend.
Data modeling for NoSQL
Modeling for NoSQL is fundamentally different from relational modeling. In SQL you normalize first and let joins reassemble data. In NoSQL you model around access patterns and often denormalize aggressively.
- Access-pattern-driven design — list every query your application needs before designing tables/collections. Structure data so each query hits one partition/document/key. You optimize for reads, not for tidy structure.
- Denormalization — duplicate data so a single read returns everything needed (e.g., store the author’s name inside each post, not just an author ID). This trades storage and write complexity for read speed. Keeping duplicates in sync becomes the application’s job.
- Embedding vs referencing (documents) — embed data that is read together and owned by the parent (order line items inside an order); reference data that is large, shared, or independently updated (a user referenced by many posts).
- Aggregates — model around the unit you read and write as a whole. A document/row should ideally match one business operation.
Single-table design (DynamoDB): A well-known advanced pattern where multiple entity types live in one table, distinguished by carefully composed partition keys (PK) and sort keys (SK). This lets a single query fetch related items (e.g., a user and all their orders) without joins.
| PK | SK | Attributes |
|---|---|---|
USER#42 | PROFILE#42 | name, email |
USER#42 | ORDER#1001 | total, status, date |
USER#42 | ORDER#1002 | total, status, date |
# One query returns the user profile AND all their orders:
Query where PK = "USER#42"
# Filter to just orders:
Query where PK = "USER#42" AND begins_with(SK, "ORDER#")
The design principle: because DynamoDB has no joins and charges per request, you co-locate related items under the same partition key so they can be read in one efficient query.
Cross-family comparison
| Key-value | Document | Wide-column | Graph | |
|---|---|---|---|---|
| Data model | Key → opaque value | Collections of JSON documents | Partitioned rows, dynamic columns | Nodes + edges + properties |
| Query capability | By key only | Rich queries on any field | Partition/clustering keys; limited ad hoc | Traversals, pattern matching |
| Consistency | Tunable (often eventual) | Tunable; MongoDB defaults strong on primary | Tunable (Cassandra quorum) | Usually strong (Neo4j ACID) |
| Scaling | Excellent horizontal | Good (sharding) | Excellent horizontal | Harder to distribute |
| Best use case | Cache, sessions, counters | App data, catalogs, profiles | Time-series, logs, feeds at scale | Relationships, social, fraud |
| Weak at | Querying by value | Cross-document joins | Ad hoc queries, joins | Very high write volume, sharding |
Best Practices
- Model around access patterns, not entities. List your read/write queries first, then design storage so each is a single efficient operation. This is the opposite of relational normalization.
- Don’t force everything into one database. Use polyglot persistence: relational for transactional core data, key-value for caching, search engine for text, and a specialized store only where it clearly wins.
- Understand your consistency needs per operation. Use strong consistency where correctness matters (payments), eventual consistency where availability/latency matter (feeds, view counts). Many products let you tune this per query (e.g., Cassandra
QUORUM, DynamoDB strongly-consistent reads). - Choose partition/shard keys carefully. A poor key creates hot partitions (uneven load) or unbounded partitions (partitions that grow forever). Aim for high-cardinality, evenly-distributed keys.
- Denormalize deliberately, and own the consistency. Duplicating data speeds reads but you must keep copies in sync on write — often via application logic, background jobs, or change-data-capture streams.
- Set TTLs and retention. For caches, sessions, and time-series, use expiry/retention policies so data doesn’t grow unbounded.
- Index for your queries, but not blindly. Indexes speed reads but slow writes and consume storage. Index the fields you actually filter/sort on.
- Know the transaction limits. If you need multi-item atomicity, verify the product supports it (MongoDB multi-doc transactions, DynamoDB
TransactWriteItems) and understand the cost. - Plan for failure and eventual convergence. In AP systems, design idempotent writes and conflict resolution (last-write-wins, vector clocks, CRDTs) so replicas reconcile correctly.
- Benchmark with realistic data and access patterns. NoSQL performance depends heavily on your keys and query shapes; test at representative scale before committing.