Cơ sở dữ liệu NoSQLNoSQL Databases
Thuộc bộ kiến thức Data Engineer Roadmap.
Tổng quan
Cơ sở dữ liệu NoSQL xuất hiện khắp nơi trong công việc của data engineer, nhưng hiếm khi là đích đến cuối cùng. Chúng thường đóng một trong hai vai trò: hệ thống vận hành (operational) mà pipeline trích xuất dữ liệu ra, hoặc lớp serving tốc độ cao mà pipeline ghi kết quả vào. Các application engineer chọn một họ NoSQL vì nó phù hợp với access pattern mà service của họ cần (session cache, product catalog, social graph). Đến lượt data engineer phải xử lý hệ quả: làm sao lấy dữ liệu ra từ một store không có schema cố định và không có JOIN, và làm sao đẩy kết quả đã xử lý ngược vào một store phải trả lời trong vài mili-giây.
Note này giả định người đọc đã nắm nền tảng khái niệm từ Backend — Cơ sở dữ liệu NoSQL — CAP theorem, bốn họ NoSQL, và data modeling cơ bản. Hãy đọc bài đó trước nếu các thuật ngữ như “eventual consistency” hay “partition tolerance” còn xa lạ. Ở đây, trọng tâm chuyển sang các góc nhìn đặc thù của data engineering: NoSQL như một nguồn (source) để ingest (xem Nguồn dữ liệu & Ingestion), NoSQL như một lớp serving sau khi xử lý analytics hoặc ML, và các engine mà roadmap này nhắc riêng — Bigtable, CosmosDB, Neptune — mà bài backend không đi sâu.
Kiến thức nền tảng
Bốn họ NoSQL, tóm tắt nhanh
| Họ | Mô hình | Engine ví dụ | Thế mạnh điển hình |
|---|---|---|---|
| Key-value | Value không định dạng đứng sau một key | Redis, Memcached, DynamoDB | Lookup dưới mili-giây, caching, session state |
| Document | Document JSON/BSON bán cấu trúc | MongoDB, CouchDB | Linh hoạt, lồng nhau, dễ tiến hóa |
| Wide-column | Hàng thưa (sparse) được khóa bởi partition key + clustering key | Cassandra, HBase, Bigtable | Ghi thông lượng cao, time-series, dữ liệu khổng lồ |
| Graph | Node và edge là công dân hạng nhất | Neo4j, Amazon Neptune | Traversal, truy vấn nặng về quan hệ |
Định nghĩa đầy đủ, các đánh đổi của CAP theorem, và hướng dẫn data modeling tổng quát cho từng họ nằm ở Backend — Cơ sở dữ liệu NoSQL. Nội dung dưới đây xây dựng tiếp trên nền đó thay vì lặp lại.
Hai chiều luân chuyển dữ liệu
Một cơ sở dữ liệu quan hệ OLTP là “nguồn” kinh điển mà data engineer trích xuất, nhưng một phần rất lớn dữ liệu ứng dụng hiện đại lại nằm trong các NoSQL store — một app mobile chạy trên MongoDB, một giỏ hàng thương mại điện tử trong DynamoDB, một đội IoT ghi liên tục vào Cassandra. Vì vậy công việc data engineering xoay quanh NoSQL chia thành hai mối quan tâm khác biệt, kéo theo hai hướng ngược nhau:
- Chiều ingestion (NoSQL là source): trích xuất dữ liệu vận hành ra khỏi NoSQL store, làm phẳng và chuẩn hóa nó, rồi nạp vào warehouse hoặc lake để phân tích. Thách thức ở đây là schema-on-read: nguồn không có cấu trúc bắt buộc, nên engineer phải tự áp đặt cấu trúc.
- Chiều serving (NoSQL là sink): lấy kết quả đầu ra của một batch job, streaming pipeline, hay mô hình ML — số liệu tổng hợp, feature, gợi ý, điểm số — rồi ghi vào một NoSQL store để ứng dụng đọc lại trong vài mili-giây. Thách thức ở đây là thiết kế access pattern và key cho một hệ thống sẽ không bao giờ nhận một truy vấn ad-hoc nào nữa.
Cả hai chiều đều quan trọng, và việc gộp lẫn chúng là lỗi thường gặp: các quyết định modeling giúp một store trở thành source tốt (document lồng nhau phong phú, schema rộng và linh hoạt) thường ngược lại với những gì làm nên một lớp serving tốt (denormalize, tra cứu bằng một key duy nhất, đã join sẵn).
Khái niệm chính
NoSQL như một source: trích xuất từ store schema-on-read
Nguồn quan hệ trao cho data engineer một hợp đồng ổn định: một tập cột và kiểu dữ liệu cố định được ép buộc tại thời điểm ghi (schema-on-write). Document store và wide-column store không có cam kết đó. MongoDB sẵn sàng để một collection orders chứa các document khác hình dạng nhau giữa các bản ghi, mảng lồng nhau có độ sâu khác nhau, và một field mà kiểu dữ liệu thay đổi theo thời gian (field discount từng là số, giờ là object {type, value}). Đây là schema-on-read: cấu trúc được phát hiện — hoặc áp đặt — tại thời điểm đọc, không phải tại thời điểm ghi.
Với data engineer, điều này tạo ra một tập vấn đề lặp lại khi trích xuất vào warehouse:
- Cấu trúc không nhất quán giữa các document — cùng một collection có thể trộn lẫn nhiều hình dạng từ các phiên bản ứng dụng, feature flag, hoặc ghi dở khác nhau. Một cú dump kiểu
SELECT *ngây thơ sẽ tạo ra một bảng với hàng chục cột thưa, nullable, một số trong đó chỉ xuất hiện ở 0.01% số dòng. - Cấu trúc lồng nhau và lặp lại — sub-document nhúng và mảng không ánh xạ trực tiếp vào một hàng phẳng. Một document
customervới mảngaddressesnhúng bên trong hoặc phải trở thành một bảng con riêng (mỗi hàng một địa chỉ, kèm khóa ngoại về customer), hoặc được làm phẳng thành cột array/struct nếu warehouse hỗ trợ kiểu bán cấu trúc (ví dụARRAY<STRUCT<...>>trong BigQuery,VARIANTtrong Snowflake). - Kiểu dữ liệu trôi dạt theo thời gian — kiểu của một field có thể thay đổi qua từng release. Pipeline trích xuất cần ép về một kiểu chung, lưu song song giá trị thô cùng giá trị đã parse, hoặc fail rõ ràng và cảnh báo thay vì âm thầm bỏ hoặc làm hỏng dữ liệu.
- Không có ngữ nghĩa primary key bắt buộc ngoài
_idcủa bản thân store — việc ánh xạ sang kỳ vọng của warehouse về tính duy nhất và theo dõi thay đổi (cho CDC/upsert) cần được thiết kế tường minh.
Các chiến lược làm phẳng phổ biến, xếp theo mức độ cấu trúc tăng dần:
| Chiến lược | Cách làm | Khi nào dùng |
|---|---|---|
| Land raw JSON | Lưu mỗi document nguyên trạng vào một cột VARIANT/JSON, mỗi hàng một document | Giữ độ trung thực dữ liệu; hoãn việc modeling; landing zone tốt trong lake |
| Shred thành bảng con | Nổ (explode) mảng lồng nhau thành các bảng riêng, kèm khóa ngoại về bảng cha | Mảng lồng sâu, cardinality cao (order line items, event log) |
| Flatten thành bảng rộng | Đưa các field cấp cao/lồng một cấp đã biết lên thành cột; giữ field hiếm/biến động trong một cột JSON còn lại | Document có phần “core” khá ổn định cộng thêm đuôi dài field tùy chọn |
| Kiểu bán cấu trúc native | Giữ cấu trúc lồng nhau dưới dạng cột STRUCT/ARRAY native, truy vấn được bằng cú pháp dot/array | Warehouse cloud hiện đại (BigQuery, Snowflake, Redshift Spectrum) hỗ trợ sẵn |
Trong thực tế, cách tiếp cận hai giai đoạn hoạt động tốt: land document thô không đổi (audit trail, khả năng replay, lưới an toàn cho schema evolution), sau đó áp một tầng transform — thường là dbt hoặc Spark job — để flatten thành hình dạng mà analyst thực sự truy vấn. Schema drift nên được phát hiện, không nên âm thầm hấp thụ: cảnh báo khi xuất hiện một nhóm field mới hoặc một field đã biết đổi kiểu, để logic flatten được cập nhật một cách có chủ đích.
Cơ chế trích xuất cũng khác nhau đáng kể theo từng engine:
- MongoDB — trích xuất toàn bộ qua
mongoexport/mongodump, hoặc CDC qua Change Streams (xây trên oplog) cho pipeline incremental; aggregation pipeline (bên dưới) có thể định hình lại dữ liệu trước khi nó rời khỏi database. - DynamoDB — export hàng loạt ra S3 (tính năng export-to-S3 native, không tốn read-capacity) cho full load, và DynamoDB Streams cho change data capture, thường được đưa vào Kinesis/Lambda để lan truyền gần thời gian thực vào lake.
- Cassandra — không có câu chuyện CDC-sang-warehouse gọn gàng như Mongo; các pattern phổ biến là dual-write từ ứng dụng, connector Cassandra của Debezium (change data capture từ commit log), hoặc đọc hàng loạt định kỳ bằng
COPY/Spark từ keyspace nguồn.
NoSQL như một lớp serving: chiều luân chuyển ngược
Sau khi một pipeline tính ra thứ gì đó có giá trị — một danh sách gợi ý, điểm gian lận (fraud score), ước tính giá trị vòng đời khách hàng, một feature vector đã tính sẵn — kết quả đó thường cần đến tay ứng dụng trong vài mili-giây, với concurrency cao, được khóa bởi một ID đơn giản. Warehouse là công cụ sai cho việc này: nó được tối ưu cho truy vấn phân tích quét nhiều dữ liệu (scan-heavy), không phải tra cứu một hàng đơn lẻ ở độ trễ mức request. Đây là lúc chiều serving của NoSQL vào cuộc, và đây thực sự là một bài toán kỹ thuật khác với ingestion.
Các pattern điển hình:
- Gợi ý đã tính sẵn (precomputed recommendations) — một batch job hàng đêm hoặc hàng giờ (Spark, dbt, hoặc truy vấn warehouse) tính “người mua X cũng mua Y” hoặc bảng xếp hạng cá nhân hóa, sau đó ghi hàng loạt kết quả vào DynamoDB hoặc Bigtable, khóa theo
user_id, để API gợi ý của ứng dụng chỉ là một lần tra cứu key nhanh thay vì truy vấn trực tiếp warehouse. - Feature store — hệ thống ML cần giá trị feature sẵn sàng ở độ trễ thấp tại thời điểm inference. “Offline store” (warehouse hoặc lake, dùng để training) và “online store” (key-value store như DynamoDB, Redis, hoặc Bigtable) được đồng bộ bởi một pipeline tính feature theo batch hoặc streaming rồi materialize giá trị mới nhất cho mỗi entity vào online store. Feast và các framework feature-store tương tự chính thức hóa đúng sự phân chia offline/online này.
- Số liệu tổng hợp đã materialize cho dashboard — thay vì gọi warehouse mỗi lần refresh dashboard, một pipeline định kỳ ghi các rollup đã tổng hợp sẵn (daily active users, doanh thu theo phân khúc) vào một document hoặc key-value store đứng trước tầng API của dashboard.
Thiết kế cho chiều serving đảo ngược bản năng data-modeling thông thường: thay vì normalize để linh hoạt, engineer denormalize mạnh tay và thiết kế key schema xoay quanh một access pattern duy nhất mà ứng dụng cần — thường là “lấy mọi thứ cần để render màn hình/response này cho ID này trong một request”. Thiết kế đúng key ở đây cũng chính là kỷ luật thiết kế partition key trong Cassandra (xem bên dưới), chỉ áp dụng cho một store khác.
Cassandra và wide-column store, đi sâu
Cassandra (và wide-column store nói chung) là lựa chọn mặc định trong data engineering bất cứ khi nào workload nặng về ghi, chủ yếu append, và có thứ tự tự nhiên theo thời gian hoặc sự kiện — telemetry, dữ liệu cảm biến, sự kiện ứng dụng, lịch sử tin nhắn. Lý do bắt nguồn trực tiếp từ storage engine: cấu trúc lưu trữ log-structured merge-tree (LSM) của Cassandra làm cho việc ghi rẻ (append vào commit log và memtable trong bộ nhớ, sau đó flush ra SSTable bất biến), và kiến trúc masterless, dạng ring với replication có thể điều chỉnh cho phép việc ghi rơi vào bất kỳ node nào mà không cần một coordinator nghẽn cổ chai duy nhất — thông lượng tăng gần như tuyến tính khi thêm node.
Thiết kế partition key là quyết định quan trọng nhất trong một data model của Cassandra, và đáng để đi sâu vì nó quyết định cả hiệu năng truy vấn lẫn mức độ phân bổ đồng đều của dữ liệu trên cluster:
- Partition key quyết định (các) node nào sở hữu một hàng cho trước (thông qua consistent hashing của key). Một partition key được chọn tốt sẽ trải đều việc ghi và lưu trữ trên toàn cluster.
- Toàn bộ dữ liệu chung một partition key được lưu cùng nhau, sắp xếp theo clustering key, trên đĩa. Điều này khiến các truy vấn dạng range bên trong một partition (ví dụ “cho tôi các chỉ số của thiết bị này giữa hai mốc thời gian”) cực nhanh — chỉ là một lần đọc tuần tự trên một node.
- Một partition key quá thô (ví dụ chỉ partition theo
sensor_typetrong khi chỉ có vài loại nhưng hàng triệu cảm biến) tạo ra hot partition: một số ít node hứng chịu phần không tương xứng của tải ghi/đọc, và các partition riêng lẻ có thể phình to đến mức ảnh hưởng hiệu năng cũng như thời gian repair/compaction. - Một partition key quá mịn, hoặc thay đổi liên tục (ví dụ partition thuần túy theo một UUID ngẫu nhiên cardinality cao không có cấu trúc gì khác), khiến không thể thực hiện bất kỳ range scan có ý nghĩa nào mà không phải truy vấn từng partition — phá hỏng mục đích của partition key.
- Pattern time-series chuẩn là bucket theo một entity tự nhiên cộng thêm một cửa sổ thời gian: partition key
(device_id, date_bucket), clustering keytimestamp. Cách này giữ kích thước partition trong tầm kiểm soát (ví dụ một partition cho mỗi thiết bị mỗi ngày) trong khi vẫn hỗ trợ truy vấn nhanh “cho tôi dữ liệu thiết bị này trong ngày này, sắp theo thời gian” — đúng access pattern mà hầu hết pipeline time-series và event cần.
Mô hình consistency của Cassandra là nửa còn lại của đánh đổi mà data engineer phải tính đến. Mỗi thao tác đọc và ghi chỉ định một consistency level (ONE, QUORUM, ALL, v.v.) độc lập với nhau, và Cassandra mặc định là eventual consistency: một ghi được xác nhận ở ONE có thể chưa hiển thị cho một đọc ở ONE trên replica khác cho đến khi replication bắt kịp. Với pipeline data engineering, điều này có hệ quả cụ thể — các tiến trình CDC đọc từ Cassandra có thể thấy các ghi hơi lệch thứ tự giữa các partition, và các job trích xuất batch nên chấp nhận những khoảng độ trễ replication nhỏ thay vì giả định một snapshot nhất quán tức thời trên toàn cluster. Dùng QUORUM cho đọc/ghi siết chặt điều này với cái giá là độ trễ, và là lựa chọn phổ biến khi pipeline cần đảm bảo read-your-writes mạnh hơn.
Bigtable: engine wide-column của Google Cloud
Bigtable là wide-column store được quản lý hoàn toàn (fully managed) của Google Cloud, và nó có một vị trí đặc biệt trong lịch sử NoSQL: bài báo “Bigtable” gốc của Google năm 2006 là tổ tiên chung của cả HBase (một bản triển khai mã nguồn mở lại) và, gián tiếp, Cassandra (kết hợp data model của Bigtable với mô hình phân phối của Amazon Dynamo). Vì vậy Bigtable, HBase và Cassandra có nét họ hàng — lưu trữ thưa, được sắp xếp, hướng column-family, khóa theo row key — dù đặc tính vận hành khác nhau.
Thiết kế của Bigtable xoay quanh một row key được chọn kỹ, sắp xếp theo thứ tự từ điển (lexicographic), với mọi đọc/ghi cho một row key được định tuyến đến tablet server đang chịu trách nhiệm cho dải key đó. Điều này khiến thiết kế row key trở thành bài toán tương đương với partition key của Cassandra, cộng thêm một điểm khác biệt: vì row key được lưu theo thứ tự sắp xếp và chia thành các dải key liên tục (tablet) trên nhiều server, một key tăng đơn điệu (như một timestamp thô) khiến mọi ghi mới đều rơi vào cùng một tablet “nóng” ở cuối dải key. Cách khắc phục chuẩn là salt hoặc đảo ngược key — thêm tiền tố là giá trị hash hoặc đảo ngược, hoặc đặt một ID entity tự nhiên lên trước timestamp (ví dụ device123#reversed_timestamp) — để trải đều việc ghi trên các tablet.
Trong pipeline data engineering, Bigtable xuất hiện phổ biến nhất với vai trò:
- Store nền cho ingestion time-series và IoT thông lượng cao trên Google Cloud — dữ liệu cảm biến, clickstream ad-tech, dữ liệu tick tài chính — thường nằm trực tiếp sau các pipeline Dataflow/Beam ghi sự kiện đã xử lý vào Bigtable, vừa để serving vừa để export batch tiếp vào BigQuery.
- Lớp serving được đọc bởi cả ứng dụng lẫn export phân tích — Bigtable tích hợp native với Dataflow và BigQuery (qua federated query hoặc job export), khiến pattern “tính toán trong pipeline, serve từ Bigtable, định kỳ export batch sang BigQuery để phân tích sâu” rất phổ biến trong các stack tập trung vào GCP.
- Một store được chọn cụ thể vì khả năng mở rộng tuyến tính đến quy mô petabyte và hàng triệu thao tác mỗi giây, khi giới hạn thông lượng của Cloud SQL hay Firestore sẽ trở thành nút cổ chai.
MongoDB, đi sâu
Mô hình document của MongoDB tự nhiên phù hợp với phía “source” của phân chia ingestion/serving chính vì lý do product team yêu thích nó: nó ôm lấy dữ liệu bán cấu trúc, lồng nhau, tiến hóa liên tục. Một hồ sơ người dùng, một mục catalog sản phẩm, hay payload sự kiện có thể có thêm field tùy chọn mới qua từng release mà không cần migration — và đó chính xác là lý do MongoDB rất thường là store vận hành mà data engineer sau này phải trích xuất và chuẩn hóa về một schema warehouse cứng nhắc (xem phần schema-on-read ở trên).
Aggregation pipeline của MongoDB đáng để hiểu rõ, cụ thể vì đây là cơ chế ETL/transform tích hợp sẵn của chính MongoDB, và nó có thể giảm đáng kể khối lượng công việc mà pipeline downstream phải làm. Một aggregation pipeline là một chuỗi các stage — $match (lọc), $project (định hình lại/chọn field), $unwind (làm phẳng một mảng thành nhiều document), $group (tổng hợp), $lookup (một stage kiểu left-outer-join với collection khác), $sort, $out/$merge (ghi kết quả vào một collection) — nối tiếp nhau, về khái niệm tương tự một truy vấn SQL được xây từ các toán tử có thể kết hợp thay vì một câu lệnh khai báo duy nhất. Với data engineer, điều này có ý nghĩa theo hai cách:
- Một số công việc làm phẳng và định hình (unwind mảng, project chỉ những field cần, pre-join một collection tham chiếu) có thể được đẩy xuống chính MongoDB thông qua aggregation pipeline trước khi trích xuất, giảm khối lượng dữ liệu di chuyển và logic transform cần thiết ở downstream.
$merge/$outcó thể materialize kết quả của một aggregation vào một collection mới, biến MongoDB thành một tầng transform nhẹ cho các collection báo cáo nặng về đọc mà nếu không sẽ phải là một bảng warehouse downstream.
Về mặt vận hành, MongoDB cực kỳ phổ biến làm datastore chính đằng sau các ứng dụng nặng về nội dung và catalog (CMS, catalog sản phẩm, hồ sơ người dùng, backend app mobile), khiến nó là một trong những NoSQL source được trích xuất thường xuyên nhất mà data engineer gặp trong thực tế — thường qua Change Streams cho CDC incremental hoặc dump full/incremental theo lịch cho nhu cầu báo cáo ít nhạy cảm về thời gian hơn.
Graph database: khi quan hệ chính là đối tượng phân tích
Phần lớn việc dùng NoSQL trong một pipeline coi quan hệ là một điều bất tiện cần làm phẳng đi. Graph database tồn tại cho trường hợp ngược lại: khi quan hệ giữa các entity chính là đối tượng phân tích, không phải cấu trúc phụ cần normalize bỏ đi. Các vòng gian lận (fraud ring), hệ thống gợi ý, phân tích mạng xã hội, ánh xạ phụ thuộc chuỗi cung ứng, và identity resolution (“đây có phải cùng một người trên năm tài khoản khác nhau không?”) đều chung một đặc điểm cấu trúc — câu trả lời thú vị nằm ở các đường đi (path) nhiều bước và mẫu kết nối, không nằm ở thuộc tính của bất kỳ hàng đơn lẻ nào.
Lý do thực tế để chọn Neo4j hoặc Amazon Neptune thay vì ép bài toán vào mô hình quan hệ hoặc document là hiệu năng ở độ sâu traversal. Một truy vấn quan hệ trả lời “bạn của bạn của bạn cũng theo dõi tài khoản X” đòi hỏi một chuỗi self-join mà chi phí tăng theo cấp số nhân với mỗi bước, và query planner suy giảm nghiêm trọng sau hai ba cấp. Một graph database lưu trực tiếp danh sách kề (adjacency) — mỗi node giữ con trỏ đến các edge của nó — nên đi sang bước kế tiếp là một phép theo con trỏ với thời gian không đổi, bất kể toàn bộ graph lớn cỡ nào. Điều này thường được tóm tắt là graph database biến quan hệ thành công dân hạng nhất thay vì giá trị được tính lúc truy vấn thông qua join.
- Neo4j — graph database property graph được sử dụng rộng rãi nhất, truy vấn bằng Cypher, một ngôn ngữ pattern-matching khai báo đọc gần giống cách một người mô tả một mẫu graph (
(a:Person)-[:FRIENDS_WITH]->(b:Person)-[:FRIENDS_WITH]->(c:Person)). Ứng dụng data-engineering phổ biến: batch-load dữ liệu giao dịch hoặc tương tác vào Neo4j để chạy thuật toán phát hiện vòng gian lận hoặc phát hiện cộng đồng (community detection), sau đó đưa các entity bị đánh dấu ngược vào các hệ thống downstream. - Amazon Neptune — graph database được quản lý của AWS, hỗ trợ cả mô hình property graph (truy vấn bằng openCypher hoặc Gremlin) lẫn RDF/SPARQL cho các use case semantic-graph và knowledge-graph. Neptune khớp tự nhiên với pipeline tập trung vào AWS: dữ liệu land ở S3 hoặc warehouse có thể được transform và bulk-load vào Neptune (qua bulk loader từ S3) để phục vụ các ứng dụng nặng về quan hệ như hệ thống gợi ý hoặc identity graph, với Neptune trả lời các truy vấn traversal độ trễ thấp cho ứng dụng.
Với data engineer, tín hiệu thực tế để chọn graph store là một ngưỡng cụ thể về hiệu năng và khả năng biểu đạt: một khi truy vấn “tìm các entity liên kết/liên quan” cần nhiều hơn hai ba join để diễn đạt, và đặc biệt khi cần số bước nhảy biến đổi hoặc không giới hạn (ví dụ “bất kỳ đường đi độ dài ≤ 6 nối hai tài khoản này”), graph database thường vượt trội cả về hiệu năng lẫn khả năng biểu đạt so với phương án quan hệ hay document.
CosmosDB: store đa mô hình của Azure
Azure Cosmos DB đi theo hướng khác so với các engine đơn mô hình ở trên: nó là một database đa mô hình (multi-model), phơi bày cùng một engine phân tán toàn cầu, partition theo chiều ngang bên dưới, thông qua nhiều API — Core (SQL) API cho workload document, một API tương thích MongoDB, một API tương thích Cassandra (wide-column), một Gremlin API (graph), và một Table API (key-value). Điểm hấp dẫn là một đội có thể chọn mô hình dữ liệu/API phù hợp với use case mà không cần cấp phát một sản phẩm database riêng, trong khi vẫn nhận được cùng các cam kết bên dưới: SLA độ trễ mức mili-giây một chữ số, các mức consistency có thể điều chỉnh (năm tùy chọn từ strong đến eventual), và replication toàn cầu tự động.
Trong một stack dữ liệu tập trung vào Azure, CosmosDB có xu hướng xuất hiện ở cả hai phía của sự phân chia được đề cập trong bài này: như một source vận hành (một ứng dụng xây trên SQL hoặc MongoDB API), được trích xuất qua Change Feed (tương đương Change Streams của MongoDB hay DynamoDB Streams) vào các pipeline Azure Synapse hoặc Data Factory; và như một lớp serving, nhận kết quả đã xử lý — gợi ý, giá trị feature, số liệu tổng hợp — từ pipeline Synapse/Databricks để ứng dụng tiêu thụ với độ trễ thấp, đúng vai trò mà DynamoDB hay Bigtable đảm nhận trong stack AWS/GCP. Điểm liên quan chính với một data engineer làm việc trên nhiều cloud là nhận ra rằng Change Feed và bề mặt đa API của CosmosDB cho phép nó thay thế cho nhiều engine kể trên, tùy vào API mà đội ứng dụng đã chọn.
Bảng so sánh: họ NoSQL, vai trò trong pipeline, và mối quan tâm modeling
| Họ | Vai trò điển hình trong pipeline | Engine ví dụ | Mối quan tâm modeling chính |
|---|---|---|---|
| Key-value | Serving (tra cứu nhanh theo ID) | DynamoDB, Redis | Thiết kế key sao cho một access pattern là một lần get đơn; tránh cần truy vấn phụ |
| Document | Source (store vận hành để trích xuất); đôi khi serving qua đọc trực tiếp từ app | MongoDB, CouchDB, CosmosDB (SQL API) | Lên kế hoạch cho schema drift và cấu trúc lồng/lặp; dùng aggregation pipeline để định hình trước khi trích xuất |
| Wide-column | Cả hai — source thông lượng cao cho sự kiện/telemetry, và serving cho tra cứu time-series/feature | Cassandra, HBase, Bigtable | Thiết kế partition/row key quyết định cả hiệu năng truy vấn lẫn phân bổ tải trên cluster |
| Graph | Source và serving cho phân tích lấy quan hệ làm trung tâm (gian lận, gợi ý, identity) | Neo4j, Amazon Neptune | Model theo pattern traversal trước tiên; truy vấn nhiều bước là lý do duy nhất để chọn graph |
| Đa mô hình | Cả hai, tùy API được dùng | Azure CosmosDB | Mối quan tâm modeling giống hệt API tương ứng (document/wide-column/graph/key-value) mà ứng dụng đã chọn |
Best Practices
- Land dữ liệu thô trước khi định hình. Khi trích xuất từ một nguồn schema-on-read, giữ một bản sao document/hàng chưa chỉnh sửa trong lake trước khi flatten — đây là cách đáng tin cậy duy nhất để replay lại pipeline sau khi phát hiện một lỗi modeling hoặc schema drift bất ngờ.
- Phát hiện schema drift, đừng âm thầm hấp thụ nó. Cảnh báo khi có field cấp cao mới, field hiện có đổi kiểu, hoặc độ sâu lồng bất ngờ, thay vì ép tất cả về
NULLhoặcSTRINGvà mất tín hiệu cho biết model của nguồn đã thay đổi. - Ưu tiên CDC (Change Streams / DynamoDB Streams / Change Feed) hơn dump toàn bộ khi khối lượng dữ liệu tăng. Trích xuất toàn bộ đơn giản nhất để xây dựng nhưng không mở rộng được về chi phí và độ trễ; mọi engine NoSQL quản lý lớn hiện nay đều có cơ chế change-capture native được thiết kế đúng cho việc này.
- Thiết kế key của lớp serving xoay quanh đúng một truy vấn mà ứng dụng cần, không phải trường hợp tổng quát. Một store serving hầu như không bao giờ nên cần secondary index hay scan; nếu cần, bước aggregation/precompute ở upstream có lẽ cần làm nhiều việc hơn.
- Xác định đúng partition/row key trước khi ghi một hàng đầu tiên vào Cassandra hay Bigtable. Đổi partition key sau khi khối lượng dữ liệu đã lớn nghĩa là phải migration toàn bộ dữ liệu; hãy model access pattern (cái gì sẽ được truy vấn cùng nhau, và cardinality của key này sẽ tăng nhanh thế nào) trước khi chọn.
- Đừng ép một bài toán nặng về quan hệ đi qua join quan hệ chỉ vì store quan hệ hoặc document đã có sẵn trong stack. Nếu một truy vấn cần traversal độ sâu không giới hạn, hoặc câu hỏi nghiệp vụ về cơ bản là “các entity này liên kết với nhau như thế nào,” hãy đánh giá một graph database thay vì tối ưu một chuỗi join ngày càng cồng kềnh.
- Giữ đồng bộ có chủ đích giữa feature/aggregate store offline (batch/warehouse) và online (serving). Coi pipeline materialize kết quả vào DynamoDB/Bigtable/Redis là một phần kiến trúc hạng nhất, có giám sát riêng cho độ trễ dữ liệu (staleness), không phải một thứ gắn thêm vào sau một batch job.
- Chọn consistency level khớp với yêu cầu thực tế. Đừng mặc định
QUORUM/strong consistency ở mọi nơi vì thận trọng — nhưng hãy dùng nó một cách tường minh ở bất cứ đâu pipeline cần đảm bảo read-your-own-writes hoặc thứ tự nhất quán giữa các replica.
Tài liệu tham khảo
- Backend — Cơ sở dữ liệu NoSQL — CAP theorem, bốn họ NoSQL, và data modeling tổng quát (đọc nền tảng cho bài này)
- Nguồn dữ liệu & Ingestion — các pattern ingestion tổng quát mà chiều “source” của bài này xây dựng tiếp
- Data Lake & Kiến trúc hiện đại — nơi các bản trích xuất NoSQL đã flatten/land thường đi tới
- MongoDB — Aggregation Pipeline
- MongoDB — Change Streams
- Amazon DynamoDB — Streams and Export to S3
- DataStax — Cassandra Data Modeling & Partition Keys
- Google Cloud — Bigtable Schema Design
- Amazon Neptune — Documentation
- Azure Cosmos DB — Multi-Model Overview
Part of the Data Engineer Roadmap knowledge base.
Overview
NoSQL databases show up everywhere in a data engineer’s world, but rarely as the final destination. They are usually one of two things: the operational system a pipeline extracts from, or the fast-serving layer a pipeline writes back into. Application engineers pick a NoSQL family because it fits an access pattern their service needs (a session cache, a product catalog, a social graph). Data engineers then have to deal with the consequences: how to pull data out of a store that has no fixed schema and no JOIN, and how to push processed results back into a store that must answer in single-digit milliseconds.
This note assumes the conceptual foundation from Backend — NoSQL Databases — the CAP theorem, the four families, and basic data modeling. Read that first if terms like “eventual consistency” or “partition tolerance” are unfamiliar. Here, the focus shifts to the data-engineering-specific angles: NoSQL as a source for ingestion (see Data Sources & Ingestion), NoSQL as a serving layer after analytics or ML processing, and the engines this roadmap calls out specifically — Bigtable, CosmosDB, and Neptune — that the backend note doesn’t dwell on.
Fundamentals
The four families, briefly
| Family | Model | Example engines | Typical strength |
|---|---|---|---|
| Key-value | Opaque value behind a key | Redis, Memcached, DynamoDB | Sub-millisecond lookups, caching, session state |
| Document | Semi-structured JSON/BSON documents | MongoDB, CouchDB | Flexible, nested, evolving records |
| Wide-column | Sparse rows keyed by partition + clustering key | Cassandra, HBase, Bigtable | High-throughput writes, time-series, huge datasets |
| Graph | Nodes and edges as first-class citizens | Neo4j, Amazon Neptune | Traversals, relationship-heavy queries |
Full definitions, the CAP theorem trade-offs, and general data-modeling guidance for each family live in Backend — NoSQL Databases. What follows here builds on top of that foundation rather than repeating it.
Two directions of data flow
A relational OLTP database is the classic “source” a data engineer extracts from, but a huge share of modern application data lives in NoSQL stores instead — a mobile app backed by MongoDB, an e-commerce cart in DynamoDB, an IoT fleet writing into Cassandra. Data engineering work around NoSQL therefore splits into two distinct concerns that pull in opposite directions:
- Ingestion direction (NoSQL as source): extracting operational data out of a NoSQL store, flattening and conforming it, and loading it into a warehouse or lake for analytics. The challenge here is schema-on-read: the source has no enforced structure, so the engineer must impose one.
- Serving direction (NoSQL as sink): taking the output of a batch job, streaming pipeline, or ML model — aggregates, features, recommendations, scores — and writing it into a NoSQL store so an application can read it back in milliseconds. The challenge here is designing access patterns and keys for a system that will never see an ad-hoc query again.
Both directions matter, and conflating them is a common mistake: the modeling decisions that make a store a good source (rich embedded documents, wide flexible schemas) are often the opposite of what makes it a good serving layer (denormalized, single-key-lookup, pre-joined).
Key Concepts
NoSQL as a source: extracting from schema-on-read stores
Relational sources hand a data engineer a stable contract: a fixed set of columns and types enforced at write time (schema-on-write). NoSQL document and wide-column stores make no such promise. MongoDB will happily let one orders collection contain documents with different fields across records, nested arrays that vary in depth, and fields whose type changes over time (a discount field that used to be a number and is now an object with {type, value}). This is schema-on-read: structure is discovered — or imposed — at the moment of reading, not enforced at the moment of writing.
For a data engineer, this creates a recurring set of problems when extracting into a warehouse:
- Inconsistent structure across documents — the same collection can mix shapes from different application versions, feature flags, or partial writes. A naive
SELECT *-style dump will produce a table with dozens of sparse, nullable columns, some of which only ever appear in 0.01% of rows. - Nested and repeated structures — embedded sub-documents and arrays don’t map to a single flat row. A
customerdocument with an embedded array ofaddresseseither has to become a separate child table (one row per address, with a foreign key back to the customer) or be flattened into an array/struct column if the warehouse supports semi-structured types (e.g.ARRAY<STRUCT<...>>in BigQuery,VARIANTin Snowflake). - Type drift over time — a field’s type can change release over release. Extraction pipelines need to either coerce to a common type, capture the raw value alongside a parsed value, or fail loudly and alert rather than silently drop or corrupt data.
- No enforced primary key semantics outside the store’s own
_id— mapping to a warehouse’s expectations around uniqueness and change tracking (for CDC/upserts) needs explicit design.
Common flattening strategies, roughly in order of increasing structure:
| Strategy | What it does | When to use |
|---|---|---|
| Land raw JSON | Store each document as-is in a VARIANT/JSON column, one row per document | Preserve fidelity; defer modeling; good first landing zone in a lake |
| Shred to child tables | Explode nested arrays into separate tables with a foreign key back to the parent | Deeply nested, high-cardinality arrays (order line items, event logs) |
| Flatten to wide table | Promote known top-level and one-level-nested fields to columns; keep rare/variable fields in a residual JSON column | Documents with a mostly-stable “core” schema plus a long tail of optional fields |
| Semi-structured native types | Keep nested structures as native STRUCT/ARRAY columns queryable with dot/array syntax | Modern cloud warehouses (BigQuery, Snowflake, Redshift Spectrum) that support this natively |
In practice, a two-stage approach works well: land the raw document unchanged (audit trail, replay capability, schema evolution safety net), then apply a transformation layer — often dbt or a Spark job — that flattens into the shapes analysts actually query. Schema drift should be detected, not silently absorbed: alert when a new field family appears or a known field changes type, so the flattening logic can be updated deliberately.
Extraction mechanics also differ meaningfully by engine:
- MongoDB — full extraction via
mongoexport/mongodump, or CDC via Change Streams (built on the oplog) for incremental pipelines; the aggregation pipeline (below) can pre-shape data before it ever leaves the database. - DynamoDB — bulk export to S3 (native export-to-S3 feature, no read-capacity cost) for full loads, and DynamoDB Streams for change data capture, typically fed into Kinesis/Lambda for near-real-time propagation into a lake.
- Cassandra — no built-in CDC-to-warehouse story as clean as Mongo’s; common patterns are dual-writing from the application, Debezium’s Cassandra connector (change data capture from commit logs), or periodic
COPY/Spark bulk reads from the source keyspace.
NoSQL as a serving layer: the reverse data flow
After a pipeline computes something valuable — a recommendation list, a fraud score, a customer lifetime-value estimate, a precomputed feature vector — that result usually needs to reach an application in milliseconds, at high concurrency, keyed by a simple ID. A warehouse is the wrong tool for that: it’s optimized for scan-heavy analytical queries, not single-row point lookups at request latency. This is where the serving direction of NoSQL comes in, and it is a genuinely different engineering problem from ingestion.
Typical patterns:
- Precomputed recommendations — a nightly or hourly batch job (Spark, dbt, or a warehouse query) computes “users who bought X also bought Y” or personalized rankings, then bulk-writes the results into DynamoDB or Bigtable keyed by
user_id, so the application’s recommendation API is a single fast key lookup instead of a live query against the warehouse. - Feature stores — ML systems need feature values available at low latency at inference time. The “offline store” (a warehouse or lake, used for training) and the “online store” (a key-value store like DynamoDB, Redis, or Bigtable) are kept in sync by a pipeline that computes features in batch or streaming and materializes the latest value per entity into the online store. Feast and similar feature-store frameworks formalize exactly this offline/online split.
- Materialized aggregates for dashboards — rather than hitting a warehouse for every dashboard refresh, a pipeline periodically writes pre-aggregated rollups (daily active users, revenue by segment) into a document or key-value store fronting the dashboard’s API layer.
Designing for the serving direction inverts the usual data-modeling instinct: instead of normalizing for flexibility, the engineer denormalizes aggressively and designs the key schema around the one access pattern the application needs — usually “fetch everything needed to render this screen/response for this ID in one request.” Getting this key design right is the same discipline as partition-key design in Cassandra (see below), just applied to a different store.
Cassandra and wide-column stores in depth
Cassandra (and wide-column stores generally) are the default choice in data engineering whenever the workload is write-heavy, append-mostly, and naturally time-ordered or event-ordered — telemetry, sensor readings, application events, message history. The reasons trace directly to the storage engine: Cassandra’s log-structured merge-tree (LSM) storage makes writes cheap (append to a commit log and an in-memory memtable, flush to immutable SSTables later), and its masterless, ring-based architecture with tunable replication lets writes land on any node without a single bottleneck coordinator — throughput scales roughly linearly by adding nodes.
Partition key design is the single most important decision in a Cassandra data model, and it is worth dwelling on because it determines both query performance and how evenly data spreads across the cluster:
- The partition key determines which node(s) own a given row (via consistent hashing of the key). A well-chosen partition key spreads writes and storage evenly across the cluster.
- All data sharing a partition key is stored together, sorted by the clustering key, on disk. This makes range queries within a partition (e.g. “give me this device’s readings between two timestamps”) extremely fast — a single sequential read on one node.
- A partition key that is too coarse (e.g. partitioning only by
sensor_typewhen there are a handful of types but millions of sensors) creates hot partitions: a small number of nodes absorb a disproportionate share of the write/read load, and individual partitions can grow so large they hurt performance and repair/compaction times. - A partition key that is too fine, or that changes constantly (e.g. partitioning purely by a high-cardinality random UUID with no other structure), makes it impossible to do any meaningful range scan without querying every partition — defeating the point of a partition key.
- The standard time-series pattern buckets by a natural entity plus a time window: partition key
(device_id, date_bucket), clustering keytimestamp. This keeps partitions bounded in size (one per device per day, say) while still supporting fast “give me this device’s data for this day, ordered by time” queries — exactly the access pattern most time-series and event pipelines need.
Cassandra’s consistency model is the other half of the trade-off data engineers must account for. Reads and writes each specify a consistency level (ONE, QUORUM, ALL, etc.) independent of each other, and Cassandra defaults to eventual consistency: a write acknowledged at ONE may not be visible to a read at ONE on a different replica until replication catches up. For data engineering pipelines this has concrete consequences — CDC processes reading from Cassandra may see writes slightly out of order across partitions, and batch extraction jobs should tolerate small windows of replication lag rather than assuming a snapshot is instantaneously consistent across the whole cluster. Using QUORUM reads/writes tightens this at the cost of latency, and is the common choice when a pipeline needs stronger read-your-writes guarantees.
Bigtable: Google Cloud’s wide-column engine
Bigtable is Google Cloud’s fully managed wide-column store, and it has a special place in NoSQL history: the original 2006 Google “Bigtable” paper is the common ancestor of both HBase (an open-source reimplementation) and, indirectly, Cassandra (which combined Bigtable’s data model with Amazon Dynamo’s distribution model). Bigtable, HBase, and Cassandra therefore share a family resemblance — sparse, sorted, column-family-oriented storage keyed by a row key — even though their operational characteristics differ.
Bigtable’s design centers on a single, carefully chosen row key, sorted lexicographically, with all reads/writes for a row key routed to the tablet server currently responsible for that key range. This makes row key design the Bigtable equivalent of Cassandra’s partition key problem, with one extra wrinkle: because row keys are stored in sorted order and split into contiguous key ranges (tablets) across servers, a monotonically increasing key (like a raw timestamp) causes all new writes to land on the same “hot” tablet at the end of the key space. The standard mitigation is to salt or reverse the key — prefixing with a hashed or reversed value, or leading with a natural entity ID before the timestamp (e.g. device123#reversed_timestamp) — to spread writes across tablets.
In data engineering pipelines, Bigtable shows up most often as:
- The backing store for high-throughput time-series and IoT ingestion on Google Cloud — sensor data, ad-tech clickstreams, financial tick data — often sitting directly behind Dataflow/Beam pipelines that write processed events into Bigtable for both serving and further batch export to BigQuery.
- A serving layer read by both applications and analytical exports — Bigtable integrates natively with Dataflow and BigQuery (via federated queries or export jobs), making the “compute in a pipeline, serve from Bigtable, periodically batch-export to BigQuery for deep analytics” pattern very common in GCP-centric stacks.
- A store chosen specifically for its linear scalability to petabyte-scale datasets and millions of operations per second, when Cloud SQL or Firestore’s throughput ceilings would be a bottleneck.
MongoDB in depth
MongoDB’s document model is a natural fit for the “source” side of the ingestion/serving split precisely because product teams love it for the same reasons it’s painful to extract from: it embraces evolving, nested, semi-structured data. A user profile, a product catalog entry, or an event payload can gain new optional fields release after release without a migration — which is exactly why MongoDB is so often the operational store a data engineer later needs to extract from and conform into a rigid warehouse schema (see the schema-on-read discussion above).
MongoDB’s aggregation pipeline is worth understanding specifically because it is MongoDB’s own built-in ETL/transformation mechanism, and it can meaningfully reduce the work a downstream pipeline has to do. An aggregation pipeline is a sequence of stages — $match (filter), $project (reshape/select fields), $unwind (flatten an array into multiple documents), $group (aggregate), $lookup (a left-outer-join-like stage against another collection), $sort, $out/$merge (write results to a collection) — chained together, conceptually similar to a SQL query built from composable operators rather than a single declarative statement. For a data engineer, this matters in two ways:
- Some flattening and shaping work (unwinding arrays, projecting only needed fields, pre-joining a reference collection) can be pushed down into MongoDB itself via an aggregation pipeline before extraction, reducing the volume of data moved and the transformation logic needed downstream.
$merge/$outcan materialize an aggregation’s result into a new collection, effectively letting MongoDB serve as its own lightweight transformation layer for read-heavy reporting collections that mirror what would otherwise be a downstream warehouse table.
Operationally, MongoDB is extremely common as the primary datastore behind content-heavy and catalog-heavy applications (CMSs, product catalogs, user profiles, mobile app backends), which makes it one of the most frequently extracted-from NoSQL sources data engineers encounter in practice — usually via Change Streams for incremental CDC or scheduled full/incremental dumps for less time-sensitive reporting needs.
Graph databases: when relationships are the analysis
Most NoSQL usage in a data pipeline treats relationships as an inconvenience to flatten away. Graph databases exist for the opposite case: when the relationships between entities are themselves the object of analysis, not incidental structure to normalize out. Fraud rings, recommendation engines, social network analysis, supply-chain dependency mapping, and identity-resolution (“is this the same person across five different accounts?”) all share a structural signature — the interesting answers live in multi-hop paths and connectivity patterns, not in any single row’s attributes.
The practical reason to reach for Neo4j or Amazon Neptune instead of forcing the problem into a relational or document model is performance at traversal depth. A relational query answering “friends of friends of friends who also follow account X” requires a chain of self-joins whose cost grows multiplicatively with each hop, and query planners degrade badly past two or three levels. A graph database stores adjacency directly — each node holds pointers to its edges — so traversing to the next hop is a constant-time pointer-follow regardless of how large the overall graph is. This is often summarized as graph databases making relationships first-class citizens rather than values computed at query time via joins.
- Neo4j — the most widely adopted property graph database, queried via Cypher, a declarative pattern-matching language that reads close to how a person would describe a graph pattern (
(a:Person)-[:FRIENDS_WITH]->(b:Person)-[:FRIENDS_WITH]->(c:Person)). Common data-engineering use: batch-loading transaction or interaction data into Neo4j to run fraud-ring or community-detection algorithms, then feeding flagged entities back into downstream systems. - Amazon Neptune — AWS’s managed graph database, supporting both the property-graph model (queried via openCypher or Gremlin) and RDF/SPARQL for semantic-graph and knowledge-graph use cases. Neptune fits naturally into AWS-centric pipelines: data landed in S3 or a warehouse can be transformed and bulk-loaded into Neptune (via its bulk loader from S3) to power relationship-heavy applications like recommendation engines or identity graphs, with Neptune serving low-latency traversal queries back to applications.
For a data engineer, the practical trigger for reaching for a graph store is a concrete performance and expressiveness ceiling: once a “find connected/related entities” query needs more than two or three joins to express, and especially once it needs a variable or unbounded number of hops (e.g. “any path of length ≤ 6 connecting these two accounts”), a graph database usually outperforms and out-expresses the relational or document alternative by a wide margin.
CosmosDB: Azure’s multi-model store
Azure Cosmos DB takes a different approach from the single-model engines above: it is a multi-model database, exposing the same underlying globally distributed, horizontally partitioned engine through multiple APIs — the Core (SQL) API for document workloads, a MongoDB-compatible API, a Cassandra-compatible API (wide-column), a Gremlin API (graph), and a Table API (key-value). The pitch is that a team can pick the data model/API that fits the use case without provisioning a separate database product, while getting the same underlying guarantees: single-digit-millisecond latency SLAs, tunable consistency levels (five options from strong to eventual), and automatic global replication.
In an Azure-centric data stack, CosmosDB tends to appear on both sides of the split covered in this note: as an operational source (an application built on the SQL or MongoDB API), extracted via its Change Feed (CosmosDB’s equivalent of MongoDB Change Streams or DynamoDB Streams) into Azure Synapse or Data Factory pipelines; and as a serving layer, receiving processed results — recommendations, feature values, aggregates — from Synapse/Databricks pipelines for low-latency application consumption, the same role DynamoDB or Bigtable plays in AWS/GCP stacks. Its main relevance for a data engineer working across multiple clouds is recognizing that CosmosDB’s Change Feed and multi-API surface let it substitute for several of the engines above depending on which API a given application team chose.
Comparison: NoSQL family, pipeline role, and modeling concerns
| Family | Typical pipeline role | Example engine | Key data-modeling consideration |
|---|---|---|---|
| Key-value | Serving (fast lookups by ID) | DynamoDB, Redis | Design the key so the one access pattern is a single get; avoid needing secondary queries |
| Document | Source (operational store to extract from); occasional serving via app-facing reads | MongoDB, CouchDB, CosmosDB (SQL API) | Plan for schema drift and nested/repeated structures; use aggregation pipeline to pre-shape before extraction |
| Wide-column | Both — high-throughput source for events/telemetry, and serving for time-series/feature lookups | Cassandra, HBase, Bigtable | Partition/row key design controls both query performance and cluster load distribution |
| Graph | Source and serving for relationship-centric analysis (fraud, recs, identity) | Neo4j, Amazon Neptune | Model the traversal pattern first; multi-hop queries are the reason to choose graph at all |
| Multi-model | Both, depending on which API is used | Azure CosmosDB | Same modeling concerns as whichever API (document/wide-column/graph/key-value) the application chose |
Best Practices
- Land raw before you shape. When extracting from a schema-on-read source, keep an unmodified copy of the raw document/row in the lake before any flattening — it is the only reliable way to replay a pipeline after discovering a modeling mistake or unexpected schema drift.
- Detect schema drift, don’t silently absorb it. Alert on new top-level fields, type changes on existing fields, or unexpected nesting depth, rather than coercing everything into
NULLorSTRINGand losing the signal that the source model changed. - Prefer CDC (Change Streams / DynamoDB Streams / Change Feed) over full dumps once volume grows. Full extraction is simplest to build but doesn’t scale in cost or latency; every major managed NoSQL engine now ships a native change-capture mechanism designed for exactly this.
- Design serving-layer keys around the one query the application needs, not the general case. A serving store should almost never need a secondary index or scan; if it does, the aggregation/precompute step upstream probably needs to do more work.
- Get the partition/row key right before writing a single row into Cassandra or Bigtable. Changing a partition key after data volume grows means a full data migration; model the access pattern (what will be queried together, and how fast will this key’s cardinality grow) before choosing.
- Don’t force a relationship-heavy problem through relational joins just because a relational or document store is already in the stack. If a query needs unbounded-depth traversals or the business question is fundamentally “how are these entities connected,” evaluate a graph database rather than optimizing an increasingly unwieldy chain of joins.
- Keep the offline (batch/warehouse) and online (serving) feature or aggregate stores in sync deliberately. Treat the pipeline that materializes results into DynamoDB/Bigtable/Redis as a first-class part of the architecture, with its own monitoring for staleness, not an afterthought bolted onto a batch job.
- Match consistency-level choices to the actual requirement. Don’t default to
QUORUM/strong consistency everywhere out of caution — but do use it explicitly wherever a pipeline requires read-your-own-writes or ordering guarantees across replicas.
References
- Backend — NoSQL Databases — CAP theorem, the four families, and general data modeling (foundational reading for this note)
- Data Sources & Ingestion — broader ingestion patterns this note’s “source” direction builds on
- Data Lakes & Modern Architectures — where flattened/landed NoSQL extracts typically end up
- MongoDB — Aggregation Pipeline
- MongoDB — Change Streams
- Amazon DynamoDB — Streams and Export to S3
- DataStax — Cassandra Data Modeling & Partition Keys
- Google Cloud — Bigtable Schema Design
- Amazon Neptune — Documentation
- Azure Cosmos DB — Multi-Model Overview