Dưới đây là các ý chính từ bài viết “Artem Khrenov – Database Schema Design: Principles Every Developer Must Know”.


1. Lý do schema thiết kế tốt rất quan trọng

  • Thiết kế schema kém có thể dẫn đến việc bảo trì khó khăn, truy vấn chậm, và chi phí kỹ thuật tăng theo thời gian.
  • Ngược lại, một schema cấu trúc rõ ràng giúp: hiệu năng tốt hơn, code dễ hiểu hơn, linh hoạt khi thay đổi yêu cầu.

2. Hiểu domain / mô hình nghiệp vụ trước khi tạo bảng

  • Trước khi viết các câu CREATE TABLE, cần xác định các entity (ví dụ: users, products, orders) và mối quan hệ giữa chúng.
  • Schema nên phản ánh đúng mô hình nghiệp vụ để khi nhìn vào bảng và quan hệ bạn có thể biết “student có thể học cùng course mấy lần?”, “customer có bao nhiêu orders?”, …

3. Khóa chính (Primary Key)

  • Mỗi bảng cần khóa chính để duy nhất xác định mỗi bản ghi.
  • Nên ưu tiên sử dụng surrogate key (ví dụ auto-increment integer hoặc UUID) thay vì natural key (ví dụ email, SKU) vì natural key có thể đổi, gây khó khăn khi thay đổi.
  • Cân nhắc kiểu khóa: auto-increment integer (nhanh, đơn giản) vs UUID (phù hợp phân tán nhưng có thể gây phân mảnh index) vs Snowflake IDs.

4. Khóa ngoại (Foreign Key) và tính toàn vẹn dữ liệu

  • Sử dụng ràng buộc khóa ngoại ở cấp cơ sở dữ liệu để đảm bảo tính toàn vẹn — tránh việc có dữ liệu “mồ côi”, tức là orders trỏ tới user không tồn tại.
  • Mặc dù có ý kiến rằng khóa ngoại làm chậm ghi hoặc khó thay đổi schema, nhưng chi phí sửa lỗi sau thường lớn hơn nhiều.
  • Cần chỉ định rõ hành vi khi xóa hoặc cập nhật bản ghi được tham chiếu (ON DELETE CASCADE, ON UPDATE CASCADE, RESTRICT, SET NULL, v.v.).

5. Quy tắc đặt tên (Naming Conventions)

  • Quy tắc đặt tên nhất quán giúp schema dễ đọc và hiểu. Ví dụ: dùng snake_case cho tên bảng và cột.
  • Tên bảng nên ở dạng plural vì chứa nhiều bản ghi (ví dụ users, orders). Cột nên singular và mô tả rõ dữ liệu của nó (first_name, is_active, không chỉ name hay value).
  • Cột tham chiếu nên có tên rõ ràng như user_id, product_id thay vì chỉ id để join dễ hiểu. Boolean columns nên đặt tên như câu hỏi: is_active, has_discount. Timestamps nên dùng tên như created_at, updated_at.

6. Xử lý thời gian và timestamp đúng cách

  • Luôn lưu UTC trong database thay vì timezone cục bộ.
  • Chọn đúng kiểu dữ liệu: nếu chỉ cần ngày thì dùng DATE, nếu cần cả thời gian thì TIMESTAMP/TIMESTAMPTZ.
  • Mỗi bảng nên có các cột audit: created_at, updated_at, và nếu dùng soft delete thì deleted_at.
  • Khi cần lưu khoảng thời gian hoặc xử lý time-ranges, có thể tận dụng các kiểu dữ liệu chuyên biệt của DB (như PostgreSQL range types) để truy vấn hiệu quả hơn.

7. Tính toàn vẹn dữ liệu mở rộng (Beyond basic FKs)

  • Ngoài khóa ngoại, còn có các loại ràng buộc khác: CHECK constraints để bắt các điều kiện nghiệp vụ, UNIQUE để đảm bảo không trùng lặp không hợp lý.
  • Ví dụ: CHECK (price >= 0), CHECK (status IN ('draft', 'active', …)).
  • Sử dụng index điều kiện (partial unique index) cho trường hợp đặc biệt như “mỗi user chỉ có 1 address primary”.
  • Triggers cũng có thể dùng nhưng nên hạn chế vì dễ gây schema phức tạp và khó hiểu.

8. Quản lý schema như mã nguồn (Version Control for Database Schemas)

  • Schema cũng nên được version control, review, migration rõ ràng — giống như code ứng dụng.
  • Sử dụng migration scripts có phần “up” (áp dụng thay đổi) và “down” (quay lại).
  • Khi thay đổi ở production, ưu tiên non-breaking changes: thêm cột mới rồi thay đổi code ứng dụng, sau đó mới drop cột cũ — tránh downtime.
  • Thiết lập quy trình team: code review thay đổi schema, test migrations trên staging bằng dữ liệu gần giống production, có kế hoạch rollback.

9. Chiến lược lập chỉ mục (Indexing Strategy)

  • Index giúp truy vấn nhanh hơn nhưng làm chậm ghi và chiếm không gian. Cần cân nhắc rồi đo đạc.
  • Luôn tạo index trên các cột khóa ngoại vì join thường dùng chúng.
  • Tạo index cho các trường lọc thường xuyên, hoặc tạo compound index (nhiều cột) nếu truy vấn lọc nhiều điều kiện. Đặt thứ tự các cột trong index sao cho cột filter chọn lọc trước, sau đó cột sort nếu cần.
  • Dùng loại index đặc biệt nếu cần: ví dụ full-text search (GIN index trên PostgreSQL), partial index để chỉ index các bản ghi đáp ứng điều kiện (ví dụ chỉ is_active = true).
  • Theo dõi việc sử dụng index: nếu một index gần như không được dùng, hãy drop để tránh lãng phí.

10. Xử lý dữ liệu phân cấp (Hierarchical Data)

  • Dữ liệu phân cấp (ví dụ: category tree, comment threads) có nhiều cách mô hình:
    • Adjacency list: đơn giản, mỗi bản ghi có parent_id. Truy vấn toàn bộ cây cần recursive CTE.
    • Materialized path: lưu đường dẫn (path) tới node mỗi bản ghi (ví dụ '1.2.3'). Truy vấn nhanh hơn nhưng cập nhật khi di chuyển cây khá tốn.
    • Nested set model: lưu các giá trị lft, rgt để biểu diễn cây, rất nhanh cho đọc (read-heavy) nhưng khó cập nhật nhiều.
  • Tùy vào đặc điểm ứng dụng (cây thay đổi thường xuyên hay không) mà chọn mô hình phù hợp.

11. Xử lý liên kết đa hình (Polymorphic Associations)

  • Khi một bảng cần tham chiếu tới nhiều bảng khác (ví dụ comments có thể liên kết với posts, photos, …) có hai cách:
    • Tạo riêng bảng join cho mỗi loại parent (ví dụ post_comments, photo_comments) — an toàn và rõ ràng nhưng nếu có nhiều loại thì phức tạp.
    • Dùng bảng chung với commentable_type + commentable_id — tiện lợi nhưng mất ràng buộc khóa ngoại mạnh, cần thêm các ràng buộc check hoặc trigger để đảm bảo.
  • Nếu số loại parent ít và ổn định, chọn cách đầu; nếu nhiều và thay đổi thường xuyên, chọn cách bảng chung với phần bổ trợ để giữ integrity.

12. Thiết kế schema cho Multi-Tenancy

  • Khi ứng dụng nhiều khách hàng (tenant) dùng chung hệ thống, có các chiến lược:
    • Shared schema với tenant_id: mỗi bảng có tenant_id để phân biệt. Dễ triển khai nhưng nếu lọc tenant bị bỏ sót thì có nguy cơ lộ dữ liệu. Cần row-level security để bảo vệ.
    • Schema per tenant: mỗi tenant có riêng schema (trong cùng DB). Cách này cô lập tốt hơn, nhưng quản lý nhiều schema và migrations phức tạp hơn.
    • Database per tenant: mỗi tenant có DB riêng. Cô lập mạnh nhất nhưng phức tạp cao nhất (quản lý kết nối, migrations, dashboard đa-tenant).
  • Tác giả khuyến nghị: nếu mới bắt đầu, chọn shared schema + row-level security, và nếu cần sau này mới chuyển mô hình mạnh hơn.

13. Soft Deletes & Audit Trails

  • Hard delete (xóa thật) có thể gây mất dữ liệu quan trọng hoặc phá vỡ truy vấn lịch sử. Soft delete (thêm deleted_at) giúp giữ dữ liệu cho audit, phục hồi.
  • Khi dùng soft delete cần nghĩ tới: unique constraints (ví dụ SKU chỉ uniqueness trong bản ghi chưa bị deleted), foreign key và logic xóa kết hợp.
  • Audit trail: thêm các cột như created_by, updated_by, deleted_by hoặc tạo bảng audit riêng để ghi lại thay đổi (INSERT/UPDATE/DELETE) — giúp truy xuất lịch sử dữ liệu.

14. JSON và dữ liệu bán cấu trúc (Semi-Structured Data)

  • Nhiều DB hiện hỗ trợ type JSON/JSONB — rất hữu ích khi mỗi bản ghi có cấu trúc khác nhau.
  • Tuy nhiên:
    • Không nên “nhồi” mọi thứ vào JSON chỉ vì tiện — nếu trường sẽ được query nhiều hoặc có ràng buộc nghiệp vụ thì nên đặt thành cột riêng.
    • Nếu dùng JSONB (PostgreSQL) thì có thể tạo index GIN và query hiệu quả.
  • Ví dụ: bảng products có cột attributes JSONB để lưu các đặc tính biến đổi theo loại sản phẩm, trong khi các cột vốn có như name, price vẫn là cột riêng.

15. Ví dụ tổng hợp

  • Bài viết cuối cùng đưa ra một schema mẫu cho một hệ thống quản lý khóa học (course management system), áp dụng hầu hết các nguyên tắc trên: từ bảng users, roles, courses, lessons, enrollments, assignments… với naming conventions, constraints, indexing, hierarchical path, soft deletes, JSON metadata.
  • Đây là minh họa rất tốt cho cách tổng hợp các nguyên tắc vào một hệ thống thực tế.