Menu
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Đang tải...

Bài 59 — Industry Case Studies

Data Modeling and ERD Mastery Bài 59/60

Mở đầu — vì sao bài này quan trọng

Suốt gần 60 bài học, chúng ta đã cùng nhau đi qua từng viên gạch của nghề data modeling: từ entity, attribute, primary key, cho đến chuẩn hóa (normalization), star schema, và cả những chủ đề đặc thù Việt Nam như địa chỉ, tên người, hay tiền tệ. Nhưng có một khoảng cách rất lớn giữa việc "biết vẽ ERD đúng lý thuyết" và việc "thiết kế được mô hình dữ liệu cho một hệ thống thật, đang chạy, phục vụ hàng chục triệu người dùng".

Bài này lấp khoảng cách đó bằng cách mổ xẻ những case study công nghiệp thật — các hệ thống mà bạn dùng hằng ngày (ví điện tử, thương mại điện tử, ngân hàng số). Chúng ta sẽ không chỉ ngắm sơ đồ đẹp; chúng ta sẽ hỏi những câu mà một Data BA thực thụ phải hỏi: Tại sao họ tách bảng như vậy? Tại sao có chỗ họ cố tình denormalize? Quyết định mô hình nào đã cứu họ khi hệ thống scale lên 50 triệu user, và quyết định nào từng khiến họ phải trả giá?

Giá trị lớn nhất của việc học qua case study là bạn học được cách suy nghĩ đánh đổi (trade-off) — thứ không sách giáo khoa nào dạy trọn vẹn. Một mô hình dữ liệu tốt trong thực tế hiếm khi là mô hình "đúng chuẩn 3NF hoàn hảo"; nó là mô hình cân bằng giữa tính toàn vẹn, hiệu năng, chi phí vận hành và tốc độ thay đổi của nghiệp vụ.

Khái niệm cốt lõi

Case study công nghiệp là gì và đọc nó thế nào

Một case study về data modeling không phải là "bức tranh tĩnh" của schema, mà là câu chuyện về những quyết định. Khi phân tích một hệ thống thật, bạn nên bóc tách theo bốn lớp:

  • Bối cảnh nghiệp vụ (business context): Công ty kinh doanh gì, quy mô người dùng, khối lượng giao dịch, ràng buộc pháp lý.
  • Yêu cầu phi chức năng (non-functional requirements): Cần đọc nhanh hay ghi nhanh? Cần nhất quán tuyệt đối (strong consistency) hay chấp nhận nhất quán cuối (eventual consistency)? Cần lưu lịch sử bao lâu?
  • Quyết định mô hình (modeling decisions): Chọn entity nào, tách/gộp bảng ra sao, dùng surrogate key hay natural key, denormalize ở đâu.
  • Hệ quả và bài học (consequences): Quyết định đó mang lại lợi ích gì, gây ra nợ kỹ thuật gì.

Ba mẫu hình lặp lại trong hệ thống quy mô lớn

Qua nhiều hệ thống, bạn sẽ thấy ba mẫu hình lặp đi lặp lại mà một BA cần nhận ra:

Tách "sổ cái" khỏi "trạng thái hiện tại" (ledger vs. balance). Trong hệ thống tài chính, không ai lưu số dư ví như một con số có thể update tùy tiện. Thay vào đó họ lưu một bảng giao dịch (transaction ledger) chỉ được thêm mới (append-only), và số dư là kết quả cộng dồn. Đây là ứng dụng trực tiếp của tư duy audit table và temporal data mà chúng ta đã học.

Tách dữ liệu "nóng" khỏi dữ liệu "nguội" (hot vs. cold). Đơn hàng đang xử lý cần đọc/ghi cực nhanh; đơn hàng của 3 năm trước chỉ cần cho báo cáo. Nhiều hệ thống mô hình hóa hai loại này khác nhau, thậm chí đưa vào hai kho khác nhau (OLTP vs. reporting database).

Mô hình đa dịch vụ (polyglot / service boundaries). Một siêu ứng dụng (super-app) không có một schema khổng lồ duy nhất; mỗi dịch vụ (thanh toán, ví, cho vay) có mô hình dữ liệu riêng, và chúng liên kết với nhau qua khóa tham chiếu chứ không qua khóa ngoại (foreign key) cứng.

Tình huống thực tế

Case 1: MoMo — Ví điện tử Việt Nam

Bối cảnh: MoMo là ứng dụng thanh toán di động lớn nhất Việt Nam, với hơn 50 triệu người dùng (2026) và hàng chục dịch vụ: chuyển tiền P2P, thanh toán hóa đơn, nạp điện thoại, ví trả sau, đầu tư. Mỗi ngày hệ thống xử lý hàng chục triệu giao dịch, và mỗi giao dịch liên quan đến tiền thật — sai một đồng cũng không được chấp nhận.

Quyết định mô hình: Trái tim của MoMo không phải là bảng wallet với cột balance. Nếu bạn thiết kế Wallet(wallet_id, user_id, balance) và mỗi lần giao dịch thì UPDATE balance, bạn sẽ gặp thảm họa: không truy vết được lịch sử, dễ sai khi có hai giao dịch đồng thời (race condition), và không đối soát được với ngân hàng.

Cách làm đúng là mô hình double-entry ledger (sổ cái kép), lấy cảm hứng từ kế toán:

  • Account(account_id PK, user_id FK, account_type, currency, created_at) — mỗi user có thể có nhiều tài khoản logic (ví chính, ví khuyến mãi, ví trả sau).
  • LedgerEntry(entry_id PK, transaction_id FK, account_id FK, direction ENUM('debit','credit'), amount BIGINT, created_at) — bảng chỉ thêm mới, không bao giờ sửa/xóa.
  • Transaction(transaction_id PK, type, status, reference_code, created_at) — mỗi giao dịch phát sinh ít nhất hai LedgerEntry (một bên ghi nợ, một bên ghi có), và tổng chúng luôn bằng 0.
Số dư ví không được lưu như "sự thật gốc" (source of truth) mà được tính từ tổng các LedgerEntry; để đọc nhanh, họ denormalize một bảng AccountBalance(account_id, balance, last_entry_id) cập nhật đồng bộ trong cùng transaction. Chú ý amount dùng BIGINT lưu theo đơn vị nhỏ nhất (đồng), không dùng FLOAT — đúng như bài Money & Currency đã nhấn mạnh.

Bài học rút ra: Với dữ liệu tài chính, ưu tiên mô hình append-only (ledger) hơn mô hình update-in-place. Denormalize số dư là hợp lệ và cần thiết, nhưng phải luôn có "nguồn sự thật" là ledger để đối soát lại được. Đây là ví dụ kinh điển cho thấy denormalization không phải là "làm bẩn thiết kế" mà là một quyết định kỹ thuật có chủ đích.

Case 2: Shopee — Thương mại điện tử Đông Nam Á

Bối cảnh: Shopee vận hành ở nhiều quốc gia Đông Nam Á với hàng trăm triệu sản phẩm và hàng triệu người bán. Thách thức mô hình hóa lớn nhất của họ không phải là đơn hàng, mà là catalog sản phẩm — vì mỗi ngành hàng có tập thuộc tính hoàn toàn khác nhau: điện thoại có RAM/màn hình/pin, quần áo có size/màu/chất liệu, thực phẩm có hạn sử dụng.

Quyết định mô hình: Đây là bài toán kinh điển "thuộc tính động". Có ba hướng, và Shopee dùng một mô hình lai:

  • Với các thuộc tính cốt lõi mọi sản phẩm đều có (tên, giá, mô tả, người bán), họ dùng cột cứng chuẩn hóa: Product(product_id, seller_id, title, base_price, category_id, ...).
  • Với biến thể (variant) như size–màu, họ dùng bảng riêng: ProductVariant(variant_id, product_id, sku, price, stock_qty)VariantOption — mô hình quan hệ chuẩn cho quan hệ many-to-many giữa sản phẩm và thuộc tính.
  • Với các thuộc tính đặc thù theo ngành hàng (chỉ áp dụng cho một số category), họ dùng cột JSON attributes để lưu semi-structured data thay vì tạo hàng trăm cột NULL hoặc dùng mô hình EAV (Entity-Attribute-Value) khó truy vấn.
Về đơn hàng, Shopee cố tình denormalize snapshot giá và tên sản phẩm vào bảng OrderItem(order_id, product_id, product_title_snapshot, unit_price_snapshot, quantity). Lý do: khi người bán đổi giá hay đổi tên sản phẩm sau này, đơn hàng cũ phải giữ nguyên thông tin tại thời điểm mua. Nếu chỉ để foreign key trỏ về Product, hóa đơn quá khứ sẽ bị "biến dạng" theo dữ liệu hiện tại — một lỗi nghiệp vụ nghiêm trọng.

Bài học rút ra: Không có một câu trả lời "đúng chuẩn" cho thuộc tính động — hãy phối hợp cột cứng cho phần ổn định, bảng quan hệ cho phần cần truy vấn/lọc, và JSON cho phần thưa và hay thay đổi. Đồng thời, snapshot dữ liệu giao dịch là kỹ thuật bắt buộc trong mọi hệ thống có hóa đơn.

Case 3: Ngân hàng số (Timo/TPBank) — Tài khoản và đối soát

Bối cảnh: Một ngân hàng số Việt Nam như Timo (hợp tác với ngân hàng nền tảng) phải mô hình dữ liệu tuân thủ nghiêm ngặt quy định của Ngân hàng Nhà nước: mọi thay đổi tài khoản phải truy vết được, dữ liệu khách hàng phải tuân thủ PDPL (Luật Bảo vệ dữ liệu cá nhân), và phải đối soát chính xác cuối ngày với hệ thống core banking.

Quyết định mô hình: Điểm khác biệt so với ví điện tử là yêu cầu temporal (dữ liệu theo thời gian) rất mạnh. Họ dùng mô hình bi-temporal cho một số bảng: mỗi bản ghi khách hàng lưu cả valid_from/valid_to (thời điểm dữ liệu có hiệu lực về mặt nghiệp vụ) lẫn recorded_at (thời điểm hệ thống ghi nhận). Nhờ vậy họ trả lời được câu hỏi kiểm toán: "Địa chỉ khách hàng này vào ngày 1/1/2025 là gì, và ngày nào chúng ta biết điều đó?"

Với dữ liệu nhạy cảm (số CCCD, thông tin định danh), theo nguyên tắc Privacy by Design họ tách thành entity riêng CustomerPII với kiểm soát truy cập chặt, mã hóa ở tầng lưu trữ, và chỉ liên kết với Customer qua khóa — thay vì trộn lẫn PII vào bảng chính mà cả hệ thống đều đọc được.

Bài học rút ra: Ràng buộc pháp lý và kiểm toán không phải là "phần bổ sung" mà là yếu tố định hình mô hình dữ liệu ngay từ đầu. Một BA giỏi phải hỏi về yêu cầu tuân thủ trước khi vẽ entity đầu tiên.

Hướng dẫn từng bước

Khi bạn được giao phân tích (hoặc thiết kế) mô hình dữ liệu cho một hệ thống công nghiệp, hãy đi theo quy trình sau:

  • Xác định bối cảnh và quy mô. Hỏi rõ: bao nhiêu user, bao nhiêu giao dịch mỗi ngày, tỷ lệ đọc/ghi, cần lưu lịch sử bao lâu. Con số quyết định kiến trúc — mô hình cho 1.000 user khác hẳn cho 50 triệu user.
  • Liệt kê yêu cầu phi chức năng và ràng buộc pháp lý. Nhất quán mạnh hay nhất quán cuối? Có dữ liệu tài chính cần đối soát không? Có PII cần tuân thủ PDPL không? Ghi lại trước khi vẽ.
  • Nhận diện "sổ cái" tiềm ẩn. Bất kỳ chỗ nào có tiền, điểm thưởng, tồn kho, hay trạng thái quan trọng thay đổi liên tục — cân nhắc mô hình append-only ledger thay vì update-in-place.
  • Phân biệt dữ liệu tham chiếu và dữ liệu giao dịch. Với mọi hóa đơn/đơn hàng, xác định trường nào cần snapshot để không bị biến dạng theo dữ liệu gốc thay đổi về sau.
  • Xử lý thuộc tính động một cách có chủ đích. Quyết định phần nào là cột cứng, phần nào là bảng quan hệ, phần nào là JSON — dựa trên việc thuộc tính đó có cần lọc/tổng hợp hay không.
  • Xác định ranh giới denormalization. Chỉ denormalize khi có lý do hiệu năng cụ thể, và luôn giữ được nguồn sự thật để tái tạo/đối soát.
  • Kiểm chứng ngược bằng truy vấn thực tế. Viết ra 5–7 câu hỏi nghiệp vụ quan trọng nhất (ví dụ "số dư ví của user X lúc 20:00 hôm qua"), rồi kiểm tra mô hình có trả lời được không, nhanh không.

Lỗi thường gặp & mẹo

Lỗi 1 — Lưu tiền/số dư như một con số update được. Đây là lỗi phổ biến nhất của người mới. Update-in-place mất lịch sử, dễ race condition, không đối soát được. Mẹo: hễ thấy tiền hoặc trạng thái quan trọng, nghĩ ngay đến ledger.

Lỗi 2 — Foreign key trỏ thẳng cho dữ liệu hóa đơn. Để OrderItem chỉ trỏ tới Product.price khiến hóa đơn cũ đổi giá theo hiện tại. Mẹo: snapshot các giá trị nghiệp vụ quan trọng vào chính bảng giao dịch.

Lỗi 3 — Lạm dụng JSON hoặc lạm dụng EAV. Nhét mọi thứ vào JSON làm mất khả năng ràng buộc và lọc; dùng EAV cho mọi thuộc tính làm truy vấn khủng khiếp. Mẹo: JSON chỉ cho thuộc tính thưa, ít truy vấn; cột cứng cho thứ cần lọc thường xuyên.

Lỗi 4 — Bỏ qua yêu cầu pháp lý và audit đến cuối mới thêm. Gắn audit/temporal vào sau cùng thường phải đập đi làm lại. Mẹo: hỏi về tuân thủ và kiểm toán ngay ở bước 1.

Lỗi 5 — Sao chép mù mô hình của công ty lớn. Mô hình của MoMo hay Shopee phù hợp với quy mô của họ; áp nguyên xi cho một startup nhỏ là over-engineering. Mẹo: hiểu lý do đằng sau quyết định, rồi cân nhắc với bối cảnh của bạn.

Mẹo tổng quát: Khi phân vân, hãy tự hỏi "quyết định này phục vụ đọc nhanh, ghi nhanh, đối soát, hay tuân thủ?" — mỗi quyết định mô hình đều nên chỉ đích danh được mục tiêu nó phục vụ.

Bài tập thực hành

Bài tập 1 — Thiết kế ledger cho ví khuyến mãi. Giả sử MoMo muốn thêm "ví xu thưởng" (loyalty points) tách khỏi ví tiền thật, xu có hạn sử dụng 90 ngày. Hãy phác thảo các entity cần thiết (gợi ý: tài khoản xu, các lô xu theo hạn dùng, ledger entry) và giải thích cách tính số dư xu còn hạn tại một thời điểm.

Bài tập 2 — Mô hình catalog đa ngành hàng. Cho ba ngành hàng: điện thoại, quần áo, thực phẩm tươi. Hãy quyết định thuộc tính nào để cột cứng, thuộc tính nào để bảng variant, thuộc tính nào để JSON. Viết 3 câu truy vấn nghiệp vụ (ví dụ "lọc điện thoại RAM ≥ 8GB") và kiểm tra mô hình của bạn trả lời được không.

Bài tập 3 — Phản biện một quyết định snapshot. Trong OrderItem, ngoài giá và tên, bạn còn nên snapshot những trường nào (thuế, chương trình giảm giá, tên người bán)? Với mỗi trường, lập luận vì sao snapshot hay không snapshot, dựa trên câu hỏi "trường này có thể thay đổi sau khi đặt hàng không, và điều đó có làm sai hóa đơn không".

Bài tập 4 — Rà soát tuân thủ. Lấy một trong ba case ở trên, liệt kê các loại dữ liệu PII và đề xuất cách tách/mã hóa/kiểm soát truy cập theo nguyên tắc Privacy by Design.

Tóm tắt

  • Case study công nghiệp không phải là schema tĩnh mà là câu chuyện về các quyết định đánh đổi; hãy đọc theo bốn lớp: bối cảnh, yêu cầu phi chức năng, quyết định mô hình, hệ quả.
  • MoMo dạy ta mô hình double-entry ledger cho dữ liệu tài chính: append-only, số dư là kết quả cộng dồn, denormalize có chủ đích nhưng luôn giữ nguồn sự thật.
  • Shopee dạy ta xử lý thuộc tính động bằng cách lai cột cứng + bảng quan hệ + JSON, và bắt buộc snapshot dữ liệu hóa đơn để không bị biến dạng.
  • Ngân hàng số dạy ta rằng ràng buộc pháp lý, audit và temporal data định hình mô hình ngay từ đầu, không phải phần thêm sau.
  • Ba mẫu hình lặp lại đáng nhớ: ledger vs. balance, hot vs. cold, và ranh giới dịch vụ (mỗi service một mô hình, liên kết qua khóa tham chiếu).
  • Nguyên tắc vàng: mỗi quyết định mô hình phải chỉ đích danh được nó phục vụ mục tiêu nào — đọc nhanh, ghi nhanh, đối soát, hay tuân thủ — và đừng sao chép mù mô hình của công ty lớn khi bối cảnh của bạn khác.