Data Modeling (P3/3): So với PostgreSQL và lỗi thường gặp

5 phút đọcSeries: MongoDB: từ gốc đến internals

Ở 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.

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.

WorkloadMô 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 idnhiều lần index lookup cộng join✓ một document
Tạo đơn trọn vẹntransaction 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ề.
  • $lookup mà 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ó. updateMany quét cả collection để sửa 10 document.
  • Quên rằng updateMany khô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?

  1. Đúng, PostgreSQL tự tạo index khi khai báo foreign key

    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. Xem mục "MongoDB vs PostgreSQL: cùng hệ thống đơn hàng".

  2. Chỉ MongoDB mới cần index phía bị join, PostgreSQL tự lo

    Bài học "index ở phía bị join" không phải chuyện riêng của MongoDB: 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. Xem mục "MongoDB vs PostgreSQL: cùng hệ thống đơn hàng".

  3. Không cần index, vì JOIN của PostgreSQL luôn nhanh hơn $lookup

    Bài không đo PostgreSQL, nên không có con số nào cho khẳng định đó; so sánh hiệu năng định lượng giữa hai hệ để dành cho bài khác, với cùng dữ liệu và điều kiện. Xem mục "MongoDB vs PostgreSQL: cùng hệ thống đơn hàng".

  4. Foreign key không tự tạo index; thiếu index ở order_items(order_id) thì join vẫn phải tìm cách quét bảng

    Đúng: tài liệu PostgreSQL ghi rõ foreign key không tự tạo index trên cột tham chiếu, nên bài học index ở phía bị join của Experiment 1 áp dụng cho cả hai hệ. Xem mục "MongoDB vs PostgreSQL: cùng hệ thống đơn hà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?

  1. Mô hình document, vì cả đơn đã nằm trong một document

    Mô hình document tối ưu cho vài câu hỏi bạn đã biết. Với báo cáo này phải $unwind items của mọi đơn, hoặc tính sẵn số liệu mỗi khi có đơn mới; trục chưa được thiết kế sẵn thì tốn hơn. Xem mục "MongoDB vs PostgreSQL: cùng hệ thống đơn hàng".

  2. Hai mô hình như nhau, vì chỉ khác cách lưu chứ không khác chi phí

    Bảng so sánh cho thấy chi phí khác nhau theo workload: đọc trọn một đơn thì document có lợi, còn báo cáo và câu hỏi ad-hoc thì mô hình chuẩn hóa có lợi. Xem mục "MongoDB vs PostgreSQL: cùng hệ thống đơn hàng".

  3. Mô hình chuẩn hóa, vì tối ưu cho tính đúng và sự linh hoạt

    Báo cáo chạy bằng quét order_items và GROUP BY, câu hỏi ad-hoc dùng JOIN linh hoạt. Mô hình chuẩn hóa tối ưu cho tính đúng khi chưa biết hết câu hỏi tương lai. Xem mục "MongoDB vs PostgreSQL: cùng hệ thống đơn hàng".

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