Hướng dẫn thiết kế Database Schema chuẩn cho hệ thống lớn
Một database schema tốt cho hệ thống lớn cần ba điều: mô hình dữ liệu phản ánh đúng nghiệp vụ, ràng buộc chặt để dữ liệu không thể sai, và cấu trúc cho phép truy vấn nhanh khi bảng có hàng chục triệu dòng. Bài hướng dẫn này đi qua từng bước từ mô hình hóa thực thể, chọn khóa và kiểu dữ liệu, đặt chỉ mục, đến cách thay đổi schema khi hệ thống đang chạy. Ví dụ dùng PostgreSQL, phần lớn áp dụng được cho MySQL và SQL Server.
Bắt đầu từ nghiệp vụ, không phải từ bảng
Trước khi viết CREATE TABLE, hãy liệt kê các thực thể và quan hệ bằng ngôn ngữ nghiệp vụ: một khách hàng có nhiều đơn hàng; một đơn hàng có nhiều dòng hàng; mỗi dòng tham chiếu một sản phẩm; sản phẩm thuộc một danh mục. Sau đó hỏi những câu hỏi khó:
- Giá trên dòng đơn hàng là giá tại thời điểm mua hay giá hiện tại của sản phẩm? (Gần như luôn phải lưu giá tại thời điểm mua.)
- Một khách hàng có thể có nhiều địa chỉ, số điện thoại không?
- Khi xóa sản phẩm, đơn hàng cũ có còn hiển thị tên sản phẩm không?
- Dữ liệu nào cần lưu lịch sử thay đổi để đối soát?
Vẽ sơ đồ ERD và cho người hiểu nghiệp vụ xem lại. Sửa trên giấy rẻ hơn nhiều so với migration trên bảng 50 triệu dòng.
Chuẩn hóa đến đâu là đủ
Chuẩn hóa nhằm loại bỏ dữ liệu trùng lặp để tránh bất thường khi cập nhật. Ba dạng chuẩn đầu tiên là đủ cho hầu hết hệ thống:
- 1NF: mỗi cột chứa giá trị nguyên tử, không lưu danh sách số điện thoại cách nhau bằng dấu phẩy trong một cột.
- 2NF: với khóa chính gồm nhiều cột, mọi cột khác phụ thuộc vào toàn bộ khóa chứ không phải một phần.
- 3NF: không có cột phụ thuộc bắc cầu, ví dụ không lưu tên tỉnh trong bảng khách hàng nếu đã có mã tỉnh tham chiếu bảng tỉnh.
Phi chuẩn hóa có chủ đích là hợp lệ khi có lý do đo được: lưu sẵn tổng tiền đơn hàng thay vì tính lại mỗi lần, lưu tên sản phẩm trên dòng đơn hàng để giữ lịch sử. Điều quan trọng là ghi rõ cột nào là bản sao và cơ chế nào giữ nó đúng.
Khóa chính và định danh
Hai lựa chọn phổ biến:
| Kiểu khóa | Ưu điểm | Nhược điểm |
|---|---|---|
| Số tự tăng (BIGINT identity) | Nhỏ gọn, chỉ mục hiệu quả, dễ đọc | Lộ số lượng bản ghi nếu đưa ra URL; khó sinh ở nhiều nơi khi phân tán |
| UUID | Sinh được ở client hoặc nhiều service, không đoán được | Lớn hơn (16 byte); UUID v4 ngẫu nhiên làm chỉ mục phân mảnh |
Với UUID, nên dùng loại có yếu tố thời gian như UUID v7 (được chuẩn hóa trong RFC 9562) để giá trị tăng dần, giảm phân mảnh chỉ mục B-tree. Một cách kết hợp phổ biến là dùng BIGINT làm khóa nội bộ và một cột UUID công khai cho API.
Dùng INT 32 bit cho bảng có thể tăng trưởng lớn là rủi ro: giới hạn khoảng 2,1 tỷ giá trị nghe có vẻ xa, nhưng bảng log, sự kiện hay bảng có nhiều lần xóa và chèn có thể chạm tới. Mặc định BIGINT cho khóa chính là lựa chọn an toàn.
Chọn kiểu dữ liệu chính xác
- Tiền tệ: dùng NUMERIC/DECIMAL hoặc số nguyên theo đơn vị nhỏ nhất, không dùng FLOAT vì sai số làm tròn.
- Thời gian: dùng kiểu có múi giờ (timestamptz trong PostgreSQL), lưu theo UTC, chuyển đổi khi hiển thị.
- Trạng thái: dùng cột text có ràng buộc CHECK hoặc bảng tham chiếu, thay vì số ma thuật 1, 2, 3 không ai nhớ nghĩa.
- Chuỗi: giới hạn độ dài hợp lý, dùng collation phù hợp để sắp xếp tiếng Việt đúng.
- JSON: JSONB hữu ích cho thuộc tính linh hoạt (thông số sản phẩm khác nhau giữa danh mục), nhưng đừng biến cả bảng thành kho JSON. Dữ liệu được lọc, join, ràng buộc thường xuyên nên là cột thật.
Ràng buộc là tuyến phòng thủ cuối
Code ứng dụng sẽ có bug, sẽ có script sửa dữ liệu chạy tay, sẽ có service thứ hai ghi vào cùng bảng. Ràng buộc trong database đảm bảo dữ liệu vẫn đúng dù tầng trên sai:
- NOT NULL cho mọi cột bắt buộc.
- FOREIGN KEY cho mọi quan hệ, chọn hành vi khi xóa (RESTRICT, CASCADE, SET NULL) có chủ đích.
- UNIQUE cho email, mã đơn hàng, cặp giá trị không được trùng.
- CHECK cho quy tắc đơn giản: số lượng lớn hơn 0, ngày kết thúc sau ngày bắt đầu.
Integration test chạy trên database thật sẽ bắt được vi phạm ràng buộc sớm; bài hướng dẫn viết integration test mô tả cách dựng môi trường đó.
Chỉ mục: thiết kế theo truy vấn
Chỉ mục tăng tốc đọc nhưng làm chậm ghi và tốn dung lượng. Quy trình hợp lý:
- Liệt kê các truy vấn quan trọng nhất (màn hình danh sách, tìm kiếm, báo cáo).
- Tạo chỉ mục cho cột trong WHERE, JOIN, ORDER BY của chúng. Mọi khóa ngoại nên có chỉ mục.
- Với chỉ mục nhiều cột, thứ tự cột quan trọng: cột lọc bằng đặt trước, cột lọc khoảng hoặc sắp xếp đặt sau.
- Dùng chỉ mục một phần (partial index) cho tập con hay truy vấn, ví dụ chỉ các đơn hàng chưa hoàn tất.
- Kiểm tra bằng EXPLAIN ANALYZE với dữ liệu gần thật, không chỉ với vài dòng dữ liệu thử.
Định kỳ rà soát chỉ mục không được dùng (PostgreSQL có thống kê trong pg_stat_user_indexes) và xóa bớt. Với nhu cầu tìm kiếm văn bản tự do trên khối dữ liệu lớn, cân nhắc công cụ chuyên dụng như trình bày trong bài so sánh các giải pháp search engine.
Chuẩn bị cho dữ liệu lớn dần
Phân vùng bảng
Bảng log, giao dịch, sự kiện tăng theo thời gian nên được phân vùng (partitioning) theo tháng hoặc quý. Truy vấn theo khoảng thời gian chỉ quét phân vùng liên quan, và xóa dữ liệu cũ chỉ cần gỡ phân vùng thay vì DELETE hàng triệu dòng.
Tách đọc và ghi
Khi tải đọc lớn, thêm bản sao chỉ đọc (read replica) cho báo cáo và màn hình danh sách. Ứng dụng phải chấp nhận độ trễ đồng bộ vài giây ở các màn hình này.
Lưu trữ dữ liệu lịch sử
Dữ liệu ít truy cập có thể chuyển sang bảng lưu trữ hoặc kho phân tích. Bài xây dựng Data Lake mô tả cách tách nhu cầu phân tích ra khỏi database giao dịch.
Thay đổi schema khi hệ thống đang chạy
Mọi thay đổi schema phải đi qua migration có phiên bản (Flyway, Liquibase, Alembic, Prisma Migrate...) và được review như code. Với bảng lớn, áp dụng mẫu "mở rộng rồi thu hẹp" (expand and contract):
- Thêm cột mới cho phép NULL (thao tác nhanh).
- Deploy code ghi vào cả cột cũ và cột mới.
- Chạy backfill theo lô nhỏ để tránh khóa bảng lâu.
- Chuyển code sang đọc cột mới.
- Thêm ràng buộc NOT NULL, rồi xóa cột cũ ở một lần deploy sau.
Tạo chỉ mục trên bảng lớn nên dùng chế độ không khóa ghi (CREATE INDEX CONCURRENTLY trong PostgreSQL). Luôn thử migration trên bản sao dữ liệu production trước để biết thời gian chạy thực tế.
Quy ước đặt tên và tài liệu
Thống nhất một quy ước và tuân thủ: tên bảng số nhiều hoặc số ít (chọn một), snake_case, khóa ngoại dạng customer_id, cột thời gian created_at, updated_at. Ghi chú (COMMENT) cho cột có ý nghĩa nghiệp vụ không hiển nhiên. Người mới vào đội sẽ hiểu hệ thống nhanh hơn nhiều khi đọc được schema như đọc tài liệu.
Câu hỏi thường gặp
Có nên dùng soft delete cho mọi bảng không?
Không nhất thiết. Soft delete (cột deleted_at) tiện khôi phục nhưng làm mọi truy vấn phải thêm điều kiện lọc và khiến ràng buộc UNIQUE phức tạp hơn. Chỉ dùng cho dữ liệu thực sự cần khôi phục hoặc kiểm toán; với nhu cầu lưu lịch sử, bảng audit riêng thường rõ ràng hơn.
Khi nào nên dùng NoSQL thay cho database quan hệ?
Khi dữ liệu thực sự không có cấu trúc cố định, cần ghi cực lớn phân tán hoặc mô hình truy cập rất đơn giản theo khóa. Với dữ liệu nghiệp vụ có nhiều quan hệ và yêu cầu nhất quán, database quan hệ vẫn là lựa chọn mặc định hợp lý.
ORM có tự thiết kế schema tốt không?
ORM sinh schema chạy được nhưng thường thiếu chỉ mục phù hợp, ràng buộc CHECK và kiểu dữ liệu tối ưu. Hãy xem lại migration do ORM sinh ra như xem code, và bổ sung bằng tay khi cần.
Bao lâu nên rà soát lại schema một lần?
Nên xem lại chỉ mục và truy vấn chậm định kỳ hằng tháng hoặc hằng quý dựa trên log truy vấn chậm, và rà soát mô hình mỗi khi có thay đổi nghiệp vụ lớn.
Bài liên quan
Hướng dẫn viết Unit Test và Integration Test đảm bảo chất lượng code
Cách viết unit test và integration test có giá trị: chọn phạm vi, đặt tên, dùng mock đúng chỗ, dựng database thử, đưa test vào CI và tránh test giòn.

So sánh Agile, Scrum và Kanban: chọn cách làm nào cho đội phần mềm
Agile là triết lý, Scrum và Kanban là hai cách thực hành. Bài so sánh vai trò, nhịp làm việc, chỉ số đo lường và gợi ý chọn mô hình theo loại dự án.

So sánh message queue: RabbitMQ, Kafka và Redis Pub/Sub
RabbitMQ, Kafka hay Redis Pub/Sub? So sánh mô hình lưu trữ, đảm bảo giao nhận, thứ tự tin nhắn, vận hành và tình huống nên dùng từng giải pháp.
So sánh nền tảng CMS WordPress, Strapi, Directus cho doanh nghiệp
WordPress, Strapi hay Directus? So sánh kiến trúc, đối tượng biên tập, khả năng mở rộng, bảo mật, chi phí vận hành để doanh nghiệp chọn CMS phù hợp.