Harbor

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.

15 phút đọcKhái niệm cốt lõi

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:

  1. Loại database: bảng quan hệ, document, wide-column, key-value hay graph.
  2. 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.
  3. 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ệuTruy vấn mạnhTruy vấn yếuVí dụ
RelationalDòng trong bảng có schemaJoin, transaction ACID, truy vấn tùy ý, ràng buộcScale ghi ngang, dữ liệu biến đổi nhiều giữa các bản ghiPostgreSQL, MySQL
DocumentDocument JSON lồng nhauĐọc trọn một thực thể trong một lần, schema linh hoạtJoin nhiều collection, transaction nhiều document5MongoDB, Firestore
Wide-columnDòng theo partition key, cột sắp theo sort keyGhi rất nhiều, đọc theo partition và khoảng sort keyTruy vấn không theo partition key, join, aggregateCassandra, ScyllaDB, HBase
Key-valueCặp key và valueGet, set theo key, độ trễ dưới msMọi thứ không theo keyRedis, DynamoDB (cơ bản), etcd
GraphNode và cạnh có thuộc tínhDuyệt quan hệ nhiều bước: bạn của bạn, đường điChia partition ra nhiều node, khó scale ngangNeo4j, 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óaPhi chuẩn hóa
ĐọcJoin hoặc nhiều truy vấnMột truy vấn
GhiMột chỗNhiều chỗ, thường bất đồng bộ
Nhất quánLuôn đúngLệch tạm thời
Dung lượngÍtNhiều hơn
Hợp vớiDữ 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_count trê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 authorName trong 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) khiTham chiếu (reference) khi
Document con nhỏDocument con lớn
Ít thay đổiThay đổi thường xuyên
Chấp nhận eventual consistencyCần nhất quán ngay
Số lượng tăng ít, có giới hạnTăng không giới hạn
Gần như luôn đọc cùng chaThường bị bỏ ra khi đọc cha
Cần đọc nhanhCầ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ƯuNhượcDùng khi
Auto incrementNhỏ, có thứ tự, dễ debugKhô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ấtMột database quan hệ, id nội bộ
UUID v4Sinh ở đâu cũng được, không cần phối hợp128 bit, làm index khóa chính chậm và chèn chậm, không tăng dầnId công khai, hệ phân tán đơn giản
UUID v7, ULIDSinh phân tán, có thứ tự theo thời gian16 byteLự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 timestampCầ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ênKhông cần id thêmGiá trị thật có thể đổiMã 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

Đang vẽ sơ đồ…

Ví dụ bài Chat:

Truy vấnTần suấtThiết kế
Lấy 50 tin mới nhất của hội thoạiRất caoPartition conversation_id, sort message_id giảm dần
Gửi tin vào hội thoạiCaoGhi 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ấtCaoBảng conversations_by_user partition user_id, sort last_message_at
Tìm tin nhắn theo từ khóaThấ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 GBVà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 URLKey-value code → longUrl, khóa tự nhiên là mã ngắn
ChatWide-column, partition theo hội thoại, bucket theo tháng cho nhóm lớn
News feedBài viết chuẩn hóa trong DB, timeline phi chuẩn hóa trong cache
Đặt vé sự kiệnRelational, bảng seats có trạng thái, transaction khi giữ ghế
Thanh toánLedger ghi thêm, không sửa, số tiền là số nguyên
InstagramMetadata trong DB, ảnh trong object storage, bảng follow hai chiều
YelpRelational cho doanh nghiệp, geo index, review tách riêng
Google DocsSnapshot tài liệu cộng log các thao tác
Đấu giá onlineBảng bid ghi thêm, giá cao nhất phi chuẩn hóa lên phiên đấu giá
Giám sát metricsTime-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

  1. 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

  2. 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

  3. Grokking Modern System Design · ch.9 Databases · bài "Data Partitioning", mục "Sharding" (hotspots) và "Key-range based sharding" (Disadvantages). ↩ ↩2 ↩3 ↩4

  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

  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. ↩

  6. 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

  7. Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations · mục "Standard numbers to remember", bảng "Important Rates". ↩ ↩2 ↩3

  8. 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

  9. Grokking Modern System Design · ch.9 Databases · bài "Types of Databases", mục "Reduced redundancy". ↩

  10. 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

  11. Grokking Modern System Design · ch.32 Design Newsfeed System · phần thiết kế cấp cao, "Newsfeed generation service" và "Newsfeed cache". ↩ ↩2

  12. 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

  13. MongoDB: The Definitive Guide 2e (file .zip, tải về giải nén) · ch.8 Application Design · mục "Cardinality" (tr. 157–158). ↩

  14. 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). ↩

  15. 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). ↩

  16. 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. ↩

  17. 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

  18. 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

  19. Quy tắc ước lượng của khoá học, xem bài Chọn server. ↩ ↩2

  20. Grokking Modern System Design · ch.36 Design WhatsApp · phần ước lượng storage (100 B mỗi tin). ↩

  21. Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations · mục "Types of data center servers", bảng "Typical Server Specifications". ↩

  22. 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. ↩

  23. Grokking Modern System Design · ch.34 TinyURL · phần chọn database (MongoDB). ↩