Mở đầu — vì sao bài này quan trọng
Trong sự nghiệp làm kiến trúc, có một câu hỏi bạn sẽ nghe đi nghe lại trong phòng họp: "Sao mình không dùng NoSQL cho cái này?" Và đáng buồn là phần lớn câu hỏi đó không xuất phát từ nhu cầu kỹ thuật thực sự, mà từ sự phấn khích với công nghệ mới, hoặc từ một bài blog ai đó vừa đọc tối qua. Là người ra quyết định kiến trúc, nhiệm vụ của bạn không phải là chọn thứ "hiện đại" — mà là chọn thứ đúng với đặc tính dữ liệu và mô hình truy vấn của bài toán.
Ở Bài 26, chúng ta đã bàn về SQL — khi nào chọn và cách chọn database quan hệ. Bài 27 này là mặt còn lại của đồng xu: khi nào bạn KHÔNG nên dùng SQL, và nếu đã quyết định rời khỏi quan hệ thì trong bốn họ NoSQL, bạn chọn họ nào. Đây không phải bài "NoSQL tốt hơn SQL" hay ngược lại. Đây là bài giúp bạn nhận diện chính xác đặc điểm bài toán để không phải trả giá bằng một cuộc migration đau đớn sau 18 tháng.
Tôi đã chứng kiến nhiều đội — cả ở startup Việt Nam lẫn công ty lớn — chọn MongoDB vì "nó linh hoạt, không cần thiết kế schema trước", rồi hai năm sau ngồi viết hàng loạt script vá dữ liệu vì mỗi document một kiểu. Ngược lại, có đội cố nhét một biểu đồ quan hệ mạng xã hội vào PostgreSQL với hàng chục bảng nối, để rồi mỗi truy vấn "bạn của bạn" mất 8 giây. Cả hai đều là lỗi ra quyết định, không phải lỗi công nghệ.
Khái niệm cốt lõi
NoSQL không phải một thứ — nó là bốn thứ khác nhau
Sai lầm đầu tiên và phổ biến nhất: coi "NoSQL" như một lựa chọn duy nhất. Thực tế NoSQL là một cái ô che bốn họ database có mô hình dữ liệu khác nhau về bản chất, giải quyết những bài toán rất khác nhau. Nói "chọn NoSQL" mà không nói rõ họ nào thì cũng vô nghĩa như nói "chọn phương tiện giao thông" mà không phân biệt xe máy, ô tô, tàu hỏa hay máy bay.
| Loại | Ví dụ điển hình | Mô hình dữ liệu | Use case phù hợp |
|---|---|---|---|
| Document | MongoDB, Couchbase, Firestore | JSON/BSON lồng nhau | Dữ liệu bán cấu trúc, schema thay đổi nhanh, đọc/ghi theo cả object |
| Key-Value | Redis, DynamoDB, Riak | Cặp khóa–giá trị đơn giản | Cache, session, đọc/ghi cực nhanh theo khóa đã biết |
| Wide-Column | Cassandra, HBase, ScyllaDB | Hàng có nhiều cột động, phân vùng theo khóa | Ghi khổng lồ, time-series, dữ liệu phân tán nhiều datacenter |
| Graph | Neo4j, Amazon Neptune, ArangoDB | Node và cạnh (quan hệ là công dân hạng nhất) | Mạng xã hội, đề xuất, phát hiện gian lận, phả hệ quan hệ |
Document Store — linh hoạt schema, đọc/ghi theo aggregate
Document store lưu dữ liệu dưới dạng document JSON lồng nhau. Sức mạnh của nó nằm ở chỗ: một aggregate (một thực thể nghiệp vụ hoàn chỉnh) được lưu và đọc trong một document duy nhất, không cần join. Ví dụ một đơn hàng cùng toàn bộ line items, địa chỉ giao, lịch sử trạng thái — tất cả nằm trong một document.
Bạn chọn document store khi: hình dạng dữ liệu thay đổi thường xuyên (sản phẩm e-commerce mỗi ngành hàng có thuộc tính khác nhau — điện thoại có RAM, áo có size, thực phẩm có hạn dùng), khi bạn thường đọc và ghi cả object cùng lúc, và khi bạn ưu tiên tốc độ lặp phát triển hơn tính toàn vẹn quan hệ chặt chẽ.
Bạn TRÁNH document store khi: dữ liệu có nhiều quan hệ many-to-many cần join, khi bạn cần transaction đa document mạnh mẽ và thường xuyên, hoặc khi bạn cần chạy các báo cáo phân tích ad-hoc phức tạp cắt ngang nhiều thực thể.
Key-Value — đơn giản đến mức nhanh khủng khiếp
Key-value là mô hình đơn giản nhất: bạn đưa một khóa, nhận về một giá trị. Không có truy vấn phức tạp, không join, không lọc theo giá trị bên trong (trừ khi có index phụ). Đổi lại, độ trễ thấp đến mức micro-giây và khả năng scale gần như tuyến tính.
Bạn chọn key-value khi: bạn luôn biết chính xác khóa cần truy cập. Session của người dùng (khóa = session_id), cache kết quả tính toán (khóa = hash của tham số), giỏ hàng, đếm lượt xem, feature flag. DynamoDB của AWS là key-value (mở rộng thành document) và là xương sống cho nhiều hệ thống quy mô lớn nhờ độ trễ ổn định ở mức single-digit millisecond bất kể dữ liệu lớn cỡ nào.
Bạn TRÁNH key-value khi cần truy vấn theo điều kiện trên giá trị ("cho tôi tất cả user ở Hà Nội") — vì mô hình này không được sinh ra để làm điều đó.
Wide-Column — cỗ máy ghi khổng lồ
Wide-column (Cassandra là đại diện) được thiết kế cho khối lượng ghi cực lớn, phân tán trên nhiều node và nhiều datacenter, với high availability. Điểm mấu chốt cần hiểu: trong Cassandra, bạn thiết kế bảng theo truy vấn, không phải theo dữ liệu. Bạn biết trước mình sẽ query thế nào rồi mới thiết kế partition key sao cho query đó chỉ chạm một node.
Bạn chọn wide-column khi: ghi nhiều hơn đọc rất nhiều, dữ liệu time-series (log, metric, sự kiện IoT, lịch sử giao dịch), cần availability cao ưu tiên hơn consistency tức thời, và cần scale ngang qua nhiều region.
Bạn TRÁNH wide-column khi: mô hình truy vấn chưa rõ ràng và hay thay đổi (vì bạn không thể query linh hoạt), khi cần join, hoặc khi dữ liệu nhỏ (dùng Cassandra cho dữ liệu vài GB là lấy dao mổ trâu giết gà, chi phí vận hành không đáng).
Graph — khi quan hệ quan trọng hơn thực thể
Graph database coi quan hệ là công dân hạng nhất. Trong SQL, để đi từ "A là bạn của B, B là bạn của C" bạn phải self-join bảng nhiều lần và hiệu năng sụp đổ theo độ sâu. Trong graph database, việc "đi bộ" qua các cạnh (traversal) có chi phí gần như không đổi theo số cạnh bạn đi, bất kể tổng đồ thị lớn cỡ nào.
Bạn chọn graph khi: bài toán bản chất là về mạng lưới quan hệ — đề xuất bạn bè, phát hiện vòng gian lận (tài khoản A chuyển tiền B chuyển C chuyển lại A), truy vết quan hệ sở hữu doanh nghiệp, hệ thống recommendation "người mua X cũng mua Y". Nếu bạn thấy mình viết truy vấn có nhiều tầng join lồng nhau trên cùng loại quan hệ, đó là tín hiệu graph.
Khung ra quyết định: KHÔNG phải SQL khi nào?
Trước khi chọn họ NoSQL nào, hãy tự hỏi: có lý do gì để rời khỏi SQL không? SQL vẫn nên là mặc định cho phần lớn ứng dụng. Bạn chỉ nên rời đi khi gặp một trong các tín hiệu rõ ràng:
- Quy mô ghi/đọc vượt giới hạn scale dọc của một instance quan hệ và việc sharding SQL thủ công trở nên đau đớn.
- Hình dạng dữ liệu vốn không phù hợp bảng phẳng — lồng sâu, đa dạng theo từng bản ghi, hoặc bản chất là đồ thị.
- Mô hình truy cập đơn giản và cố định đến mức sức mạnh của SQL (join, query tùy ý) bị lãng phí, trong khi bạn cần độ trễ và throughput mà key-value/wide-column cho được.
Tình huống thực tế
Ví dụ 1 — Sàn thương mại điện tử Việt: catalog sản phẩm đa ngành hàng
Một sàn TMĐT tầm trung ở TP.HCM (giả định, quy mô ~2 triệu SKU) ban đầu để toàn bộ sản phẩm trong PostgreSQL với một bảng products và các bảng thuộc tính phụ. Vấn đề xuất hiện khi họ mở rộng từ điện thoại sang thời trang, thực phẩm, đồ gia dụng: mỗi ngành hàng có bộ thuộc tính hoàn toàn khác — điện thoại cần RAM/ROM/chip, áo cần size/màu/chất liệu, sữa cần hạn sử dụng/xuất xứ. Đội đã tạo một bảng product_attributes kiểu key-value dạng EAV (Entity-Attribute-Value), và mỗi trang chi tiết sản phẩm phải join 15–20 dòng thuộc tính, rồi ghép lại trong tầng ứng dụng.
Họ chuyển catalog sản phẩm (chỉ phần catalog, không phải toàn hệ thống) sang MongoDB. Mỗi sản phẩm là một document chứa đủ mọi thuộc tính đặc thù ngành hàng của nó. Truy vấn trang chi tiết từ 15–20 dòng join xuống còn một lần đọc document. Thời gian phát triển tính năng cho ngành hàng mới giảm mạnh vì không cần migration schema.
Bài học: Document store tỏa sáng khi hình dạng dữ liệu đa dạng theo từng bản ghi và bạn đọc/ghi theo cả object. Nhưng lưu ý — họ không chuyển đơn hàng, thanh toán, tồn kho sang Mongo. Những phần đó cần transaction và tính toàn vẹn quan hệ, nên vẫn ở PostgreSQL. Đây là bài học lớn nhất: bạn chọn database theo từng phần của hệ thống, không phải chọn một cái cho tất cả (polyglot persistence).
Ví dụ 2 — Ví điện tử: chống gian lận bằng graph
Một ví điện tử tại Đông Nam Á cần phát hiện các vòng rửa tiền: tiền chảy qua chuỗi tài khoản A → B → C → D rồi quay lại A, hoặc nhiều tài khoản cùng trỏ về một thiết bị/số điện thoại. Đội rủi ro ban đầu viết truy vấn trên PostgreSQL để dò các vòng này, nhưng để phát hiện vòng dài 4–6 bước, họ phải self-join bảng giao dịch 4–6 lần. Với hàng chục triệu giao dịch mỗi tháng, một số truy vấn dò vòng chạy mất hàng chục giây, không thể chạy real-time.
Họ đưa một bản sao dữ liệu quan hệ tài khoản–thiết bị–giao dịch vào Neo4j. Bài toán "tìm vòng có độ dài ≤ 6 quay về node gốc" trở thành một traversal đồ thị tự nhiên, chạy trong mili-giây thay vì hàng chục giây. Việc phát hiện "nhiều tài khoản chia sẻ chung một thiết bị và một pattern nạp tiền" cũng trở nên trực quan.
Bài học: Khi bài toán bản chất là về quan hệ nhiều tầng, graph database không chỉ nhanh hơn — nó biến một truy vấn không viết nổi thành một truy vấn tự nhiên. Lưu ý họ cũng dùng graph như một hệ thống chuyên biệt bên cạnh database giao dịch chính, không thay thế nó.
Ví dụ 3 — Nền tảng gọi xe: lưu vị trí và sự kiện time-series
Một nền tảng gọi xe (giả định quy mô khu vực) cần ghi lại vị trí GPS của tài xế mỗi vài giây, cộng với sự kiện chuyến đi (nhận cuốc, đón khách, hoàn thành). Đây là workload ghi cực nặng: hàng trăm nghìn tài xế, mỗi người ghi liên tục. Một cluster PostgreSQL đơn không kham nổi tốc độ ghi này, và việc sharding thủ công đau đớn.
Họ chọn Cassandra cho luồng vị trí và sự kiện. Partition key thiết kế theo driver_id cộng cửa sổ thời gian, để mọi truy vấn "lịch sử vị trí của tài xế X trong khoảng thời gian Y" chỉ chạm một partition. Cassandra ghi khổng lồ mượt mà, scale ngang bằng cách thêm node, và chịu được mất một node mà không sập nhờ replication.
Bài học: Wide-column là lựa chọn đúng cho time-series ghi nặng khi bạn biết trước mô hình truy vấn và thiết kế partition key quanh nó. Nhưng nếu sau này họ cần query linh hoạt kiểu "tất cả tài xế đi qua quận 1 lúc 8h", Cassandra sẽ bó tay — vì nó không được thiết kế cho query bạn chưa lường trước. Đó là cái giá của throughput.
Hướng dẫn từng bước
Đây là quy trình ra quyết định tôi khuyên bạn dùng mỗi khi đứng trước câu hỏi "có nên dùng NoSQL không, và loại nào":
- Bắt đầu từ SQL làm mặc định. Viết ra lý do cụ thể vì sao SQL KHÔNG đủ. Nếu không viết nổi lý do thuyết phục, dừng lại và ở lại với SQL.
- Liệt kê các mô hình truy cập (access patterns). Bạn sẽ đọc/ghi dữ liệu này bằng cách nào? Luôn theo khóa đã biết? Theo quan hệ nhiều tầng? Theo bộ lọc tùy ý? Ghi nhiều hay đọc nhiều? Đây là input quan trọng nhất — NoSQL bắt bạn thiết kế theo truy vấn, không phải theo dữ liệu.
- Xác định hình dạng dữ liệu. Phẳng và đều? Lồng sâu và đa dạng? Bản chất đồ thị? Chuỗi thời gian?
- Đối chiếu với bảng bốn họ. Access pattern + hình dạng dữ liệu sẽ trỏ khá rõ về một họ. Query theo khóa → key-value. Object đa dạng, đọc/ghi cả cục → document. Ghi nặng time-series → wide-column. Quan hệ nhiều tầng → graph.
- Kiểm tra ràng buộc consistency và transaction. NoSQL thường đánh đổi consistency mạnh lấy availability và scale (chủ đề CAP sẽ đào sâu ở Bài 22). Nếu phần này cần transaction đa thực thể chặt chẽ (tiền, tồn kho), hãy cân nhắc rất kỹ — có thể phần đó nên ở lại SQL.
- Chấp nhận polyglot persistence. Đừng ép một database cho cả hệ thống. Rất bình thường khi đơn hàng ở PostgreSQL, session ở Redis, catalog ở MongoDB, phát hiện gian lận ở Neo4j.
- Ghi lại quyết định thành ADR. Nêu rõ access pattern, đánh đổi đã chấp nhận, và điều kiện nào sẽ khiến bạn xem xét lại. (Cấu trúc ADR đã học ở Bài 4.)
Lỗi thường gặp & mẹo
Lỗi 1 — Chọn NoSQL vì "không cần thiết kế schema". Đây là cái bẫy nguy hiểm nhất. NoSQL không phải "không schema" mà là "schema nằm trong code ứng dụng thay vì trong database". Nếu bạn không kỷ luật, sau hai năm mỗi document một kiểu và bạn phải viết code phòng thủ cho mọi trường hợp thiếu field. Mẹo: dù dùng document store, hãy áp schema validation ở tầng ứng dụng hoặc dùng tính năng schema validation của chính database.
Lỗi 2 — Coi MongoDB như PostgreSQL rồi cố join. Nếu bạn thấy mình cần $lookup liên tục để nối nhiều collection, đó là dấu hiệu dữ liệu của bạn vốn quan hệ và bạn đã chọn sai họ.
Lỗi 3 — Thiết kế wide-column mà chưa biết truy vấn. Cassandra phạt nặng nếu bạn thiết kế bảng trước rồi mới nghĩ query. Luôn bắt đầu từ danh sách truy vấn, rồi thiết kế partition key cho từng truy vấn.
Lỗi 4 — Dùng NoSQL cho dữ liệu quá nhỏ. Chi phí vận hành một cluster Cassandra hay một cụm Mongo replica set không đáng nếu dữ liệu của bạn chỉ vài GB. Một PostgreSQL đơn giản mạnh hơn bạn tưởng — nó chạy tốt đến hàng trăm GB, thậm chí TB.
Mẹo — Hỏi "khóa truy cập là gì?" trước tiên. Câu hỏi này lọc rất nhanh: nếu bạn luôn biết khóa → key-value; nếu bạn cần lọc theo nội dung → có lẽ cần document store có index hoặc quay lại SQL.
Mẹo — NoSQL và SQL không loại trừ nhau. Nhiều database hiện đại nhòe ranh giới: PostgreSQL có kiểu JSONB cho phép lưu document ngay trong bảng quan hệ. Với nhiều bài toán "cần linh hoạt schema nhưng chưa đến quy mô khổng lồ", JSONB trong PostgreSQL là câu trả lời tốt nhất — bạn có cả linh hoạt lẫn transaction, join, SQL quen thuộc. Đừng bỏ qua lựa chọn này trước khi nhảy sang MongoDB.
Bài tập thực hành
Bài tập 1 — Phân loại. Với mỗi bài toán dưới đây, hãy chọn họ NoSQL (hoặc SQL) phù hợp nhất và viết một câu lý do dựa trên access pattern:
- (a) Lưu session đăng nhập của người dùng, tra cứu theo session_id.
- (b) Lưu bài viết của một blog cùng comment lồng nhau, đọc cả bài lẫn comment một lần.
- (c) Ghi log sự kiện click của app di động, hàng trăm triệu bản ghi/ngày.
- (d) Xây tính năng "những người bạn có thể quen" cho một mạng xã hội.
- (e) Quản lý đơn hàng, thanh toán, trừ tồn kho có transaction.
Bài tập 3 — Thiết kế theo truy vấn. Giả sử bạn dùng Cassandra để lưu lịch sử vị trí tài xế. Truy vấn chính là "lấy vị trí của tài xế X trong ngày Y". Hãy đề xuất partition key và giải thích vì sao thiết kế đó khiến truy vấn chỉ chạm một partition.
(Gợi ý đáp án bài 1: a→key-value; b→document; c→wide-column; d→graph; e→SQL.)
Tóm tắt
NoSQL không phải một lựa chọn — nó là bốn họ database khác nhau về bản chất, mỗi họ giải một loại bài toán riêng. Document cho dữ liệu bán cấu trúc đa dạng, đọc/ghi theo aggregate. Key-value cho truy cập theo khóa đã biết với độ trễ cực thấp. Wide-column cho ghi khổng lồ và time-series với mô hình truy vấn cố định. Graph cho bài toán mà quan hệ nhiều tầng là trung tâm.
Nguyên tắc ra quyết định cốt lõi: giữ SQL làm mặc định và chỉ rời đi khi có tín hiệu rõ ràng — quy mô vượt scale dọc, hình dạng dữ liệu không hợp bảng phẳng, hoặc mô hình truy cập đơn giản/cố định cần throughput mà quan hệ không cho được. Luôn bắt đầu từ access pattern, không phải từ sự phấn khích công nghệ. Và hãy nhớ hai điều thực dụng: polyglot persistence là bình thường (mỗi phần hệ thống một database phù hợp), và JSONB trong PostgreSQL thường là điểm trung gian tốt nhất trước khi bạn thực sự cần nhảy sang một hệ NoSQL chuyên biệt. Cuối cùng, ghi lại quyết định thành ADR để tương lai biết bạn đã đánh đổi những gì và khi nào nên xem xét lại.