Tôi từng thấy một startup ở TP.HCM cười khả lạc vì "chỉ cần scale" cơ sở dữ liệu. Tháng thứ nhất, họ có 10K users. Tháng thứ sáu, query SELECT * from orders cần 45 giây để chạy. Không phải cơ sở dữ liệu yếu, mà là thiết kế từ đầu đã bị sai. Họ mất 3 tháng refactor toàn bộ schema mà vẫn không xong hẳn.
Đó là lý do tôi đặt tiêu đề này không phải "Database Design Best Practices" buồn ngủ, mà là câu chuyện thực.
Thiết kế schema là thiết kế business logic
Mọi người thường nghĩ database chỉ là chỗ lưu trữ. Sai lận. Schema chính là biểu diễn của cách bạn hiểu business. Nếu schema sai, mọi thứ sau đó sẽ tệ.
Ví dụ: bạn làm app giao hàng tại Việt Nam. Bạn sẽ có bảng orders. Nhưng sẽ có order chưa thanh toán, order đã thanh toán, order đã giao, order bị hủy. Bạn có 2 cách:
Cách 1: Thêm column status và paid, delivered, cancelled (boolean).
Cách 2: Có một bảng order_status_history lưu lại toàn bộ hành trình, với timestamp.
Cách 1 nhanh hơn khi query (ít joins), nhưng khi sếp muốn xem "có bao nhiêu order đó ở status nào vào ngày nào", bạn méo có dữ liệu. Cách 2 chậm hơn một tí khi query (vì cần LEFT JOIN), nhưng bạn có toàn bộ lịch sử — điều vô giá khi làm analytics, tracing bugs, hay phát hiện gian lận.
Startup mà tôi nói ở trên chọn cách 1. 6 tháng sau, họ muốn làm feature "hiển thị timeline order", phải sửa 10 cái schema khác. Nếu thiết kế từ đầu theo cách 2, sẽ còn 8 schema.
Normalize vs Denormalize — không phải đen trắng
Chia sẻ bài viết
Bài viết liên quan
Bạn cần tư vấn về công nghệ?
Đội ngũ Idflow luôn sẵn sàng hỗ trợ bạn trong hành trình chuyển đổi số.
Trong đại học, bạn học "chuẩn hóa dữ liệu" (normalization): tách bảng, tránh trùng lặp. Chuẩn hóa đầy đủ? Yêu cầu 5-6 joins cho một query đơn giản. Trong thực tế, các app lớn không bao giờ normalize 100%.
PostgreSQL ở Shopify có thể chứa 10 tỉ rows. Họ vẫn denormalize một số dữ liệu — lưu user_email thẳng trong bảng orders thay vì chỉ foreign key user_id. Tại sao? Vì khi query "tất cả orders của user X", họ không muốn join thêm bảng users.
Chiến lược thực tế:
- Normalize những thứ thay đổi thường xuyên (user profile, product info).
- Denormalize những thứ thay đổi hiếm (email, name khi order được tạo).
- Luôn có audit trail — bảng history để tracking thay đổi.
Index là phần khó nhất
Viết query đúng dễ. Viết query nhanh khó. Và 90% vấn đề performance không phải vì database engine yếu, mà vì thiếu index hoặc index sai chỗ.
Một table users có 5 triệu rows. Bạn query SELECT * FROM users WHERE email = 'foo@bar.com'. Không index? 5 triệu rows phải scan. Với index? 0.5ms.
Nhưng index cũng có giá. Mỗi index lưu thêm dữ liệu. Khi bạn INSERT hoặc UPDATE, database phải update tất cả indexes — chậm hơn. Bạn có 20 indexes cho một table? Write performance sẽ chết.
Quy tắc ngón tay cái:
- Index những column bạn hay filter hoặc sort (WHERE, ORDER BY).
- Index những column trong JOIN.
- Chỉ index 5-7 column quan trọng nhất. Ngoài ra, cân nhắc composite index (index trên nhiều column cùng lúc).
Ví dụ ở Việt Nam: Grab, Tiki, Shopee — họ sử dụng PostgreSQL hoặc MySQL với hàng trăm indexes, nhưng không ai index tất cả column. Họ dùng query analyzer để xem cái nào slow, rồi index strategic.
Constraints là bảo vệ của bạn
Nhiều người bỏ qua NOT NULL, UNIQUE, FOREIGN KEY vì "ứng dụng sẽ handle". Sai. Database constraints là tầng bảo vệ cuối cùng — khi ứng dụng có bug, constraints sẽ chặn dữ liệu xấu vào database.
Nếu không có NOT NULL trên email, bạn sẽ có users mà email là NULL. Rồi một ngày nào đó, code quên check NULL, gửi email về NULL — ứng dụng crash. Nếu có constraint, INSERT sẽ fail ngay, bạn biết vấn đề là ở application logic, không phải cơ sở dữ liệu.
Tương tự với FOREIGN KEY: nếu bạn xóa một user mà không xóa orders của họ, database sẽ từ chối (hoặc cascade delete nếu bạn cấu hình). Điều này buộc bạn suy nghĩ về data integrity từ đầu.
Migrations — phần bị quên lãng
Bạn viết schema, deploy. 3 tháng sau cần thêm column. Bạn chạy ALTER TABLE. Nếu table có 100M rows ở AWS RDS, ALTER TABLE sẽ lock table trong vài phút, app bị down.
Cách đúng: dùng zero-downtime migration. Ví dụ Shopify dùng Mysql 5.7+ với ALGORITHM=INPLACE, PostgreSQL dùng CONCURRENTLY cho indexes. Hoặc bạn dùng tool như Alembic (Python), Flyway (Java), hay go-migrate (Go) để version schema giống code.
Một chiếc startup ở Hà Nội mất 2 tiếng down service vì ALTER TABLE. Giờ họ dùng Alembic cho tất cả migrations — 0 down time.
---
Thiết kế cơ sở dữ liệu không phải art, cũng không phải pure science — nó là craft. Bạn cần kinh nghiệm, feedback loop, và sự sẵn sàng thay đổi khi thực tế khác với kỳ vọng. Khi dự án phức tạp, đầu tư thời gian vào design từ đầu sẽ tiết kiệm hàng tháng refactor sau này.
Công cụ như Idflow Technology giúp bạn visualize, monitor, và optimize schema — nhưng chiếc khóa là hiểu rõ business, và thiết kế schema theo đó.