Data Modeling (P3/3): So với PostgreSQL và lỗi thường gặp
Ở phần trước: $lookup không có index ở foreignField thì chậm hơn nhiều, còn denormalization đổi tốc độ đọc lấy công sức ghi. Bản chép chỉ đáng giá khi thông tin đó ít khi đổi.
- Cần đọc trước: Denormalization,
$lookupvà thí nghiệm hai mô hình - Dẫn tới: Schema Design Patterns & Schema Evolution, bài tiếp theo. Phần này là phần cuối của bài Data Modeling.
MongoDB vs PostgreSQL: cùng hệ thống đơn hàng
Bắt đầu từ bài toán, không phải từ database. Cùng năm access pattern ở đầu bài.
PostgreSQL (chuẩn hóa) MongoDB (mô hình A)
customers ──┐ Order
│ customer_id ├── customer {_id, name, phone}
orders ─────┤ ├── items [ {productId, name, qty, price} ]
│ order_id ├── status, createdAt
order_items ┤ └── total
│ product_id
products ───┘ customers, products: collection riêng-- PostgreSQL: màn chi tiết đơn
SELECT o.id, o.status, c.name, c.phone, i.product_id, i.product_name, i.qty, i.unit_price
FROM orders o
JOIN customers c ON c.id = o.customer_id
JOIN order_items i ON i.order_id = o.id
WHERE o.id = 4242 AND o.tenant_id = 't43';Vài điều đáng chú ý, theo thứ tự quan trọng:
1. PostgreSQL cũng cần index ở phía bị join. Tài liệu PostgreSQL ghi rõ: khai báo foreign key không tự tạo index trên cột tham chiếu. Thiếu index trên order_items(order_id), PostgreSQL cũng phải tìm cách quét bảng hoặc chọn kiểu join khác. Bài học "index ở phía bị join" ở Experiment 1 không phải chuyện riêng của MongoDB.
2. PostgreSQL cũng denormalize. Gần như mọi hệ thống đơn hàng tử tế đều lưu unit_price (và thường cả product_name) trong order_items, chứ không join sang giá hiện tại của products. Lý do giống tờ hóa đơn siêu thị: đó là dữ liệu temporal. Khác biệt nằm ở mức độ: MongoDB đẩy ý tưởng "lưu theo hình dạng khi đọc" đi xa hơn, đến mức gom cả đơn vào một document.
3. Mỗi bên tối ưu cho một kiểu câu hỏi.
| Workload | Mô hình chuẩn hóa (PostgreSQL, hoặc MongoDB mô hình B) | Mô hình document (MongoDB mô hình A) |
|---|---|---|
| Đọc trọn một đơn theo id | nhiều lần index lookup cộng join | ✓ một document |
| Tạo đơn trọn vẹn | transaction nhiều dòng (PostgreSQL làm rất tốt) | ✓ một lần insert, atomic sẵn |
| Khách đổi tên | ✓ sửa 1 dòng | ⚠ sửa mọi bản chép (hoặc quyết định không sửa) |
| Báo cáo "top sản phẩm tháng" | ✓ quét order_items, GROUP BY | ⚠ $unwind items của mọi đơn, hoặc tính sẵn số liệu mỗi khi có đơn mới |
| Câu hỏi ad-hoc theo trục bất kỳ | ✓ JOIN linh hoạt, planner trưởng thành | ⚠ trục nào chưa được thiết kế sẵn thì tốn hơn |
| Đơn có cấu trúc khác nhau theo ngành hàng | ⚠ thêm cột / bảng phụ / JSONB | ✓ schema linh hoạt (kèm $jsonSchema từ bài Document Model) |
PostgreSQL cũng có JSONB để lưu một đơn thành một giá trị JSON nếu muốn. Ranh giới "SQL thì phải chuẩn hóa" không cứng như người ta hay nghĩ.
Hai thiết kế này không phải một cái tốt, một cái xấu. Chúng đang tối ưu cho những cách sử dụng dữ liệu khác nhau. Mô hình chuẩn hóa tối ưu cho tính đúng và sự linh hoạt khi bạn chưa biết hết câu hỏi tương lai. Mô hình document tối ưu cho vài câu hỏi bạn đã biết là sẽ hỏi hàng triệu lần. Tôi không đo PostgreSQL trong bài này. So sánh hiệu năng định lượng giữa hai hệ để dành cho bài Anti-patterns & MongoDB vs PostgreSQL, với cùng dữ liệu và cùng điều kiện.
Những lỗi thường gặp
- Dịch thẳng schema SQL sang MongoDB. Mỗi bảng thành một collection, mỗi JOIN thành một
$lookup. Bạn nhận về cái giá của join mà không có lợi ích của document. - Embed mọi thứ "cho nhanh". Comments, logs, lịch sử đơn của khách được nhét vào mảng. Nó chạy tốt ở tuần đầu và vỡ khi dữ liệu thật đổ về.
- Copy field hay thay đổi. Ví dụ copy số lượng tồn kho của sản phẩm vào từng đơn, rồi phải cập nhật hàng nghìn đơn mỗi lần có hàng về.
$lookupmà không có index ởforeignField. Trên lab này, khác biệt là khoảng 75 ms so với 0,3 ms cho mỗi đơn.- Cập nhật bản chép mà không có index để tìm nó.
updateManyquét cả collection để sửa 10 document. - Quên rằng
updateManykhông atomic trên toàn bộ. Lệnh đồng bộ bản chép phải chạy lại được an toàn. - Thiết kế mà không viết access pattern ra giấy. Không có bảng "ai đọc gì, bao nhiêu lần" thì mọi tranh luận embed hay reference chỉ là cảm tính.
Cột mốc: Bạn đã có thể đặt cạnh một hệ thống đơn hàng ở MongoDB và PostgreSQL, và tránh những lỗi hay gặp như dịch thẳng schema SQL hay embed mọi thứ. Bài Data Modeling khép lại ở đây.
Hỏi & đáp
Một nhóm chuyển hệ thống đơn hàng sang PostgreSQL, khai báo FOREIGN KEY từ order_items.order_id sang orders và cho rằng vậy là đủ để màn chi tiết đơn nhanh. Điều gì đúng?
Báo cáo "top sản phẩm tháng" và nhiều câu hỏi ad-hoc theo trục chưa biết trước. Mô hình nào hợp hơn, theo bảng so sánh của bài?
Bài tiếp theo
Bài này dạy cách chọn: embed hay reference, dựa trên access pattern và cardinality của quan hệ. Trên đường đi, ta đã dùng vài cách làm mà chưa gọi tên: copy name và phone của khách vào đơn, tính sẵn total lúc tạo đơn, giữ một mảng có trần cho vài phần tử gần nhất. Bài BSON & ObjectId cho biết mỗi field tốn bao nhiêu byte; bài này cho thấy document to dần lên khi bạn embed. Còn ba câu hỏi bài này cố tình để lại:
- Những cấu trúc document hay gặp có tên là gì, mỗi cái giải vấn đề nào, tốn gì, và khi nào không nên dùng?
- Khi schema phải đổi trong lúc hệ thống đang chạy (thêm field, đổi kiểu, tách một field thành object), làm sao để document cũ và mới cùng sống được mà không cần downtime?
- Document to thì tốn gì cụ thể: một document 10 KB, 500 KB và 10 MB khác nhau bao nhiêu về latency, chỗ trong cache, byte qua mạng và công deserialize ở client?
Schema Design Patterns & Schema Evolution trả lời cả ba, và đo câu hỏi cuối bằng thí nghiệm thay vì đoán.
Tài liệu tham khảo
- MongoDB Manual: Data Modeling
- MongoDB Manual: Embedded Data Versus References / Data Modeling Best Practices
- MongoDB Manual: Handle Duplicate Data
- MongoDB Manual: Model One-to-Many Relationships with Document References
- MongoDB Manual: Avoid Unbounded Arrays
- MongoDB Manual: Reduce $lookup Operations
- MongoDB Manual: $lookup (aggregation stage)
- MongoDB Manual: db.collection.updateMany()
- MongoDB Manual: MongoDB Limits and Thresholds
- MongoDB Blog: 6 Rules of Thumb for MongoDB Schema Design
- PostgreSQL Documentation: Constraints (Foreign Keys)