SQL & Database — Tư tưởng & Nguyên lý/Constraints — DB là last line of defense, không trust app validation
30/51
Bài 30 / 51~18 phútSchema designMiễn phí lượt xem

Constraints — DB là last line of defense, không trust app validation

NOT NULL, UNIQUE, PK, FK, CHECK chuẩn SQL. Deferred constraint, cascading. Vì sao app-level validate race-prone còn DB-level enforce safe — nguyên lý agnostic.

TL;DR: Constraint là last line of defense cho integrity — không thể trust app-level validation vì khoảng giữa SELECT (check) và INSERT (write) không được lock, hai request đồng thời đều pass rồi đều ghi. 5 constraint chuẩn SQL: NOT NULL, UNIQUE, PRIMARY KEY, FOREIGN KEY, CHECK — mỗi loại enforce một tầng integrity tại write time, atomic. FK quyết định cascading behavior (CASCADE/SET NULL/RESTRICT). Deferred constraint dời check tới cuối transaction (giải circular FK). Pitfall: app validate không thay được DB constraint; CHECK chỉ cho immutable per-row predicate.

App đã validate email duplicate trước khi insert. Chạy 6 tháng ổn. Black Friday traffic spike — 2 request cùng email pass validate trong cùng millisecond, cả hai insert thành công, cả hai row tồn tại trong database. Bug được phát hiện 3 tháng sau khi support team báo "user X có 2 account".

Đây là race condition kinh điển của app-level validation: khoảng thời gian giữa SELECT (check) và INSERT (write) không được bảo vệ. Constraint là last line of defense — không thể trust app validation cho integrity. Bài này map 5 loại constraint cốt lõi chuẩn SQL, generalized UNIQUE cho non-overlap, deferred constraint cho circular FK, cascading behavior, và 2 pitfall phổ biến. Tất cả là nguyên lý agnostic — đúng cho mọi RDBMS.

1. Analogy — Constraint là khoá vault

Hãy hình dung app validation là bảo vệ đứng trước cổng tòa nhà. Database constraint là khoá vault bên trong. Bảo vệ có thể nhầm — hai người cùng xuất trình giấy tờ đúng trong cùng lúc, cả hai đều được thả vào. Khoá vault không nhầm — chỉ một người mở được, người thứ hai nhận lỗi.

Đời thườngDatabase concept
Bảo vệ kiểm tra giấy tờ trước cổngApp-level validation (có thể pass đồng thời)
Khoá vault — chỉ mở được 1 lầnDB constraint — enforce tại write time, atomic
Quy định "ô trống phải điền" trong formNOT NULL constraint
Mã số nhân viên duy nhất toàn công tyUNIQUE constraint
Số thẻ nhân viên là định danh chính thứcPRIMARY KEY
Phòng ban phải tồn tại trước khi assign nhân viênFOREIGN KEY
Tuổi nhân viên phải trong khoảng hợp lệCHECK constraint
💡 Cách nhớ

App validate = guard có thể nhầm. Constraint = vault không thể nhầm. Luôn cần cả hai, nhưng integrity đến từ constraint — guard chỉ để UX tốt hơn.

Race condition khi chỉ dựa vào app validation (hai request đồng thời cùng email):

sequenceDiagram
  participant R1 as Request 1
  participant R2 as Request 2
  participant DB as Database
  R1->>DB: SELECT email = '[email protected]'
  DB-->>R1: khong ton tai
  R2->>DB: SELECT email = '[email protected]'
  DB-->>R2: khong ton tai
  R1->>DB: INSERT '[email protected]'
  DB-->>R1: OK
  R2->>DB: INSERT '[email protected]'
  Note over DB: Khong co UNIQUE -> ca hai row ton tai (BUG)<br/>Co UNIQUE -> reject request 2 (SAFE)

2. 5 loại constraint cốt lõi

-- 1. NOT NULL: column khong duoc NULL
email VARCHAR(254) NOT NULL

-- 2. UNIQUE: khong duplicate value trong column
email VARCHAR(254) UNIQUE NOT NULL
-- Multi-column UNIQUE: cap (user_id, project_id) phai duy nhat
UNIQUE (user_id, project_id)

-- 3. PRIMARY KEY: NOT NULL + UNIQUE + tao index co thu tu mac dinh
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY
-- Composite PK cho junction table M:N
PRIMARY KEY (user_id, project_id)

-- 4. FOREIGN KEY: tham chieu den PK bang khac
project_id BIGINT NOT NULL REFERENCES projects(id)
-- Voi cascading (giai thich o Section 5)
project_id BIGINT NOT NULL REFERENCES projects(id) ON DELETE CASCADE

-- 5. CHECK: dieu kien bat buoc per-row
status VARCHAR(20) NOT NULL CHECK (status IN ('todo', 'doing', 'done'))
salary DECIMAL(12,2) NOT NULL CHECK (salary >= 0)

Quirks quan trọng (đúng với hầu hết RDBMS):

  • Phần lớn engine tự tạo index có thứ tự cho mọi PRIMARY KEY và UNIQUE constraint — không cần tạo index riêng. (Cơ chế index có thứ tự được mổ ở Module 6 của khoá này.)
  • FOREIGN KEY không tự tạo index trên cột FK ở nhiều engine. JOIN trên task.project_id mà thiếu index sẽ scan toàn bảng khi bảng lớn — cân nhắc tạo index thủ công cho cột FK hay JOIN.
  • NOT NULL không cần index — NULL check xảy ra tại write time, không phải query time.

3. Generalized UNIQUE — chặn non-overlap, không chỉ bằng-nhau

UNIQUE chỉ check operator = (hai giá trị có bằng nhau không). Nhưng có ràng buộc cần chặn theo operator khác — điển hình là range non-overlap: booking phòng không được trùng khoảng thời gian.

Use case: booking phòng — không thể có 2 booking cùng phòng overlap thời gian.

-- Y tuong agnostic: rang buoc "khong hai row nao conflict theo dieu kien overlap"
-- Khong the dien dat bang UNIQUE thuong vi UNIQUE chi so sanh =

-- Cach phổ quat nhat: bang phu (helper) + constraint logic o app/transaction
--   Insert moi booking trong transaction:
--   1. Kiem tra khong co booking nao overlap (SELECT ... WHERE room_id = ? AND
--      khong (end <= new_start OR start >= new_end))
--   2. Neu khong co -> INSERT; neu co -> reject
--   De an toan race condition: lock row room hoac dung isolation level cao hon

Một số engine cung cấp ràng buộc loại trừ chuyên dụng (generalized UNIQUE check theo operator tuỳ ý, dựa trên index hỗ trợ range) — đây là tính năng dialect, cú pháp khác nhau theo engine. Khái niệm chung quan trọng hơn cú pháp: UNIQUE chỉ chặn giá trị bằng nhau chính xác, còn non-overlap cần một cơ chế mạnh hơn — hoặc ràng buộc loại trừ của engine, hoặc kiểm tra trong transaction với lock phù hợp.

📝 Note dialect — exclusion constraint

Việc enforce non-overlap ngay ở tầng DB bằng một constraint declarative (thay vì kiểm tra trong app) là tiện ích riêng của một số engine, thường dựa trên loại index hỗ trợ so sánh range. Cú pháp và loại index yêu cầu khác nhau theo engine. Nguyên lý cần nhớ: đây là "UNIQUE tổng quát hoá" cho operator không phải =.

4. Deferred constraint — giải quyết circular FK

Mặc định, RDBMS check constraint sau mỗi statement. Chuẩn SQL cho phép khai báo DEFERRABLE INITIALLY DEFERRED để dời check xuống cuối transaction (COMMIT).

Use case điển hình: circular FK — companies tham chiếu employees (CEO), employees tham chiếu companies. Insert bên nào trước cũng vi phạm FK.

CREATE TABLE companies (
  id     BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  ceo_id BIGINT REFERENCES employees(id) DEFERRABLE INITIALLY DEFERRED
);

CREATE TABLE employees (
  id         BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  company_id BIGINT NOT NULL REFERENCES companies(id) DEFERRABLE INITIALLY DEFERRED
);

BEGIN;
  -- Insert company truoc, ceo_id tro den employee chua ton tai
  -- OK vi constraint deferred den COMMIT
  INSERT INTO companies(id, ceo_id) VALUES (1, 1);

  -- Insert employee, company_id tro den company vua insert
  INSERT INTO employees(id, company_id) VALUES (1, 1);

  -- Ca hai check tai COMMIT -- luc nay ca hai row da co
COMMIT;

Nếu không có DEFERRABLE, INSERT đầu tiên sẽ báo lỗi FK ngay lập tức.

Deferred constraint hiếm dùng trong app thông thường — chủ yếu xuất hiện khi schema có circular FK hoặc khi migration tool cần tái cấu trúc dữ liệu trong một transaction (xem Bài 06 — Schema migration).

5. Cascading — ON DELETE / ON UPDATE behavior

Khi xoá/cập nhật row cha, FK quyết định hành vi với row con:

-- 4 option ON DELETE
ON DELETE NO ACTION   -- mac dinh: block delete neu co row con reference
ON DELETE RESTRICT    -- giong NO ACTION nhung check ngay, khong defer duoc
ON DELETE CASCADE     -- xoa row con theo khi xoa row cha
ON DELETE SET NULL    -- set column FK thanh NULL (column phai nullable)
ON DELETE SET DEFAULT -- set column FK ve gia tri DEFAULT

-- TaskFlow: comments la weak entity, ton tai phu thuoc task
CREATE TABLE comments (
  id      BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  task_id BIGINT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
  user_id BIGINT       REFERENCES users(id)    ON DELETE SET NULL, -- nullable: bat buoc cho SET NULL
  body    VARCHAR(2000) NOT NULL
);
-- Xoa task -> tu dong xoa comment (weak entity, khong co y nghia doc lap)
-- Xoa user -> set comment.user_id NULL (preserve comment, lose author link)
⚠️ SET NULL bắt buộc cột nullable

ON DELETE SET NULL chỉ hợp lệ khi cột FK cho phép NULL — nếu cột có NOT NULL, hành vi SET NULL sẽ gây lỗi runtime khi xoá parent. Vì vậy ở đây comments.user_id để nullable, khác với bản schema chặt ở tour TaskFlow (nơi user_id NOT NULL vì giả định mọi comment phải có tác giả). Chọn nullable + SET NULL khi muốn giữ comment sau khi user rời đi; chọn NOT NULL + CASCADE khi comment không có tác giả là vô nghĩa. Hai thiết kế phục vụ hai yêu cầu khác nhau — quyết định theo nghiệp vụ.

Decision matrix:

Tình huốngNên dùngLý do
Child không tồn tại được nếu parent xoáCASCADEComments không có task là vô nghĩa
Muốn giữ child, chấp nhận mất linkSET NULLComment vẫn có giá trị dù user xoá tài khoản
Phải cleanup explicit trướcRESTRICTOrders của customer — xoá customer khi còn order là nguy hiểm
Child có default fallbackSET DEFAULTAssign task về "unassigned" user khi assignee rời đi

NO ACTIONRESTRICT khác nhau chỉ khi dùng với deferred constraint: NO ACTION có thể defer đến cuối transaction, RESTRICT luôn check ngay.

6. Pitfall — app validation race condition

// SAI: race condition giua check va insert
const existing = await db.query(
  'SELECT id FROM users WHERE email = $1',
  [email]
);
if (existing.rows.length > 0) {
  throw new Error('Email already exists');
}
// Khoang trong o day: 2 request cung pass check, ca 2 tien den INSERT
await db.query('INSERT INTO users(email) VALUES ($1)', [email]);

Fix: tin tưởng DB UNIQUE constraint, catch lỗi unique violation:

try {
  await db.query('INSERT INTO users(email) VALUES ($1)', [email]);
} catch (e) {
  if (isUniqueViolation(e)) {  // moi engine bao loi unique violation theo cach rieng
    throw new Error('Email already exists');
  }
  throw e;  // re-throw loi khac
}

Cách nhận diện "lỗi unique violation" là dialect — chuẩn SQL định nghĩa SQLSTATE 23505 cho unique_violation, nhưng nhiều driver/engine còn phơi ra error code hoặc message riêng. Đóng gói việc nhận diện này vào một hàm (isUniqueViolation) để app không phụ thuộc cứng vào một engine.

Pitfall — app validation không thay thế được DB constraint

Khoảng thời gian giữa SELECT (check) và INSERT (write) không được bảo vệ bởi bất kỳ lock nào trong isolation level mặc định. Hai transaction đồng thời đều thấy "email chưa tồn tại" và đều insert thành công. UNIQUE constraint enforce tại tầng storage — atomic, không có race condition. Bắt lỗi unique violation (SQLSTATE 23505 theo chuẩn SQL) để trả user-facing message phù hợp. Đây là nguyên lý đúng cho mọi RDBMS.

7. Pitfall — CHECK không thay thế validation phức tạp

-- OK: check don gian, immutable, per-row
CHECK (age >= 0 AND age <= 150)
CHECK (status IN ('todo', 'doing', 'done'))
CHECK (start_at < end_at)

-- KHONG OK: check cross-row (phai dung trigger hoac app logic)
-- "User chi tao toi da 5 project" -- khong the dung CHECK
-- CHECK chi nhin thay row hien tai, khong nhin thay row khac

-- NGUY HIEM: check goi function khong immutable
CHECK (verified_at <= CURRENT_TIMESTAMP)
-- CURRENT_TIMESTAMP thay doi theo thoi gian -- nhieu engine reject CHECK nay
-- hoac neu accept, se fail random tuy thoi diem re-evaluate
Pitfall — CHECK chỉ cho immutable per-row predicate

CHECK expression phải là immutable per-row predicate — không gọi function thay đổi theo thời gian (CURRENT_TIMESTAMP, hàm random), không tham chiếu row khác. Cross-row rule (vd giới hạn số lượng) cần trigger hoặc app logic kết hợp DB unique constraint. Time-dependent check cần xử lý ở app layer hoặc trong trigger BEFORE INSERT/UPDATE (trigger chạy tại execution time nên gọi được hàm thời gian). Nguyên tắc này đúng ở mọi RDBMS hỗ trợ CHECK.

8. Applied — TaskFlow constraint review

Đối chiếu TaskFlow schema (DDL đầy đủ ở Module 1 — tour TaskFlow schema) với constraint rationale:

-- users.email VARCHAR(254) UNIQUE NOT NULL
--   App validate unique? Race-prone. DB UNIQUE enforce: safe.
--   Bat loi unique violation de tra user-facing message.

-- project_members.role VARCHAR(20) NOT NULL CHECK (role IN ('owner','member','viewer'))
--   Enum-like validation bang CHECK.
--   Alternative la kieu ENUM/domain cua engine (dialect): type-safe hon,
--   nhung them gia tri thuong can ALTER schema phuc tap hon.
--   CHECK linh hoat hon cho value set co the thay doi.

-- tasks.project_id BIGINT NOT NULL REFERENCES projects(id)
--   Mandatory parent: task khong the ton tai khong co project.
--   Khong ON DELETE CASCADE -- xoa project KHONG tu xoa task.
--   Default (NO ACTION/RESTRICT): phai cleanup task truoc khi xoa project.
--   Design choice: preserve task history, require explicit cleanup.

-- tasks.assignee_id BIGINT REFERENCES users(id)
--   Nullable: task chua assign la hop le.
--   Khong ON DELETE CASCADE: xoa user KHONG xoa task.
--   Nen them ON DELETE SET NULL? Design choice -- task con gia tri
--   doc lap voi assignee, set NULL preserve task, lose assignment.

-- comments.task_id BIGINT NOT NULL REFERENCES tasks(id)
--   Comment la weak entity -- xoa task nen xoa comment (ON DELETE CASCADE).
--   Neu can preserve comment history (audit)? Thi RESTRICT + soft delete.
--   Them constraint nay an toan cho bang dang co du lieu: xem bai 06.

Thêm/sửa constraint trên bảng production đang có data cần migration cẩn thận — Bài 06 — Schema migration đi sâu về pattern an toàn (expand-contract, validate riêng).

9. Deep Dive — Constraints

📚 Deep Dive — Constraints
  • Use The Index, Luke — góc nhìn agnostic về index tự sinh từ PK/UNIQUE và vì sao cột FK nên có index, không gắn engine.
  • Wikipedia — Database constraints / Referential integrity — tổng quan agnostic về FK, cascading action, và referential integrity theo chuẩn SQL.
  • Tài liệu chính thức của engine bạn dùng — cho cú pháp chính xác của exclusion constraint, deferred constraint, và cách nhận diện lỗi unique violation (SQLSTATE/error code).

Ghi chú: Use The Index Luke cho intuition agnostic. Tài liệu engine cụ thể cho cú pháp non-overlap constraint và behavior cascade chi tiết — đối chiếu khi triển khai.

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

11. Tóm tắt

  • 5 constraint core: NOT NULL, UNIQUE, PRIMARY KEY, FOREIGN KEY, CHECK — mỗi loại enforce một tầng khác nhau của data integrity. Nguyên lý đúng ở mọi RDBMS.
  • Hầu hết engine tự tạo index có thứ tự cho PK và UNIQUE. FK thường không tự có index — cân nhắc tạo thủ công cho cột FK hay JOIN (Module 6 của khoá này).
  • Generalized UNIQUE (chặn non-overlap, không chỉ bằng-nhau): khái niệm phổ quát; một số engine có exclusion constraint declarative, còn lại kiểm tra trong transaction với lock phù hợp.
  • Deferred constraint dời check xuống cuối transaction — dùng khi có circular FK. Hiếm gặp trong app thường, phổ biến trong migration.
  • Cascading (CASCADE / SET NULL / RESTRICT / SET DEFAULT) là design decision: CASCADE cho weak entity, SET NULL để preserve child mất link, RESTRICT để force explicit cleanup.
  • App validate email unique race-prone — trust DB UNIQUE constraint, bắt lỗi unique violation (SQLSTATE 23505; cách nhận diện cụ thể là dialect).
  • CHECK chỉ cho immutable per-row predicate — không cross-row, không time-dependent function.
  • Forward: Bài 03 — Normalization, Bài 06 — Schema migration, Module 7 của khoá này (transaction + retry on unique violation).

11. Tự kiểm tra

Tự kiểm tra
Q1
Vì sao app validate 'email unique' race-prone dù check trước insert? Cơ chế nào của DB enforce safe?

Giữa bước SELECT (check) và INSERT (write) có một khoảng thời gian không được lock. Trong default isolation level (READ COMMITTED), hai transaction đồng thời đều thấy snapshot chưa có email — cả hai pass check. Khi đến INSERT, cả hai write thành công → duplicate.

UNIQUE constraint enforce tại tầng storage engine với locking cấp index entry — atomic. Khi transaction thứ nhất insert, nó lock index entry cho email đó. Transaction thứ hai muốn insert cùng email sẽ phải chờ (hoặc nhận conflict ngay). Không có race condition vì lock ở tầng thấp hơn app code có thể reach. Nguyên lý này đúng ở mọi RDBMS.

Pattern đúng: bỏ SELECT check, dùng INSERT trực tiếp, bắt lỗi unique violation (SQLSTATE 23505 theo chuẩn SQL — cách driver phơi ra error code là dialect) để trả user-facing message.

Q2
Phân biệt khi nào dùng CASCADE, SET NULL, RESTRICT cho FK. Cho 2 ví dụ TaskFlow cụ thể cho mỗi option.

CASCADE — khi child không có ý nghĩa độc lập nếu parent bị xoá:

  • Xoá task → xoá comments của task (comment không tồn tại được không có task).
  • Xoá project → xoá project_members rows của project đó (membership không tồn tại được không có project).

SET NULL — khi muốn preserve child, chấp nhận mất link:

  • Xoá user → set comments.user_id = NULL (giữ comment, mất thông tin tác giả).
  • Xoá user → set tasks.assignee_id = NULL (task về trạng thái unassigned, không mất task).

RESTRICT — khi phải cleanup explicit trước khi xoá parent:

  • Xoá project khi còn task → lỗi, phải archive/delete task trước (preserve intent).
  • Xoá user khi còn project owned → lỗi, phải transfer ownership hoặc delete project trước.
Q3
Generalized UNIQUE (chặn non-overlap) vs UNIQUE thường — cho 1 use case mỗi loại. Vì sao UNIQUE không đủ cho non-overlap booking?

UNIQUE thường phù hợp khi cần block hai row có giá trị bằng nhau: email VARCHAR(254) UNIQUE — hai user không được có cùng email. UNIQUE chỉ dùng operator =.

Generalized UNIQUE (non-overlap) phù hợp khi cần block hai row conflict theo operator tùy ý: booking phòng không được overlap thời gian (operator "overlap" giữa hai khoảng).

UNIQUE không đủ cho booking vì nó chỉ check hai period có bằng nhau chính xác không. Booking [14:00, 16:00)[15:00, 17:00) khác nhau (UNIQUE không block), nhưng overlap (cần block). Không có cách express "không overlap" bằng UNIQUE. Giải pháp phổ quát: exclusion constraint của engine (dialect) nếu có, hoặc kiểm tra overlap trong transaction với lock phù hợp ở mọi RDBMS.

Q4
CHECK constraint gọi hàm thời gian (CURRENT_TIMESTAMP) — vì sao fail random? Pattern thay thế là gì?

Hàm thời gian như CURRENT_TIMESTAMP là volatile — giá trị thay đổi theo thời gian. Chuẩn SQL yêu cầu CHECK expression phải immutable: cùng input luôn cho cùng output. Predicate verified_at <= CURRENT_TIMESTAMP đúng tại thời điểm insert nhưng có thể sai khi engine re-evaluate constraint (vd sau maintenance, reindex, hoặc sau khi row được update).

Nhiều engine reject hoặc warn khi định nghĩa CHECK với non-immutable function. Ngay cả khi accept được, behavior undefined và có thể gây lỗi lạ.

Pattern thay thế (agnostic):

  • Validation time-dependent → app layer trước khi insert.
  • Nếu cần enforce ở DB: dùng trigger BEFORE INSERT OR UPDATE — trigger có thể gọi hàm thời gian vì nó chạy tại execution time, không phải definition time. (Ngôn ngữ viết trigger là dialect.)
Q5
DB báo unique violation (SQLSTATE 23505). App nên catch + retry hay throw user-facing error? Tradeoff của mỗi approach.

Throw user-facing error (phổ biến hơn cho unique constraint trên business key như email):

  • Ưu: rõ ràng với user, không retry vô nghĩa — email đã tồn tại là trạng thái terminal, không thể resolve bằng retry.
  • Cách: nhận diện lỗi unique violation (SQLSTATE 23505; driver còn phơi ra error code/constraint name theo cách riêng — dialect), trả message cụ thể.

Retry (phù hợp cho unique constraint trên generated key hoặc idempotent insert):

  • Ưu: tự động resolve khi conflict là tạm thời — vd ULID/UUID collision cực hiếm nhưng có thể xảy ra.
  • Cách: retry với exponential backoff, giới hạn số lần, log nếu vượt ngưỡng.
  • Cũng dùng khi cần upsert: chuẩn SQL có câu lệnh MERGE; nhiều engine thêm cú pháp upsert riêng (dialect) — tránh round-trip select + retry. Xem Module 4 của khoá này về upsert/MERGE.

Module 7 của khoá này (transactions & consistency) đi sâu về retry strategy và isolation level liên quan đến serialization failure.

Bài tiếp theo: Normalization 1NF→3NF — refactor TaskFlow tags TEXT thành tags table

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