SQL & Database — Tư tưởng & Nguyên lý
3/52
Bài 3 / 52~20 phútNền tảng & mô hình dữ liệuLộ trình · chặng 10/21Miễn phí lượt xem

ER modeling — bản vẽ trước khi gõ DDL

Entity, attribute, relationship, cardinality. Vẽ ER diagram TaskFlow rồi map sang SQL DDL trong 5 phút. Bỏ qua bước này = data model rách sau 6 tháng.

TL;DR: ER modeling là bước thiết kế trước khi gõ DDL — vẽ entity và relationship trên giấy hoặc Mermaid, review logic, rồi mới sinh SQL. 4 khái niệm cốt lõi: entity (danh từ có ID riêng), attribute (4 loại: simple/composite/multi-valued/derived), relationship (cạnh nối entity), cardinality (1:1/1:N/M:N). Mapping rules rõ ràng: entity → CREATE TABLE, 1:N → FK column ở "many" side, M:N → junction table với composite PK, weak entity → ON DELETE CASCADE. Thay đổi trên diagram miễn phí; thay đổi sau khi có 50M row production rất tốn kém.

Bạn được giao "thiết kế DB cho TaskFlow". Mở DBeaver, gõ CREATE TABLE users ( — rồi đứng. Ai assign task cho ai? Task có nhiều assignee được không? Comment có thể nested không? Mỗi câu hỏi này là một quyết định thiết kế, không phải quyết định DDL. Gõ DDL trước khi trả lời những câu hỏi đó nghĩa là bạn đang xây nhà trước khi có bản vẽ — mọi thứ có vẻ nhanh lúc đầu, nhưng thay đổi sau đó rất tốn kém.

ER modeling (Entity-Relationship modeling) là bước trước khi gõ DDL — vẽ entity và relationship trên giấy hoặc Mermaid trước, gõ DDL sau. Bài này giải thích 4 khái niệm cốt lõi (entity, attribute, relationship, cardinality) qua TaskFlow schema và cách map ER diagram thành SQL DDL.

1. Analogy — Bản vẽ kiến trúc trước khi xây nhà

Kiến trúc sư vẽ blueprint trước khi thợ xây đổ móng. Blueprint không phải là nhà — nó là bản đặc tả các phòng, cửa nối, và cột chịu lực. Chỉ khi blueprint được review và chỉnh sửa trên giấy (miễn phí), thợ xây mới bắt đầu. Thay đổi cột chịu lực trên giấy tốn 5 phút. Thay đổi cột chịu lực sau khi đã đổ bê tông tốn hàng tuần và rất tốn kém.

ER diagram là blueprint của database — bạn vẽ entity và relationship trước, review logic, rồi mới sinh DDL.

Bản vẽ kiến trúcER diagram
Phòng (phòng ngủ, bếp, phòng khách)Entity (users, tasks, projects)
Cửa nối giữa hai phòngRelationship (users — tasks)
Quy tắc sức chứa phòngCardinality (1:N, M:N)
Cột chịu lực — không thể thiếuPrimary key
Kích thước và thuộc tính của phòngAttribute (name, email, status)
Bản vẽ không phải nhà thậtER diagram không phải DDL thật
Phải có blueprint trước khi xâyPhải vẽ ER trước khi gõ DDL
Khác bản vẽ: blueprint chỉ là tài liệuER diagram có thể export DDL tự động (dbdiagram.io, DBeaver reverse engineering)
💡 Cách nhớ

ER diagram là bản vẽ — thay đổi trên giấy miễn phí, thay đổi sau khi migrate 50M row rất tốn kém. Vẽ trước, gõ DDL sau.

2. Entity — danh từ cụ thể có ID riêng

Entity là bất kỳ "vật thể" hoặc "khái niệm" nào trong domain mà bạn cần lưu thông tin và có thể xác định duy nhất bằng một ID. Câu kiểm tra nhanh: nếu bạn cần đặt câu "tìm thing theo ID" thì thing có thể là entity.

Trong TaskFlow:

  • users — entity vì mỗi user có ID riêng, tồn tại độc lập
  • projects — entity vì mỗi project có ID riêng, tồn tại độc lập
  • tasks — entity vì mỗi task có ID riêng, tồn tại độc lập

Strong entity vs weak entity:

Strong entity tự đứng được — tồn tại không phụ thuộc entity khác. usersprojects là strong entity: xóa một project không làm user biến mất.

Weak entity phụ thuộc vào parent entity để tồn tại. comments trong TaskFlow là weak entity: comment tồn tại vì gắn với một task cụ thể. Xóa task, comment mất ý nghĩa — thậm chí nên xóa luôn (CASCADE DELETE).

SQL
-- Strong entity: ton tai doc lap
CREATE TABLE tasks (
  id         INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- khoa tu tang (IDENTITY chuan SQL; AUTO_INCREMENT/SERIAL tuy engine)
  title      TEXT NOT NULL,
  project_id INT  NOT NULL REFERENCES projects(id)
);

-- Weak entity: phu thuoc task, xoa task -> xoa comment theo
CREATE TABLE comments (
  id         INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  task_id    INT  NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
  body       TEXT NOT NULL,
  author_id  INT  NOT NULL REFERENCES users(id)
);

Thử ngẫmtrong hệ thống bạn đang làm, entity nào là weak entity nhưng đang thiếu ON DELETE CASCADE? Record cha bị xoá thì bảng con đó rơi vào trạng thái gì?

3. Attribute — thuộc tính của entity

Attribute là thông tin mô tả entity. Mỗi entity có một tập attribute, mỗi attribute có tên và domain (kiểu dữ liệu). Có 4 loại attribute cần phân biệt:

Simple attribute — một giá trị atomic, không thể chia nhỏ hơn. Phần lớn attribute là loại này: email, name, status, created_at. Mỗi cái map thẳng thành 1 column.

Composite attribute — một giá trị phức hợp gồm nhiều sub-field. address là composite: gồm street, city, zip_code. Trong relational model, composite attribute thường được flatten thành nhiều column riêng:

SQL
-- Flatten composite attribute "address" thanh 3 column rieng
CREATE TABLE user_profiles (
  user_id  INT  PRIMARY KEY REFERENCES users(id),
  street   TEXT,
  city     TEXT,
  zip_code TEXT
);

Multi-valued attribute — một entity có nhiều giá trị cho cùng một attribute. Ví dụ: một task có nhiều tags. Trong relational model, multi-valued attribute vi phạm atomic value (1NF — sẽ học chi tiết trong Module 4 của khoá này), nên phải tách thành bảng riêng:

SQL
-- WRONG: luu tags nhu comma-separated string
-- vi pham 1NF, khong index duoc, khong query hieu qua
CREATE TABLE tasks (
  id    SERIAL PRIMARY KEY,
  title TEXT NOT NULL,
  tags  TEXT  -- 'bug,urgent,backend'
);

-- RIGHT: tach thanh bang rieng
CREATE TABLE task_tags (
  task_id INT  NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
  tag     TEXT NOT NULL,
  PRIMARY KEY (task_id, tag)
);

Derived attribute — giá trị có thể tính từ attribute khác, không cần lưu. age tính được từ birth_date và ngày hiện tại. task_count của một project tính được từ COUNT trên bảng tasks. Trong relational model, derived attribute thường không lưu — tính lúc query:

SQL
-- Derived attribute: tinh luc query, khong luu column rieng
-- (cu phap tinh tuoi khac nhau tuy engine; y tuong chung: tinh tu birth_date)
SELECT
  u.id,
  u.name,
  EXTRACT(YEAR FROM CURRENT_DATE) - EXTRACT(YEAR FROM u.birth_date) AS age_approx
FROM users u;

4. Relationship — cạnh nối entity

Relationship mô tả liên kết giữa hai entity. Trong ER diagram, relationship là cạnh nối hai entity node. Trong SQL, relationship được hiện thực bằng foreign key — hoặc junction table khi cần.

Có 3 loại cardinality:

1:1 (một — một) — mỗi instance của entity A liên kết với tối đa một instance của entity B, và ngược lại. Loại này hiếm trong production vì thường có thể gộp vào cùng một bảng. Ví dụ hợp lý: users ↔ user_profiles khi muốn tách dữ liệu nhạy cảm (bio, avatar URL) ra bảng riêng để kiểm soát truy cập.

SQL
-- 1:1 via unique foreign key
CREATE TABLE user_profiles (
  user_id   INT  PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
  bio       TEXT,
  avatar_url TEXT
);
-- user_id vua la PK, vua la FK -> dam bao 1:1

1:N (một — nhiều) — loại phổ biến nhất. Một instance của entity A liên kết với nhiều instance của entity B. Ví dụ: một project có nhiều tasks, nhưng mỗi task chỉ thuộc về một project. Hiện thực bằng FK column ở phía "nhiều":

SQL
-- 1:N: project (1) -> tasks (N)
-- FK project_id dat tren bang "nhieu" (tasks)
CREATE TABLE tasks (
  id         SERIAL PRIMARY KEY,
  title      TEXT NOT NULL,
  project_id INT  NOT NULL REFERENCES projects(id) ON DELETE CASCADE
);

M:N (nhiều — nhiều) — mỗi instance của A liên kết với nhiều instance của B, và ngược lại. Ví dụ: một user có thể là member của nhiều projects, và một project có nhiều users. Loại này bắt buộc phải có junction table (bảng trung gian) — và lý do "bắt buộc" nằm ở chỗ một cột chỉ chứa được một giá trị:

Luật nghiệp vụ một user tham gia nhiều project và một project có nhiều user, hai lần thử thất bại là thêm cột users.project_id hoặc thêm cột projects.member_id vì mỗi hàng chỉ giữ được một giá trị nên bên mang cột bị bó về 1, cách đúng là bảng project_members ở giữa với PRIMARY KEY gồm user_id và project_id, hai cạnh 1 N nối sang users và projects

SQL
-- M:N: users <-> projects qua junction table project_members
CREATE TABLE project_members (
  user_id    INT  NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  project_id INT  NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
  role       TEXT NOT NULL DEFAULT 'member',
  joined_at  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- TIMESTAMPTZ / DATETIME tuy engine
  PRIMARY KEY (user_id, project_id)  -- composite PK dam bao unique
);

Thử ngẫmtask_watchers: một user theo dõi được nhiều task và ngược lại. Quan hệ đó là 1:N hay M:N, và bạn cần thêm bảng nào để hiện thực đúng?

5. ER diagram TaskFlow — bức tranh đầy đủ

Kết hợp cả 5 entity và các relationship, bản đồ quan hệ của TaskFlow trông như sau. Mỗi mũi tên đi từ bảng chứa cột FK sang bảng nó trỏ tới, và nhãn trên mũi tên ghi luôn cột đó có NOT NULL hay không:

Bản đồ quan hệ TaskFlow: projects trỏ về users bằng owner_id NOT NULL, project_members trỏ về cả users và projects, tasks trỏ về projects bằng project_id NOT NULL và trỏ về users bằng assignee_id cho phép NULL, comments trỏ về tasks bằng task_id NOT NULL và về users bằng user_id NOT NULL

Participation — vì sao NOT NULL cũng là thông tin của ER

Cardinality (1:1, 1:N, M:N) trả lời "bao nhiêu"; NOT NULL trả lời một câu khác — có bắt buộc tham gia quan hệ hay không. Lý thuyết ER gọi đây là total participation (mọi task phải thuộc một project) và partial participation (task được phép chưa có assignee). Hai trục độc lập: cùng 1:N mà một cái bắt buộc, một cái không. Công cụ ER (DBeaver, dbdiagram.io) vẽ khác biệt đó bằng vòng tròn ở đầu cạnh — "không hoặc nhiều".

Giải thích từng cạnh:

  • projects.owner_id → users — 1 user tạo/sở hữu nhiều project (1:N, bắt buộc)
  • project_members trỏ về cả usersprojects — M:N qua junction table
  • tasks.project_id → projects — 1 project chứa nhiều task (1:N, bắt buộc)
  • tasks.assignee_id → users — 1 user được assign nhiều task (1:N, tuỳ chọn: task chưa gán ai vẫn hợp lệ)
  • comments.task_id → tasks — 1 task có nhiều comment (1:N, weak entity)
  • comments.user_id → users — 1 user viết nhiều comment (1:N, bắt buộc)

6. ER → SQL DDL trong 5 phút

Từ ER diagram, apply 4 mapping rules để sinh DDL:

ER elementSQL mapping
Entity (strong)CREATE TABLE với PRIMARY KEY
Entity (weak)CREATE TABLE với FK + ON DELETE CASCADE
Simple/composite attributeColumn với type; composite → flatten
1:N relationshipFK column ở "many" side
M:N relationshipJunction table với composite PK
Multi-valued attributeBảng riêng với FK

Áp dụng cho TaskFlow — full DDL từ ER diagram Section 5:

SQL
-- Strong entity: users
CREATE TABLE users (
  id         INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- IDENTITY chuan SQL; AUTO_INCREMENT/SERIAL tuy engine
  email      TEXT NOT NULL UNIQUE,
  name       TEXT NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- Strong entity: projects
CREATE TABLE projects (
  id          INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name        TEXT NOT NULL,
  description TEXT,
  owner_id    INT  NOT NULL REFERENCES users(id),
  created_at  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- Junction table: M:N between users and projects
CREATE TABLE project_members (
  user_id    INT  NOT NULL REFERENCES users(id)    ON DELETE CASCADE,
  project_id INT  NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
  role       TEXT NOT NULL DEFAULT 'member',
  joined_at  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (user_id, project_id)
);

-- Strong entity: tasks (1:N from projects, 1:N from users)
CREATE TABLE tasks (
  id          INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  title       TEXT NOT NULL,
  status      TEXT NOT NULL DEFAULT 'todo',
  project_id  INT  NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
  assignee_id INT       REFERENCES users(id) ON DELETE SET NULL,
  created_at  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- Weak entity: comments (ON DELETE CASCADE theo task)
CREATE TABLE comments (
  id         INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  body       TEXT NOT NULL,
  task_id    INT  NOT NULL REFERENCES tasks(id)    ON DELETE CASCADE,
  author_id  INT           REFERENCES users(id)    ON DELETE SET NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

Mỗi dòng DDL có thể trace ngược về một quyết định trong ER diagram. Đây là lý do vẽ ER trước: khi đồng nghiệp hỏi "sao project_members lại cần?", bạn chỉ cần chỉ vào cạnh M:N trong ER diagram, không cần giải thích từ đầu.

7. Pitfall — 3 lỗi phổ biến

Lỗi 1 — Quên junction table cho M:N: lưu member_ids như array column trên bảng projects.

SQL
-- WRONG: luu array user_id tren projects
-- query "user A thuoc project nao?" phai scan + parse array
CREATE TABLE projects (
  id         SERIAL PRIMARY KEY,
  name       TEXT NOT NULL,
  member_ids INT[]  -- WRONG: vi pham 1NF, khong index FK duoc
);

-- RIGHT: junction table
CREATE TABLE project_members (
  user_id    INT NOT NULL REFERENCES users(id),
  project_id INT NOT NULL REFERENCES projects(id),
  PRIMARY KEY (user_id, project_id)
);
Pitfall — array column cho M:N: query ngắn, maintain dài

Lưu member_ids INT[] trông gọn lúc viết DDL, nhưng không thể tạo FK constraint trên array element, không thể index để JOIN hiệu quả, và query "tìm tất cả user trong project X" phải unnest array — chậm hơn JOIN với junction table khi data lớn. Khi cần thêm attribute vào relationship (vd role, joined_at), array hoàn toàn không đủ.

Lỗi 2 — Nhầm 1:N với M:N dẫn đến over-normalize: thêm junction table khi quan hệ thực tế là 1:N. Ví dụ: nếu một task chỉ có đúng một assignee, tạo task_assignees junction table là thừa — chỉ cần assignee_id FK trên bảng tasks. Junction table chỉ cần thiết khi thực sự có M:N.

Lỗi 3 — Quên CASCADE cho weak entity: xóa task nhưng không CASCADE xóa comment → comment trở thành orphan row (FK violation nếu có constraint, hoặc dead row nếu không có constraint).

SQL
-- WRONG: thieu ON DELETE CASCADE cho weak entity
CREATE TABLE comments (
  task_id INT NOT NULL REFERENCES tasks(id)  -- thieu CASCADE
);
-- Xoa task -> comment van con, FK violation hoac dead data

-- RIGHT
CREATE TABLE comments (
  task_id INT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE
);

8. Applied — vẽ ER trước cứu 6 tháng debt

Một team backend bỏ qua bước ER, gõ DDL thẳng sau buổi kickoff 30 phút. Schema đầu tiên: projects(id, name, member_ids TEXT) — lưu user ID dưới dạng comma-separated string. Hệ thống chạy được với vài trăm user.

Sau 6 tháng, product yêu cầu thêm role cho từng member trong project (admin/member/viewer). Không thể thêm vào column member_ids TEXT được — phải migrate toàn bộ data sang junction table. Với hàng chục triệu row và zero downtime requirement, migration đó mất nhiều tuần lên kế hoạch và gây downtime đáng kể trong quá trình thực thi.

Team B trong cùng công ty dành 30 phút vẽ ER diagram trước, phát hiện M:N ngay trong buổi review. Junction table project_members có trong schema từ ngày đầu. Khi product yêu cầu thêm role, chỉ cần ALTER TABLE project_members ADD COLUMN role TEXT — zero downtime, xong trong 5 phút.

Tools vẽ ER: dbdiagram.io (free, export SQL), DrawIO (free, offline), Mermaid trong README/MDX (giống bài này — diagram sống cùng code).

Bonus: DBeaver có tính năng ER reverse engineering — kết nối database đang chạy và tự sinh ER diagram. Hữu ích khi tiếp nhận codebase cũ không có documentation.

9. Deep Dive — ER modeling foundations

📚 Deep Dive — ER modeling foundations
  • Database System Concepts — Ch.6 "Database Design Using the E-R Model" (Silberschatz/Korth/Sudarshan, 7th ed) — treatment formal với cardinality notation đầy đủ, weak entity, participation constraint, và extended ER (EER). Đây là nguồn học ER modeling có hệ thống nhất. Đọc 6.1–6.4 để có nền tảng, 6.5–6.6 khi cần EER cho domain phức tạp.
  • Chen 1976 — "The Entity-Relationship Model: Toward a Unified View of Data" (bản rút gọn ~5 trang) — paper gốc của Peter Chen, link này là bản rút gọn miễn phí (paper đầy đủ trên ACM TODS dài hơn). Notation gốc của Chen khác crow's foot hiện đại, nhưng paper này cho thấy intuition ban đầu: tại sao cần tách entity khỏi relationship, tại sao cardinality là concept cốt lõi. Đọc sau DSC Ch.6 để thấy ý tưởng gốc từ nguồn.
  • Mermaid ER diagram syntax docs — reference thực hành khi vẽ ER trong README hoặc MDX. Mermaid dùng crow's foot notation, render trong GitHub, GitLab, và Notion.

Ghi chú: Đọc DSC Ch.6 đầu tiên để có nền tảng có hệ thống. Chen 1976 đọc sau để hiểu lịch sử và intuition gốc. Mermaid docs mở khi cần tra cứu syntax.

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

  • Bài 02 — Relational model: 6 thuộc tính của relation (đặc biệt atomic value và tuple uniqueness) là lý do tại sao ER modeling cần junction table cho M:N và PRIMARY KEY cho mọi entity.
  • Bài 04 — Tour TaskFlow schema: đọc DDL chi tiết của schema TaskFlow được vẽ ở bài này — hiểu vì sao từng quyết định kiểu dữ liệu và constraint được chọn.
  • Module 5 — Thiết kế schema: normalization (1NF/2NF/3NF) là hệ quả trực tiếp của multi-valued attribute và composite attribute — ER modeling là bước trước khi normalize.
  • Module 6 — Storage & indexing: PRIMARY KEY trong ER diagram tạo ra clustered index — ảnh hưởng trực tiếp đến query performance.

11. Tóm tắt

  • ER modeling là bước trước DDL — vẽ entity và relationship trước, sinh DDL sau. Thay đổi trên diagram miễn phí; thay đổi sau khi có data production rất tốn kém.
  • Entity = danh từ trong domain có ID riêng, tồn tại độc lập (strong) hoặc phụ thuộc parent (weak entity → CASCADE DELETE).
  • 4 loại attribute: simple (1 giá trị atomic), composite (nhiều sub-field → flatten), multi-valued (nhiều giá trị → tách bảng riêng, vi phạm 1NF), derived (tính lúc query, không lưu).
  • 3 cardinality: 1:1 (hiếm, dùng khi tách concern), 1:N (phổ biến nhất, FK ở "many" side), M:N (bắt buộc junction table).
  • M:N cần junction table — không dùng array column hay comma-separated string; junction table cho phép thêm attribute vào relationship và JOIN hiệu quả.
  • Mapping rules ER → DDL: entity → CREATE TABLE, 1:N → FK column, M:N → junction table với composite PK, weak entity → ON DELETE CASCADE.
  • Multi-valued attribute vi phạm 1NF — sẽ giải thích chi tiết trong Module 4 (Normalization) của khoá này.

12. Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Vì sao quan hệ 1:1 hiếm trong production schema? Khi nào nó vẫn hợp lý và không nên gộp vào một bảng?
  2. Q2
    Phân biệt strong entity và weak entity. Cho thêm một ví dụ trong TaskFlow ngoài comments — entity đó là strong hay weak, và vì sao?
  3. Q3
    Bạn có entity Order với attribute items là array sản phẩm. Vi phạm gì? Refactor như thế nào để đúng relational model?
  4. Q4
    Khi nào nên flatten composite attribute (split address → street, city, zip_code thành 3 column) và khi nào giữ nguyên trong 1 column TEXT là hợp lý?
  5. Q5
    Vẽ ER diagram (mô tả bằng chữ hoặc Mermaid) cho hệ thống "user follow user khác" kiểu Twitter. Cardinality gì? Cần junction table không?

Bài tiếp theo: Bài 04 — Tour TaskFlow schema — đọc DDL hiểu vì sao

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

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

Bài tiếp theo

Tour TaskFlow schema — đọc DDL hiểu vì sao chọn từng quyết định