SQL & Database — Tư tưởng & Nguyên lý/Các mô hình dữ liệu — relational, document, key-value, graph
6/51
Bài 6 / 51~18 phútNền tảng & mô hình dữ liệuMiễn phí lượt xem

Các mô hình dữ liệu — relational, document, key-value, graph

Relational là một mô hình trong nhiều lựa chọn. Tại sao document/KV/graph tồn tại, khi nào chúng thắng relational, impedance mismatch và schema-on-read.

TL;DR: Trước khi có relational, người ta lưu dữ liệu dưới dạng cây (hierarchical) hay mạng lưới (network). Sau Codd 1970, relational thống trị vài thập kỷ — nhưng không có mô hình nào phù hợp mọi bài toán. Document model lưu dữ liệu dạng cây/aggregate (JSON-like) — đọc ghi cả khối, ít JOIN; key-value cho tra cứu cực nhanh theo khoá; graph cho dữ liệu có nhiều mối quan hệ phức tạp như mạng xã hội. Hiểu landscape này giúp bạn biết relational đứng ở đâu và khi nào cần chọn khác.

Bạn vừa xây một ứng dụng blog: mỗi bài viết có nhiều tag, nhiều comment, mỗi comment của một user có profile. Bạn model hết vào bảng SQL: posts, tags, post_tags, comments, users, user_profiles — 6 bảng, mỗi lần render một trang blog cần JOIN 5 bảng. Tốn thời gian viết query, tốn thời gian giải thích schema cho đồng đội.

Đồng nghiệp gợi ý: "Dùng MongoDB đi, lưu post kèm comment và tag hết vào một document JSON luôn, đọc ra là xong." Bạn thử — thực sự nhanh hơn để build. Nhưng rồi người dùng hỏi: "Cho tôi xem tất cả comment của user này trên mọi bài viết." Và việc query ngược từ comment về user qua mọi document trở nên cồng kềnh hơn bạn tưởng.

Hai cách tiếp cận đều đúng trong ngữ cảnh của mình. Bài này giải thích tại sao — và khi nào mỗi mô hình tỏa sáng.

1. Analogy — Cách tổ chức hồ sơ bệnh viện

Tưởng tượng một bệnh viện cần lưu hồ sơ bệnh nhân:

Cách A — Hồ sơ đóng gói: mỗi bệnh nhân có một phong bì riêng, bên trong có tất cả: giấy khám bệnh, đơn thuốc, kết quả xét nghiệm, lịch sử tái khám. Lấy ra một bệnh nhân là có mọi thứ. Nhưng nếu muốn biết "bác sĩ Hà đã khám bao nhiêu bệnh nhân tuần này?", phải mở từng phong bì một tìm.

Cách B — Sổ đăng ký phân loại: bệnh nhân ghi vào sổ A, đơn thuốc vào sổ B, lịch khám vào sổ C — mỗi sổ liên kết nhau qua số ID. Truy vấn chéo dễ hơn. Nhưng để xem toàn bộ hồ sơ một bệnh nhân cụ thể, phải đối chiếu 3 sổ.

Cách C — Thẻ tra cứu nhanh: chỉ lưu số phòng hiện tại của bệnh nhân — tra tên là biết ngay đang ở đâu. Không có gì khác, nhưng cực kỳ nhanh.

Cách D — Bản đồ quan hệ: vẽ ai quen ai, ai cùng phòng ai, ai là người thân của ai — phù hợp khi bệnh viện muốn theo dõi chuỗi lây nhiễm.

Cách lưu hồ sơMô hình dữ liệu
Phong bì đóng gói theo bệnh nhânDocument model
Sổ đăng ký phân loại + ID liên kếtRelational model
Thẻ tra cứu nhanh (tên → phòng)Key-value model
Bản đồ quan hệ (ai quen ai)Graph model
💡 Cách nhớ

Mỗi mô hình là cách tổ chức khác nhau cho cùng dữ liệu — không có "đúng/sai" tuyệt đối, chỉ có "phù hợp/không phù hợp" với bài toán cụ thể.

2. Relational model — nhiều quan hệ, query linh hoạt

Relational model (đã học ở bài 02) lưu dữ liệu thành các bảng liên kết nhau qua khoá ngoại. Điểm mạnh cốt lõi: query linh hoạt theo mọi chiều — bất kỳ câu hỏi nào có thể trả lời bằng SQL mà không cần thiết kế lại cấu trúc.

-- Cung mot du lieu, co the query theo nhieu chieu khac nhau:

-- Cau hoi A: tat ca bai viet cua user 42
SELECT p.title, p.created_at
FROM posts p
WHERE p.user_id = 42
ORDER BY p.created_at DESC;

-- Cau hoi B: tat ca comment cua user 42 tren moi bai viet
SELECT p.title, c.body, c.created_at
FROM comments c
JOIN posts p ON p.id = c.post_id
WHERE c.user_id = 42;

-- Cau hoi C: tag nao duoc dung nhieu nhat trong 30 ngay qua
SELECT t.name, COUNT(*) AS usage_count
FROM tags t
JOIN post_tags pt ON pt.tag_id = t.id
JOIN posts p      ON p.id = pt.post_id
WHERE p.created_at >= CURRENT_TIMESTAMP - INTERVAL '30' DAY
GROUP BY t.name
ORDER BY usage_count DESC;

Ba câu hỏi A, B, C đều trả lời được mà không cần thay đổi schema. Đây là query flexibility — thế mạnh của relational mà document model khó match.

Relational thắng khi dữ liệu có nhiều loại quan hệ phức tạp (user có nhiều post, post có nhiều tag, tag dùng chung giữa nhiều post) và bạn cần query linh hoạt theo nhiều chiều khác nhau.

3. Document model — aggregate, đọc ghi cả khối

Document model lưu dữ liệu dưới dạng document tự chứa (JSON hoặc BSON) — mỗi document là một "aggregate" hoàn chỉnh, chứa tất cả thông tin liên quan theo cấu trúc lồng nhau.

{
  "id": "post-891",
  "title": "Gioi thieu Docker",
  "author": {
    "id": "user-42",
    "name": "Nguyen Van A",
    "avatar": "/avatars/42.png"
  },
  "tags": ["docker", "devops", "container"],
  "comments": [
    {
      "id": "cmt-001",
      "body": "Bai viet rat hay!",
      "author": { "id": "user-17", "name": "Tran Thi B" },
      "created_at": "2024-03-15T09:30:00Z"
    }
  ],
  "created_at": "2024-03-14T10:00:00Z"
}

Đọc một bài viết kèm tác giả, tag và comment: 1 lần đọc, không JOIN. Đây là data locality — dữ liệu cần cho một use case được lưu cạnh nhau trên đĩa.

Khi nào document thắng relational:

  • Dữ liệu có cấu trúc lồng nhau tự nhiên (blog post + comments, invoice + line items, user profile + preferences)
  • Pattern đọc ghi chủ yếu là "lấy hoặc cập nhật toàn bộ aggregate"
  • Schema của từng document có thể khác nhau (vd product catalog với attribute khác nhau tùy loại)

Khi nào document gặp khó:

Document model gặp khó khi có many-to-many relationship — vd một user viết nhiều comment trên nhiều bài viết. Để lấy tất cả comment của một user, phải scan toàn bộ collection và tìm trong array lồng nhau — không có cách index hiệu quả cho bài toán này theo kiểu relational.

4. Key-value model — tra cứu cực nhanh theo khoá

Key-value model là cấu trúc đơn giản nhất: một khoá (string) ánh xạ tới một giá trị (blob tuỳ ý). Không có schema, không có query ngôn ngữ phức tạp — chỉ có GET keySET key value.

GET  "session:user-42"  → {"userId": 42, "roles": ["admin"], "expires": 1735689600}
SET  "cache:post:891"   → "<html>...</html>"        (TTL: 300 giây)
GET  "rate:ip:1.2.3.4"  → 47                         (so lan request trong 60 giay)
INCR "rate:ip:1.2.3.4"  → 48

Điểm mạnh: O(1) lookup — tìm theo khoá là nhanh nhất có thể, không phụ thuộc kích thước dữ liệu. Thích hợp cho:

  • Session storage (khoá = session ID, giá trị = session data)
  • Cache (khoá = cache key, giá trị = HTML hoặc JSON đã render)
  • Rate limiting (khoá = IP hoặc user ID, giá trị = counter)
  • Feature flags (khoá = flag name, giá trị = true/false + rollout %)

Điểm yếu: không thể query theo nội dung giá trị. Muốn biết "tất cả session của user 42 đang active" thì không làm được nếu chỉ biết session ID từ trước — phải thiết kế khoá đủ khéo hoặc lưu index riêng.

5. Graph model — dữ liệu quan hệ chằng chịt

Graph model biểu diễn dữ liệu như một đồ thị: node (thực thể) và edge (quan hệ có hướng + thuộc tính). Thích hợp nhất khi số lượng loại quan hệ rất lớn và bản thân quan hệ cũng mang thông tin quan trọng.

(User: Alice) --[FOLLOWS]--> (User: Bob)
(User: Alice) --[LIKED]--> (Post: "Gioi thieu Docker")
(User: Bob) --[AUTHORED]--> (Post: "Gioi thieu Docker")
(Post: "Gioi thieu Docker") --[TAGGED_WITH]--> (Tag: "docker")
(User: Alice) --[WORKS_AT]--> (Company: "TechCorp")
(Company: "TechCorp") --[PARTNER_OF]--> (Company: "CloudVendor")

Câu hỏi tự nhiên với graph: "Tìm tất cả người Alice quen qua tối đa 3 bước" hay "Công ty nào là đối tác của đối tác của TechCorp" — với relational, mỗi "bước" cần thêm một JOIN, và số bước có thể không biết trước.

Graph thắng khi:

  • Mạng xã hội: follower/following, gợi ý bạn bè, phát hiện community
  • Đồ thị tri thức (knowledge graph): thực thể + quan hệ ngữ nghĩa
  • Fraud detection: phát hiện vòng lặp giao dịch, chuỗi tài khoản liên kết
  • Recommendation engine: "người mua X cũng mua Y" qua nhiều hop

6. Bảng so sánh 4 mô hình

Tiêu chíRelationalDocumentKey-ValueGraph
Hình dạng dữ liệuBảng phẳng, quan hệ qua khoá ngoạiCây lồng nhau (JSON/BSON)Cặp khoá-giá trị phẳngĐồ thị node + edge
Query patternSQL linh hoạt, JOIN nhiều bảngĐọc/ghi theo aggregate, ít JOINGET/SET theo khoá, O(1)Duyệt đồ thị qua các hop
Điểm mạnhQuery linh hoạt mọi chiều; ACID; chuẩn hoá tránh dư thừaData locality; schema linh hoạt; phù hợp dữ liệu câyTốc độ cực cao; đơn giản; TTL tự nhiênQuan hệ nhiều chiều; traversal nhiều hop
Điểm yếuMany-to-many nhiều JOIN; schema cứng; impedance mismatchMany-to-many query khó; consistency khi update nhiều documentKhông query theo nội dung; không có relational integrityKhông phù hợp dữ liệu bảng phẳng; query ngôn ngữ đặc thù
Use case điển hìnhERP, tài chính, e-commerce, hệ thống có audit trailBlog, CMS, catalog sản phẩm, event logCache, session, rate limiting, leaderboardMạng xã hội, fraud detection, recommendation
Agnostic — mô hình vs sản phẩm

Bài này nói về mô hình lý thuyết, không phải sản phẩm cụ thể. Nhiều hệ thống hiện đại hỗ trợ nhiều mô hình cùng lúc — ví dụ một số RDBMS đã thêm hỗ trợ JSON document và graph query. Việc chọn mô hình nào phụ thuộc vào bài toán, không phải hype của sản phẩm.

7. Hai khái niệm nền tảng — impedance mismatch và schema-on-read

7.1 Impedance mismatch

Impedance mismatch là khoảng cách giữa cấu trúc dữ liệu trong code ứng dụng (object, class, struct) và cấu trúc lưu trong database (bảng phẳng).

// Object trong code (Java, Python, TypeScript...)
User {
  id: 42,
  name: "Nguyen Van A",
  address: {                   // nested object
    city: "Ha Noi",
    street: "123 Le Duan"
  },
  hobbies: ["doc sach", "lap trinh"]  // array
}

// Relational: phai tach thanh nhieu bang
users(id, name)
addresses(user_id, city, street)
user_hobbies(user_id, hobby)

Mỗi lần đọc, phải JOIN 3 bảng và ghép lại thành object. Mỗi lần ghi, phải tách object ra 3 bảng. Đây là chi phí "dịch" giữa hai thế giới — ORM (Object-Relational Mapper) sinh ra chính để giảm chi phí này, nhưng không xoá được hoàn toàn.

Document model giảm impedance mismatch vì cấu trúc lưu trữ gần hơn với cấu trúc object trong code — lưu nguyên JSON, đọc ra là object. Nhưng đổi lại, mất query flexibility của relational.

7.2 Schema-on-write vs schema-on-read

Schema-on-write (relational): schema định nghĩa trước, database enforce khi ghi. Ghi sai format thì bị reject ngay.

-- Schema dinh nghia truoc, enforce tai thoi diem ghi
CREATE TABLE users (
  id   INT NOT NULL PRIMARY KEY,
  name TEXT NOT NULL,
  age  INT CHECK (age > 0)
);

INSERT INTO users (id, name, age) VALUES (1, 'Alice', -5);
-- ERROR: new row violates check constraint

Schema-on-read (document, key-value): không có schema cứng — bất kỳ format nào đều ghi được. Ứng dụng tự parse và validate khi đọc ra.

// Document collection khong co schema enforce:
{ "id": 1, "name": "Alice", "age": 30 }
{ "id": 2, "name": "Bob",   "email": "[email protected]" }
{ "id": 3, "name": "Carol"  }
// 3 document khac nhau cau truc -- deu hop le

Khi nào nào schema-on-write tốt hơn: dữ liệu có cấu trúc cố định, cần integrity mạnh, lỗi format phát hiện sớm, team lớn cần rõ ràng về kiểu dữ liệu.

Khi nào schema-on-read tốt hơn: schema thay đổi thường xuyên, dữ liệu đến từ nhiều nguồn khác nhau, cần flexibility để add field mới mà không cần migration.

Schema-on-read ≠ không cần nghĩ về schema

"Không có schema" là hiểu nhầm phổ biến. Schema-on-read có nghĩa là schema nằm trong code ứng dụng, không phải trong database. Thay vì database reject dữ liệu sai, ứng dụng phải xử lý mọi biến thể có thể. Khi schema thay đổi, cần update code đọc để handle cả format cũ và mới cùng lúc — đôi khi phức tạp hơn migration relational.

8. Cơ chế — cùng dữ liệu, 3 cách biểu diễn

Hãy xem cùng một tập dữ liệu — user, post, tag — được biểu diễn khác nhau trong 3 mô hình:

flowchart LR
  subgraph REL["Relational (bang phan tach)"]
    direction TB
    T1["users\n id=1, name=Alice"]
    T2["posts\n id=101, user_id=1"]
    T3["tags\n id=10, name=docker"]
    T4["post_tags\n post_id=101, tag_id=10"]
    T1 --- T2
    T2 --- T4
    T4 --- T3
  end

  subgraph DOC["Document (long nhau)"]
    direction TB
    D1["post doc\n{id:101\n author:{id:1, name:Alice}\n tags:[docker]\n comments:[...]}"]
  end

  subgraph GR["Graph (node + edge)"]
    direction LR
    N1["(Alice)"]
    N2["(Post:101)"]
    N3["(docker)"]
    N1 -->|"AUTHORED"| N2
    N2 -->|"TAGGED_WITH"| N3
  end

Relational tách tất cả ra bảng phẳng — query linh hoạt nhưng phải JOIN. Document gom lại theo use case đọc — đọc nhanh nhưng update một phần khó hơn (vd đổi tên tác giả: phải cập nhật trong mọi post document). Graph biểu diễn quan hệ như first-class entity — traversal tự nhiên, thêm loại quan hệ mới không ảnh hưởng data cũ.

9. Normalization vs aggregate locality

Relational ưu tiên normalization — mỗi thông tin lưu một chỗ duy nhất, không dư thừa. Tên tác giả chỉ nằm trong bảng users, không copy vào posts hay comments.

Document model ưu tiên aggregate locality — nhóm dữ liệu theo pattern đọc, chấp nhận một chút dư thừa để đổi lấy tốc độ đọc.

Normalization (relational):
  - "Nguyen Van A" luu 1 lan trong bang users
  - Moi post, moi comment chỉ luu user_id
  - Doi ten: UPDATE users SET name=... WHERE id=42 -> tu dong hieu luc moi noi

Aggregate locality (document):
  - "Nguyen Van A" co the duoc embed trong moi post, moi comment
  - Doc nhanh vi khong can JOIN
  - Doi ten: phai cap nhat tat ca document chua ten cu -> risk inconsistency

Khi nào normalization quan trọng: dữ liệu thay đổi thường xuyên (tên user, giá sản phẩm, trạng thái đơn hàng) — normalization đảm bảo cập nhật một chỗ, phản ánh mọi nơi.

Khi nào aggregate locality quan trọng: dữ liệu đọc nhiều lần hơn ghi, và nội dung không thay đổi sau khi tạo (event log, snapshot, lịch sử đơn hàng đã giao).

10. Pitfall — chọn mô hình theo hype thay vì bài toán

Pitfall — NoSQL không tự động nhanh hơn SQL

Câu hỏi "relational hay NoSQL?" đặt sai. Câu đúng là: "dữ liệu của mình có hình dạng gì và access pattern chính là gì?"

Dấu hiệu chọn sai mô hình:

  • Dùng document store cho dữ liệu nhiều many-to-many relationship → phải tự implement "JOIN" trong application code, chậm hơn và dễ sai hơn JOIN trong SQL optimizer.
  • Dùng relational cho dữ liệu cache session → thêm latency network + query parsing không cần thiết, trong khi key-value cho O(1) lookup.
  • Dùng relational cho graph traversal nhiều hop → mỗi hop thêm một JOIN, query ngày càng phức tạp, optimizer khó tối ưu.
  • Dùng document store vì "không cần schema" rồi viết validation code phức tạp trong app → schema vẫn tồn tại, chỉ chuyển gánh nặng từ database sang application code.
-- Document store: query "tat ca comment cua user-42 tren moi bai viet"
-- Khong co index phu hop -> full collection scan
-- Application phai loc tu mang comments long trong moi document
-- O(N) tren so document * kich thuoc mang comments

-- Relational: cung query tren schema da normalize
SELECT c.body, p.title
FROM comments c
JOIN posts p ON p.id = c.post_id
WHERE c.user_id = 42;
-- Index tren comments.user_id -> O(log N), optimizer chon join algorithm tot nhat

Hệ quả: nhiều dự án phải migrate schema sau khi production thấy rõ access pattern thật — việc này tốn kém hơn nhiều so với thiết kế đúng từ đầu. Đánh giá access pattern trước khi chọn mô hình.

11. Deep Dive

📚 Deep Dive — Nền tảng so sánh mô hình
  • Designing Data-Intensive Applications (Martin Kleppmann) — Chương 2 "Data Models and Query Languages" — nguồn nền tảng agnostic, không gắn engine cụ thể. Kleppmann so sánh relational vs document vs graph model qua lens: historical motivation, data locality, schema flexibility, many-to-many handling. Đọc mục "Relational Model vs Document Model" và "Graph-Like Data Models" (khoảng 25 trang) để có full picture.
  • Wikipedia — NoSQL — lịch sử thuật ngữ, phân loại các loại NoSQL (key-value, document, column-family, graph), lý do NoSQL xuất hiện (web scale, schema flexibility).
  • Wikipedia — Graph database — định nghĩa node/edge/property, so sánh với relational, use case điển hình (mạng xã hội, fraud detection, knowledge graph).

Ghi chú: DDIA Chương 2 là nguồn chuẩn nhất — Kleppmann giải thích tại sao không có mô hình nào "thắng tuyệt đối" và lý do tại sao relational vẫn dominant dù NoSQL phong trào đã có từ 2009. Wikipedia cho context lịch sử và fact check nhanh; không dùng để hiểu cơ chế sâu.

12. Liên hệ các bài khác

  • Bài 02 — Relational model: bài này đã dạy tại sao relational thắng hierarchical (IMS) năm 1970 — bài hiện tại zoom out thêm để thấy relational là một trong nhiều lựa chọn, mỗi cái có trade-off riêng.
  • Bài 03 — ER modeling: kỹ thuật thiết kế schema cho relational — áp dụng trực tiếp sau khi đã quyết định relational là mô hình phù hợp.
  • Module 5 — Thiết kế schema — Normalization 1NF→3NF: normalization là kỹ thuật cốt lõi của relational để tránh dư thừa — trade-off ngược lại với aggregate locality của document model.
  • Module 10 — OLTP vs OLAP: một góc nhìn khác về việc chọn đúng mô hình lưu trữ cho đúng workload — phân tích thêm về khi nào cần tách hệ thống.

13. Tóm tắt

  • Relational lưu dữ liệu phẳng, kết nối qua khoá ngoại — query SQL linh hoạt mọi chiều, ACID, chuẩn hoá tránh dư thừa. Thắng khi dữ liệu có nhiều loại quan hệ và cần query đa chiều.
  • Document lưu aggregate lồng nhau (JSON) — data locality tốt, đọc ghi cả khối không JOIN. Thắng khi dữ liệu có cấu trúc cây tự nhiên và access pattern chủ yếu là "đọc/ghi một document".
  • Key-value là map đơn giản khoá → giá trị — O(1) lookup, không schema, TTL tự nhiên. Thắng cho cache, session, rate limiting, leaderboard.
  • Graph lưu node + edge — traversal nhiều hop tự nhiên. Thắng khi quan hệ giữa các thực thể chính là dữ liệu (mạng xã hội, fraud detection, knowledge graph).
  • Impedance mismatch là khoảng cách giữa object trong code và bảng phẳng trong relational — document model giảm mismatch nhưng đánh đổi query flexibility.
  • Schema-on-write (relational): database enforce khi ghi, lỗi phát hiện sớm. Schema-on-read (document/KV): ứng dụng tự validate khi đọc, linh hoạt hơn nhưng gánh nặng chuyển sang code.
  • Normalization (relational) vs aggregate locality (document): normalization tốt khi dữ liệu thay đổi thường; locality tốt khi đọc nhiều, ghi ít, nội dung ổn định.
  • Relational là trọng tâm của khoá học này — hiểu landscape giúp bạn thấy relational đứng ở đâu và khi nào nó là lựa chọn đúng đắn.

14. Tự kiểm tra

Tự kiểm tra
Q1
Bạn cần build hệ thống lưu lịch sử đơn hàng: mỗi đơn gồm thông tin khách hàng, địa chỉ giao hàng tại thời điểm đặt, và danh sách sản phẩm với giá tại thời điểm đặt. Thông tin này không thay đổi sau khi đặt. Mô hình nào phù hợp hơn và vì sao?

Document model phù hợp hơn trong trường hợp này vì hai lý do. Thứ nhất, dữ liệu đơn hàng có cấu trúc aggregate tự nhiên: một đơn hàng là một khối thông tin hoàn chỉnh (khách hàng + địa chỉ + sản phẩm) — đọc ra là đủ, không cần JOIN thêm bảng nào. Thứ hai, dữ liệu không thay đổi sau khi tạo — đây là điểm then chốt. Nếu dùng relational và normalize, tên khách hàng và giá sản phẩm chỉ lưu một chỗ; nhưng nếu giá thay đổi sau, query lịch sử đơn hàng sẽ trả về giá mới thay vì giá cũ — sai nghiệp vụ.

Embedding thông tin vào document đơn hàng (snapshot giá + địa chỉ tại thời điểm đặt) là lựa chọn đúng: aggregate locality tốt cho read-heavy use case, và tính bất biến của đơn hàng đã giao loại bỏ rủi ro inconsistency khi embed.

Q2
Phân biệt impedance mismatch và cách document model giải quyết nó — nhưng đổi lại điều gì?

Impedance mismatch là khoảng cách giữa cấu trúc object trong code ứng dụng (lồng nhau, có array, có nested object) và cấu trúc bảng phẳng trong relational database. Mỗi lần đọc phải JOIN nhiều bảng rồi ghép lại; mỗi lần ghi phải tách object ra nhiều bảng — chi phí "dịch" này tốn code, tốn thời gian, và ORM chỉ giảm chứ không xoá được.

Document model giải quyết bằng cách lưu dữ liệu theo cấu trúc gần với object trong code — đọc ra là JSON, parse thẳng thành object, không JOIN. Impedance giảm rõ rệt.

Đổi lại: mất query flexibility. Câu hỏi dọc theo nhiều aggregate (vd "tất cả comment của user X trên mọi post") không có cách query hiệu quả trong document model vì không có JOIN — phải scan toàn collection hoặc thiết kế lại cấu trúc document cho từng access pattern riêng.

Q3
Schema-on-read có nghĩa là 'không cần schema' không? Giải thích hiểu nhầm phổ biến này và hệ quả thực tế.

Không. Schema-on-read không có nghĩa là không cần schema — nó có nghĩa là schema nằm trong code ứng dụng thay vì trong database. Database không enforce format khi ghi; nhưng khi đọc ra, code phải parse và validate dữ liệu theo schema mà nó kỳ vọng.

Hệ quả thực tế của hiểu nhầm này: team dùng document store vì "linh hoạt không cần schema", rồi viết code validation phức tạp ở mọi nơi đọc dữ liệu. Khi schema thay đổi (thêm field, đổi kiểu), phải cập nhật code để handle cả format cũ còn trong database và format mới — migration ngầm, không rõ ràng, dễ sót.

So với relational schema-on-write: lỗi format bị reject ngay tại database, migration có thể tự động hóa với DDL, toàn bộ team thấy schema rõ ràng qua DESCRIBE table hoặc \d tablename. Trade-off là ít linh hoạt hơn khi schema thay đổi thường xuyên trong giai đoạn đầu phát triển.

Q4
Khi nào nhiều mô hình dữ liệu có thể coexist trong cùng một hệ thống? Cho ví dụ cụ thể với use case thực tế.

Rất thường xuyên — các hệ thống production lớn thường dùng nhiều mô hình song song, mỗi mô hình cho một loại dữ liệu khác nhau. Lý do: không có mô hình nào tối ưu cho mọi bài toán.

Ví dụ hệ thống e-commerce:

  • Relational cho catalog sản phẩm, đơn hàng, user account — cần ACID, query linh hoạt, audit trail đầy đủ.
  • Key-value cho session người dùng đang đăng nhập và cache trang sản phẩm — cần O(1) lookup, TTL tự động, không cần query phức tạp.
  • Document cho lịch sử đơn hàng đã giao — snapshot immutable, đọc theo đơn hàng là chủ yếu, không cần JOIN.
  • Graph cho recommendation engine — "người mua X cũng mua Y" qua nhiều hop quan hệ.

Mỗi lớp được chọn theo access pattern và tính chất dữ liệu, không phải theo hype của công nghệ.

Q5
Vì sao graph model thắng relational khi cần traversal nhiều hop? Ví dụ tìm 'bạn của bạn' cách 3 bước.

Với relational, mỗi "hop" trong graph traversal cần một JOIN. Tìm bạn trực tiếp (1 hop): 1 JOIN vào bảng friendships. Bạn của bạn (2 hop): 2 JOIN. Bạn của bạn của bạn (3 hop): 3 JOIN self-join vào cùng bảng. Số bước không biết trước (depth variable) thì không viết được SQL cố định — phải dùng recursive CTE, vốn tốn kém và khó tối ưu khi graph lớn.

Graph model lưu edge như first-class entity với pointer trực tiếp từ node sang node — traversal 1 hop chỉ là "follow the pointer". 3 hop là 3 lần follow pointer, độc lập với kích thước graph. Database graph index được tối ưu cho pattern này, tốc độ không giảm theo số node như JOIN trong relational.

Thêm nữa: trong relational, khi thêm loại quan hệ mới (vd COLLEAGUE, CLASSMATE bên cạnh FRIEND), phải thêm bảng hoặc thêm cột. Trong graph, thêm loại edge mới không ảnh hưởng data cũ — schema mở rộng tự nhiên hơn.

Q6
Bạn đang build hệ thống rate limiting — giới hạn mỗi IP không quá 100 request mỗi phút. Mô hình nào phù hợp nhất và tại sao relational lại không phải lựa chọn tốt ở đây?

Key-value model là lựa chọn tốt nhất. Với key-value: khoá là IP address, giá trị là counter; mỗi request chỉ cần INCR (tăng counter) và GET để kiểm tra giới hạn — O(1), không cần query phức tạp. TTL tự động reset counter sau 60 giây mà không cần cron job hay cleanup process. Throughput rất cao: có thể xử lý hàng triệu operation/giây trên single instance.

Tại sao relational không phù hợp ở đây:

  • Latency: mỗi request cần query database (SELECT count + UPDATE) — thêm ít nhất vài millisecond network roundtrip + query parse + lock. Với rate limiting, mọi request đều phải check → bottleneck nghiêm trọng.
  • Write contention: nhiều request đến cùng lúc cùng IP sẽ contend lock trên cùng một row — throughput thấp hơn nhiều so với atomic INCR trong key-value store.
  • TTL: relational không có TTL native — phải tự viết cleanup job để xoá record cũ, thêm complexity.

Đây là ví dụ điển hình: biết mô hình nào phù hợp = tiết kiệm đáng kể về latency, complexity và chi phí vận hành.

Q7
Normalization và aggregate locality là hai triết lý đối lập. Khi nào normalization quan trọng hơn, và khi nào locality quan trọng hơn? Cho một ví dụ cụ thể cho mỗi trường hợp.

Normalization quan trọng hơn khi dữ liệu thay đổi thường xuyên và cần cập nhật đồng bộ ở mọi nơi. Ví dụ: giá sản phẩm trong catalog e-commerce — nếu embed giá vào mọi document liên quan (giỏ hàng, wishlist, recommendation), khi giá thay đổi phải cập nhật hàng triệu document. Normalization: giá lưu một chỗ trong bảng products, cập nhật 1 row là phản ánh mọi nơi ngay lập tức, đảm bảo consistency tuyệt đối.

Aggregate locality quan trọng hơn khi dữ liệu bất biến sau khi tạo và access pattern là đọc theo đơn vị aggregate. Ví dụ: hóa đơn điện tử đã phát hành — giá, tên sản phẩm, địa chỉ phải là snapshot tại thời điểm phát hành, không được thay đổi dù thông tin gốc có thay đổi sau. Embed vào document hóa đơn là đúng nghiệp vụ, đọc ra là đủ, không JOIN.

Câu hỏi thực tế khi thiết kế: "Nếu thông tin gốc thay đổi, data này có nên phản ánh thay đổi đó không?" Nếu có → normalize. Nếu không (snapshot) → embed.

Bài tiếp theo: Bài 05 — SQL flavor map — chuẩn ANSI và dialect từng vendor

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên