Mô hình dữ liệu
Cách chọn và thiết kế mô hình dữ liệu: relational, document, wide-column, key-value, graph; chuẩn hóa và phi chuẩn hóa, embedding và referencing, chọn primary key, thiết kế theo access pattern, kèm ví dụ từ các bài trong khóa.
Mô hình dữ liệu là gì
Mô hình dữ liệu là cách bạn biểu diễn các thực thể của bài toán (user, bài viết, ghế, đơn hàng) và quan hệ giữa chúng trong một kho lưu trữ cụ thể. Nó gồm ba quyết định:
- Loại database: bảng quan hệ, document, wide-column, key-value hay graph.
- Hình dạng dữ liệu: tách thành nhiều bảng hay gộp vào một document, trường nào lặp lại ở đâu.
- Khóa: primary key, partition key, sort key, index phụ.
Ba quyết định này phụ thuộc vào cùng một câu hỏi: hệ thống đọc và ghi dữ liệu theo những cách nào, bao nhiêu lần mỗi giây. Đó là access pattern.12
Vì sao quan trọng trong phỏng vấn
Sau phần API, người phỏng vấn thường hỏi "dữ liệu lưu thế nào". Câu trả lời yếu là liệt kê bảng và cột như một bài tập cơ sở dữ liệu ở trường. Câu trả lời tốt nối mỗi bảng với một truy vấn chính: "bảng messages có partition key là conversation_id, sort key là message_id, vì truy vấn chính là lấy 50 tin mới nhất của một hội thoại".
Mô hình dữ liệu cũng quyết định khả năng scale. Chọn sai partition key thì dữ liệu và truy vấn dồn vào vài partition (hotspot), thành nút thắt của cả hệ thống. Chọn đúng thì tải chia đều cho các node.3
Trong phỏng vấn: Chỉ cần vẽ 3–5 thực thể chính với các trường quan trọng (khóa, trường dùng để lọc, trường dùng để sắp xếp). Bỏ qua các trường hiển thị như avatarUrl, bio.
Năm loại mô hình
Grokking ch.9 chia database thành quan hệ và NoSQL, trong NoSQL có key-value, document, column-oriented và graph.4 Seven Databases in Seven Weeks cũng chia thành năm nhóm như vậy và nêu rõ mỗi nhóm hợp và không hợp với việc gì.2
| Loại | Đơn vị dữ liệu | Truy vấn mạnh | Truy vấn yếu | Ví dụ |
|---|---|---|---|---|
| Relational | Dòng trong bảng có schema | Join, transaction ACID, truy vấn tùy ý, ràng buộc | Scale ghi ngang, dữ liệu biến đổi nhiều giữa các bản ghi | PostgreSQL, MySQL |
| Document | Document JSON lồng nhau | Đọc trọn một thực thể trong một lần, schema linh hoạt | Join nhiều collection, transaction nhiều document5 | MongoDB, Firestore |
| Wide-column | Dòng theo partition key, cột sắp theo sort key | Ghi rất nhiều, đọc theo partition và khoảng sort key | Truy vấn không theo partition key, join, aggregate | Cassandra, ScyllaDB, HBase |
| Key-value | Cặp key và value | Get, set theo key, độ trễ dưới ms | Mọi thứ không theo key | Redis, DynamoDB (cơ bản), etcd |
| Graph | Node và cạnh có thuộc tính | Duyệt quan hệ nhiều bước: bạn của bạn, đường đi | Chia partition ra nhiều node, khó scale ngang | Neo4j, Neptune |
Các cột "mạnh" và "yếu" tóm theo Seven Databases ch.9 và Grokking ch.9.24 Riêng với wide-column, sách nhấn mạnh phải thiết kế schema theo cách định truy vấn, nên không hợp với báo cáo ad hoc.2
Cùng một dữ liệu "user theo dõi user" sẽ trông khác nhau:
CREATE TABLE follows (
follower_id BIGINT NOT NULL,
followee_id BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (follower_id, followee_id)
);
CREATE INDEX follows_by_followee ON follows (followee_id, created_at DESC);
-- Cassandra: một bảng cho mỗi chiều truy vấn
CREATE TABLE following_by_user (
user_id bigint, followee_id bigint, created_at timestamp,
PRIMARY KEY ((user_id), created_at, followee_id)
) WITH CLUSTERING ORDER BY (created_at DESC, followee_id ASC);
CREATE TABLE followers_by_user (
user_id bigint, follower_id bigint, created_at timestamp,
PRIMARY KEY ((user_id), created_at, follower_id)
) WITH CLUSTERING ORDER BY (created_at DESC, follower_id ASC);
Postgres dùng một bảng và hai index. Cassandra dùng hai bảng, ghi hai lần, vì nó chỉ đọc hiệu quả theo partition key.6 Microservices Patterns mô tả cùng nguyên tắc với DynamoDB: truy vấn quyết định schema, thao tác query() cần partition key cộng sort key, truy vấn khác cần thêm secondary index.1
Mẹo: Khi không có lý do đặc biệt, chọn database quan hệ như PostgreSQL. Grokking gọi database quan hệ là lựa chọn mặc định cho dữ liệu có cấu trúc, và gợi ý chọn NoSQL khi dữ liệu phi cấu trúc hoặc quá lớn cho một node.4 Mốc tham khảo của Grokking cho một server MySQL là khoảng 1.000 QPS.7 Chuyển sang loại khác khi có lý do cụ thể: ghi rất nhiều, cần độ trễ rất thấp, hoặc duyệt đồ thị sâu. Xem Chọn database.
Chuẩn hóa và phi chuẩn hóa
Chuẩn hóa (normalization): mỗi dữ liệu nằm ở đúng một chỗ, các chỗ khác tham chiếu tới bằng id. Sửa tên người dùng chỉ sửa một dòng. Đổi lại, đọc một màn hình cần join hoặc nhiều truy vấn.
Phi chuẩn hóa (denormalization): sao chép dữ liệu tới nơi cần đọc. Đọc nhanh trong một lần, nhưng sửa phải cập nhật nhiều chỗ và có thể lệch nhau một lúc.89
MongoDB: The Definitive Guide ch.8 tóm gọn: chuẩn hóa thường làm ghi nhanh hơn, phi chuẩn hóa làm đọc nhanh hơn. Tần suất thay đổi so với tần suất đọc của dữ liệu quyết định nghiêng về phía nào.8
| Tiêu chí | Chuẩn hóa | Phi chuẩn hóa |
|---|---|---|
| Đọc | Join hoặc nhiều truy vấn | Một truy vấn |
| Ghi | Một chỗ | Nhiều chỗ, thường bất đồng bộ |
| Nhất quán | Luôn đúng | Lệch tạm thời |
| Dung lượng | Ít | Nhiều hơn |
| Hợp với | Dữ liệu hay sửa, cần đúng tuyệt đối (số dư, tồn kho) | Đọc nhiều hơn ghi rất nhiều (feed, trang sản phẩm) |
Các dòng của bảng theo MongoDB TDG ch.8 (đọc một truy vấn, tốn chỗ hơn, khó đồng bộ hơn) và Bảng 8-1 của sách.810
Các dạng phi chuẩn hóa hay dùng trong phỏng vấn:
- Counter cache: lưu
like_counttrên bài viết thay vìCOUNT(*)mỗi lần đọc (ví dụ minh họa của khoá học). - Sao chép trường hiển thị: lưu
authorNametrong bài viết, cập nhật bất đồng bộ khi user đổi tên. MongoDB TDG khuyên có job định kỳ kiểm tra để chắc mọi bản sao đã được cập nhật.8 - Bảng đọc riêng (read model): newsfeed tính sẵn cho mỗi user và giữ trong newsfeed cache, như thiết kế News Feed của Grokking.11
- Index tìm kiếm: đẩy dữ liệu sang Elasticsearch qua Change data capture. Đây là một dạng CQRS view.12
Nguồn sự thật vẫn nên là một bảng chuẩn hóa. Các bản sao là dữ liệu dẫn xuất, có thể dựng lại; Microservices Patterns lưu ý dựng lại view từ đầu đơn giản về khái niệm nhưng khó trong thực tế, phải dùng event đã lưu trữ và snapshot.12
Embedding và referencing trong document database
Với MongoDB, câu hỏi chuẩn hóa thành "nhúng document con vào cha, hay để riêng và tham chiếu bằng id". MongoDB TDG ch.8 đưa ra bảng so sánh, tóm lại như sau:10
| Nhúng (embed) khi | Tham chiếu (reference) khi |
|---|---|
| Document con nhỏ | Document con lớn |
| Ít thay đổi | Thay đổi thường xuyên |
| Chấp nhận eventual consistency | Cần nhất quán ngay |
| Số lượng tăng ít, có giới hạn | Tăng không giới hạn |
| Gần như luôn đọc cùng cha | Thường bị bỏ ra khi đọc cha |
| Cần đọc nhanh | Cần ghi nhanh |
Sách cũng gợi ý chia "nhiều" thành "ít" và "nhiều": quan hệ "ít" thường hợp với nhúng, quan hệ "nhiều" hợp với tham chiếu. Ví dụ của sách: một bài viết có nhiều comment là quan hệ một-nhiều; cây comment hay danh sách hoạt động nên là document riêng, không nhúng.138 Có cách lai: nhúng một mảng nhỏ gồm id và vài trường hay hiển thị, phần chi tiết để ở collection riêng.8
// orders: nhúng các dòng hàng vì chúng nhỏ, cố định sau khi đặt và luôn đọc cùng đơn
{
_id: "ord_9f2",
userId: "u_17",
status: "PAID",
items: [
{ productId: "p_1", name: "Áo thun", price: 199000, qty: 2 },
{ productId: "p_8", name: "Mũ", price: 99000, qty: 1 }
],
shippingAddress: { city: "Đà Nẵng", line1: "12 Bạch Đằng" },
createdAt: ISODate("2026-11-03T08:00:00Z")
}
// comments: tách collection riêng, tham chiếu postId, vì số lượng không giới hạn
{ _id: "c_551", postId: "post_42", authorId: "u_3", body: "Hay quá", createdAt: ISODate("2026-11-03T08:05:00Z") }
Trong ví dụ trên, name và price được sao chép vào đơn hàng một cách có chủ ý: đơn hàng cần giữ giá tại thời điểm mua, kể cả khi sản phẩm đổi giá sau này. Đây là phi chuẩn hóa vì đúng nghiệp vụ, không chỉ vì hiệu năng (ví dụ và lập luận của khoá học).
Lưu ý: MongoDB giới hạn mỗi document 16 MB.14 Mảng nhúng tăng không giới hạn (comment, follower, lịch sử hoạt động) nên được tham chiếu thay vì nhúng; MongoDB TDG cũng cảnh báo trường follower thay đổi liên tục làm document to ra và bị phân mảnh.815
Chọn primary key và thiết kế theo access pattern
Primary key
| Kiểu khóa | Ưu | Nhược | Dùng khi |
|---|---|---|---|
| Auto increment | Nhỏ, có thứ tự, dễ debug | Không dùng được khi nhiều node tự sinh id; một DB trung tâm cấp id là điểm lỗi duy nhất | Một database quan hệ, id nội bộ |
| UUID v4 | Sinh ở đâu cũng được, không cần phối hợp | 128 bit, làm index khóa chính chậm và chèn chậm, không tăng dần | Id công khai, hệ phân tán đơn giản |
| UUID v7, ULID | Sinh phân tán, có thứ tự theo thời gian | 16 byte | Lựa chọn mặc định tốt cho bảng lớn16 |
| Snowflake (64 bit) | Có thứ tự theo thời gian, nhỏ, chứa timestamp | Cần cấp worker id, phụ thuộc đồng hồ | Tweet, tin nhắn, đơn hàng ở quy mô rất lớn |
| Khóa tự nhiên | Không cần id thêm | Giá trị thật có thể đổi | Mã quốc gia, mã ngắn của URL rút gọn |
Các dòng auto increment, UUID v4 và Snowflake theo Grokking ch.12.17 Dòng khóa tự nhiên là nhận xét của khoá học.
Trong database phân tán, khóa còn quyết định dữ liệu nằm ở shard nào. Partition key cần hai tính chất: chia đều tải (không có một key nóng nhận phần lớn request) và gom dữ liệu cần đọc cùng nhau vào cùng một partition. Hai mục tiêu này đôi khi ngược nhau.3 Ví dụ, partition theo conversation_id gom tin nhắn một hội thoại lại, nhưng nhóm chat 100.000 người sẽ thành partition nóng. Cách xử lý: thêm bucket theo thời gian vào khóa, như (conversation_id, yyyymm). MongoDB TDG gợi ý cách tương tự cho shard key: ghép khóa ít biến thiên với một khóa biến thiên nhiều như timestamp để tổ hợp có cardinality cao.18
Thiết kế theo access pattern
Quy trình bốn bước (cách trình bày của khoá học, dựa trên nguyên tắc "truy vấn quyết định schema"):12
Ví dụ bài Chat:
| Truy vấn | Tần suất | Thiết kế |
|---|---|---|
| Lấy 50 tin mới nhất của hội thoại | Rất cao | Partition conversation_id, sort message_id giảm dần |
| Gửi tin vào hội thoại | Cao | Ghi một dòng vào partition trên |
| Danh sách hội thoại của user, sắp theo tin mới nhất | Cao | Bảng conversations_by_user partition user_id, sort last_message_at |
| Tìm tin nhắn theo từ khóa | Thấp | Đẩy sang Elasticsearch |
Với database quan hệ, bước 3 và 4 thường là chọn index. Xem Database index. Với Cassandra và DynamoDB, mỗi truy vấn quan trọng thường có một bảng hoặc một GSI riêng.16
Con số tham khảo (ước lượng)
| Thông số | Giá trị |
|---|---|
| Postgres 4 vCPU · 16 GB | Vài nghìn QPS truy vấn đơn giản có index, thoải mái tới ~1–2 TB (quy tắc ước lượng của khoá học)19 |
| Grokking napkin | ~1.000 QPS MySQL, ~10.000 QPS key-value store, 100.000–1 triệu QPS cache server, xem như mốc tham khảo7 |
| Redis một node | ~100.000 ops/s (mốc cache server của Grokking); RAM = số item × (key + value + ~100 B) × 1,3 (quy tắc ước lượng của khoá học)719 |
| Một dòng tin nhắn | ~300 B gồm khóa và metadata (giả định của khoá học; Grokking ước 100 B cho nội dung một tin WhatsApp, chưa tính metadata)20 |
| 1 tỉ tin nhắn/ngày × 300 B | ~300 GB/ngày, ~110 TB/năm trước replication |
Con số cuối cho thấy vì sao chat cần một database chia được ra nhiều node: một node không giữ được 110 TB mỗi năm (server điển hình trong Grokking ch.5 có khoảng 15 TB ổ đĩa), còn database scale ngang thêm node để tăng cả dung lượng lẫn throughput.2143
Sai lầm hay gặp
- Thiết kế bảng trước khi biết truy vấn. Mô hình đẹp về lý thuyết nhưng truy vấn chính phải join năm bảng.1
- Dùng MongoDB rồi làm như database quan hệ. Mỗi màn hình cần ba bốn truy vấn tham chiếu nối tiếp; MongoDB TDG chỉ ra ví dụ student/class cần ba lượt gọi server và nói đó thường không phải cách nên tổ chức dữ liệu.8
- Nhúng mảng không giới hạn. Comment, follower, log hoạt động phải tách ra.8
- Partition key có độ phân tán thấp. Khóa chỉ có vài giá trị (như
country) không chia được dữ liệu ra nhiều phần hơn số giá trị đó, và dồn tải vào vài node.183 - Phi chuẩn hóa mà không nói cách đồng bộ. Phải nói rõ ai cập nhật bản sao, đồng bộ hay qua event, lệch bao lâu.8
- Dùng UUID v4 làm khóa chính cho bảng ghi rất nhiều. Khóa 128 bit ngẫu nhiên làm index khóa chính chậm và chèn chậm. Dùng id có thứ tự như Snowflake.17
- Lưu tiền bằng float. Dùng số nguyên theo đơn vị nhỏ nhất (đồng, cent) hoặc
NUMERIC.22
Sai lầm hay gặp: Nói "dùng NoSQL vì scale tốt hơn". Hãy nêu truy vấn và con số: "ghi 50.000 tin nhắn/giây, đọc luôn theo hội thoại, nên tôi chọn Cassandra với partition key là conversation_id". Grokking cũng lưu ý ranh giới giữa NoSQL và database truyền thống ngày càng mờ, nên chữ "NoSQL" tự nó không phải lý do.4
Khi nào nhắc tới trong phỏng vấn
Bảng dưới tóm lời giải của các bài trong khoá; nguồn chi tiết nằm trong từng bài. Một số điểm có trong Grokking: Rút gọn URL lưu ánh xạ trong NoSQL (Grokking chọn MongoDB),23 News feed giữ feed tính sẵn trong cache.11
| Bài | Điểm mô hình dữ liệu cần nói |
|---|---|
| Rút gọn URL | Key-value code → longUrl, khóa tự nhiên là mã ngắn |
| Chat | Wide-column, partition theo hội thoại, bucket theo tháng cho nhóm lớn |
| News feed | Bài viết chuẩn hóa trong DB, timeline phi chuẩn hóa trong cache |
| Đặt vé sự kiện | Relational, bảng seats có trạng thái, transaction khi giữ ghế |
| Thanh toán | Ledger ghi thêm, không sửa, số tiền là số nguyên |
| Metadata trong DB, ảnh trong object storage, bảng follow hai chiều | |
| Yelp | Relational cho doanh nghiệp, geo index, review tách riêng |
| Google Docs | Snapshot tài liệu cộng log các thao tác |
| Đấu giá online | Bảng bid ghi thêm, giá cao nhất phi chuẩn hóa lên phiên đấu giá |
| Giám sát metrics | Time-series, khóa gồm tên metric, tag và thời gian |
Đọc tiếp: Chọn database cho quyết định công nghệ, Database index cho cách làm truy vấn nhanh, Sharding cho cách chọn partition key ở quy mô lớn, và Cassandra, DynamoDB, PostgreSQL cho từng loại cụ thể.
Nguồn trích dẫn
-
Microservices Patterns (file .zip, tải về giải nén) · ch.7 · §7.4.2 Data modeling and query design with DynamoDB (tr. 244–246). ↩ ↩2 ↩3 ↩4 ↩5
-
Seven Databases in Seven Weeks (file .zip, tải về giải nén) · ch.1 Introduction · §1.2 The Genres (tr. 3–7) và ch.9 Wrapping Up · §9.1 Genres Redux (tr. 307–311). ↩ ↩2 ↩3 ↩4 ↩5
-
Grokking Modern System Design · ch.9 Databases · bài "Data Partitioning", mục "Sharding" (hotspots) và "Key-range based sharding" (Disadvantages). ↩ ↩2 ↩3 ↩4
-
Grokking Modern System Design · ch.9 Databases · bài "Types of Databases", các mục "Relational databases", "Why non-relational (NoSQL) databases?", "Types of NoSQL databases", "Choose the right database". ↩ ↩2 ↩3 ↩4 ↩5
-
MongoDB: The Definitive Guide 2e (file .zip, tải về giải nén) · ch.8 Application Design · mục "When Not to Use MongoDB" (tr. 165). Sách bản 2 viết MongoDB chưa hỗ trợ transaction và không mạnh về join nhiều loại dữ liệu; các bản MongoDB mới hơn đã có transaction nhiều document nhưng thư viện chưa có sách cập nhật. ↩
-
Chưa có nguồn trong thư viện: thư viện không có sách về Cassandra/CQL; cú pháp bảng và nhận xét "một bảng cho mỗi truy vấn" chỉ nêu ở mức khái niệm. ↩ ↩2
-
Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations · mục "Standard numbers to remember", bảng "Important Rates". ↩ ↩2 ↩3
-
MongoDB: The Definitive Guide 2e (file .zip, tải về giải nén) · ch.8 Application Design · mục "Normalization versus Denormalization" (tr. 153–156). ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10
-
Grokking Modern System Design · ch.9 Databases · bài "Types of Databases", mục "Reduced redundancy". ↩
-
MongoDB: The Definitive Guide 2e (file .zip, tải về giải nén) · ch.8 Application Design · Table 8-1 "Comparison of embedding versus references" (tr. 157). ↩ ↩2
-
Grokking Modern System Design · ch.32 Design Newsfeed System · phần thiết kế cấp cao, "Newsfeed generation service" và "Newsfeed cache". ↩ ↩2
-
Microservices Patterns (file .zip, tải về giải nén) · ch.7 · §7.2 Using the CQRS pattern và §7.3.3 Adding and updating CQRS views (tr. 241–242). ↩ ↩2
-
MongoDB: The Definitive Guide 2e (file .zip, tải về giải nén) · ch.8 Application Design · mục "Cardinality" (tr. 157–158). ↩
-
MongoDB: The Definitive Guide 2e (file .zip, tải về giải nén) · ch.3 Creating, Updating, and Deleting Documents · mục "Insert Validation" (tr. 30). ↩
-
MongoDB: The Definitive Guide 2e (file .zip, tải về giải nén) · ch.8 Application Design · mục "Friends, Followers, and Other Inconveniences" (tr. 158–159). ↩
-
Chưa có nguồn trong thư viện: UUID v7 và ULID mới hơn các sách trong thư viện; giữ lại vì là lựa chọn phổ biến hiện nay. ↩
-
Grokking Modern System Design · ch.12 Sequencer · bài "Design of a Unique ID Generator", các mục "First solution: UUID", "Second solution: using a database", "Twitter Snowflake". ↩ ↩2
-
MongoDB: The Definitive Guide 2e (file .zip, tải về giải nén) · ch.15 Choosing a Shard Key · mục "Shard Key Cardinality" (tr. 271). ↩ ↩2
-
Quy tắc ước lượng của khoá học, xem bài Chọn server. ↩ ↩2
-
Grokking Modern System Design · ch.36 Design WhatsApp · phần ước lượng storage (100 B mỗi tin). ↩
-
Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations · mục "Types of data center servers", bảng "Typical Server Specifications". ↩
-
Chưa có nguồn trong thư viện: lời khuyên không lưu tiền bằng số thực là thực hành phổ biến, chưa tìm thấy đoạn sách tương ứng. ↩
-
Grokking Modern System Design · ch.34 TinyURL · phần chọn database (MongoDB). ↩