Menu
ESC

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

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

Đang tải...

Bài 38 — Hands-on Lab: ERD cho Insurance VN

Data Modeling and ERD Mastery Bài 38/60

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

Nếu bạn từng làm việc với một công ty bảo hiểm ở Việt Nam — Bảo Việt, PVI, Prudential, Manulife hay AIA — bạn sẽ nhận ra ngay rằng đây là một trong những nghiệp vụ có mô hình dữ liệu phức tạp nhất mà một Business Analyst có thể gặp. Lý do rất đơn giản: một công ty bảo hiểm không bán "một sản phẩm", mà bán hàng chục dòng sản phẩm khác nhau — bảo hiểm xe cơ giới (auto), bảo hiểm nhân thọ (life), bảo hiểm sức khỏe (health) — mỗi loại có cấu trúc thông tin, quy tắc tính phí, và cách bồi thường hoàn toàn khác nhau. Vậy mà tất cả phải cùng tồn tại trong một hệ thống, cùng gắn với một khách hàng, cùng chảy qua một quy trình thu phí và bồi thường.

Đây chính là lý do bài lab này quan trọng. Nó buộc bạn phải vận dụng gần như mọi thứ đã học trong khóa: entity và attribute, primary key (bao gồm surrogate key), quan hệ một-nhiều và nhiều-nhiều, subtype/supertype để mô hình hóa các dòng sản phẩm khác nhau, và cả audit/temporal data vì hợp đồng bảo hiểm có vòng đời theo thời gian. Nói cách khác, ERD bảo hiểm là một "đề thi tổng hợp" thực chiến.

Trong bài này chúng ta sẽ đi từ đề bài (scenario) đến một ERD hoàn chỉnh, từng bước một, với đủ quyết định thiết kế và lý do đằng sau mỗi quyết định. Mục tiêu không phải là bạn thuộc lòng sơ đồ của tôi, mà là bạn học được cách tư duy để tự dựng ERD cho bất kỳ công ty bảo hiểm nào.

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

Bối cảnh nghiệp vụ bảo hiểm cần nắm trước khi vẽ

Trước khi đụng vào entity, bạn phải hiểu vài khái niệm nghiệp vụ. Nếu không, bạn sẽ đặt tên bảng sai và mô hình hóa sai quan hệ.

  • Policyholder (Bên mua bảo hiểm / Chủ hợp đồng): người đứng tên ký hợp đồng và trả phí. Đây thường là một khách hàng (cá nhân hoặc doanh nghiệp).
  • Insured (Người được bảo hiểm): đối tượng được bảo vệ. Trong bảo hiểm nhân thọ, người mua và người được bảo hiểm có thể khác nhau — ví dụ cha mua bảo hiểm cho con. Đây là điểm bẫy kinh điển mà người mới hay bỏ sót.
  • Beneficiary (Người thụ hưởng): người nhận tiền khi sự kiện bảo hiểm xảy ra (đặc biệt trong bảo hiểm nhân thọ).
  • Insurance Product (Sản phẩm bảo hiểm): định nghĩa loại bảo hiểm được bán — "Bảo hiểm vật chất xe ô tô", "An Phát Trọn Đời", "Bảo hiểm sức khỏe Bảo An"...
  • Policy (Hợp đồng / Đơn bảo hiểm): một hợp đồng cụ thể của một policyholder mua một product, có số hợp đồng, ngày hiệu lực, ngày hết hạn, số tiền bảo hiểm (sum insured), và phí (premium).
  • Premium (Phí bảo hiểm): khoản tiền khách hàng trả, có thể trả một lần hoặc theo kỳ (tháng/quý/năm).
  • Claim (Yêu cầu bồi thường): khi rủi ro xảy ra, khách hàng nộp claim; công ty thẩm định rồi chi trả (payout).

Các entity cốt lõi và trục thiết kế

Xương sống của ERD bảo hiểm gồm sáu nhóm entity: CUSTOMER (khách hàng, đóng cả vai policyholder / insured / beneficiary), INSURANCE_PRODUCT (danh mục sản phẩm), POLICY (hợp đồng), PREMIUM_PAYMENT (thanh toán phí), CLAIM (bồi thường), và AGENT (đại lý/tư vấn viên bán hợp đồng).

Trục thiết kế khó nhất chính là chỗ này: auto, life, health khác nhau ở đâu? Một hợp đồng bảo hiểm xe cần lưu biển số, hãng xe, năm sản xuất, giá trị xe. Một hợp đồng nhân thọ cần lưu người được bảo hiểm, người thụ hưởng, thời hạn đóng phí dài hạn. Một hợp đồng sức khỏe cần lưu gói quyền lợi, hạn mức bồi thường theo năm, bệnh có sẵn (pre-existing conditions). Ba nhóm thuộc tính này gần như không giao nhau.

Có ba chiến lược mô hình hóa, và bạn phải chọn:

  • Single table (một bảng POLICY khổng lồ) với tất cả cột của cả ba loại — đơn giản nhưng đầy cột NULL, khó bảo trì.
  • Subtype/Supertype (bảng cha POLICY + ba bảng con)POLICY giữ thuộc tính chung, còn AUTO_POLICY, LIFE_POLICY, HEALTH_POLICY giữ thuộc tính riêng, liên kết 1-1 với bảng cha qua chung policy_id. Đây là lựa chọn chuẩn hóa nhất và thường là câu trả lời "đúng" trong môi trường học/thi.
  • JSON/EAV — nhét thuộc tính riêng vào một cột JSON product_attributes. Linh hoạt nhưng mất tính ràng buộc và khó query báo cáo.
Trong lab này chúng ta chọn chiến lược subtype/supertype vì nó thể hiện rõ tư duy data modeling và là cách các hệ thống bảo hiểm nghiêm túc thực sự làm.

Quan hệ then chốt

  • Một CUSTOMER có nhiều POLICY → quan hệ one-to-many.
  • Một INSURANCE_PRODUCT được bán trong nhiều POLICYone-to-many.
  • Một POLICY có nhiều PREMIUM_PAYMENTone-to-many.
  • Một POLICY có nhiều CLAIMone-to-many.
  • Một POLICY có thể có nhiều BENEFICIARY, và một người có thể là thụ hưởng của nhiều hợp đồng → many-to-many, cần bảng trung gian POLICY_BENEFICIARY kèm cột benefit_percentage (tỷ lệ thụ hưởng).
  • Một AGENT bán nhiều POLICYone-to-many.

Tình huống thực tế

Tình huống 1 — Bảo Minh và cái bẫy "một bảng cho tất cả"

Một nhóm dự án xây hệ thống mới cho một công ty phi nhân thọ (giả định dựa trên mô hình Bảo Minh) quyết định làm nhanh: một bảng policy duy nhất với 60 cột, gộp cả bảo hiểm xe, bảo hiểm nhà và bảo hiểm du lịch. Với hợp đồng xe, các cột travel_destination, trip_start_date để trống; với hợp đồng du lịch, các cột license_plate, vehicle_value để trống.

Sau sáu tháng, hệ thống có 400.000 hợp đồng và bắt đầu vỡ: đội báo cáo không thể viết truy vấn "tổng giá trị xe được bảo hiểm" mà không lọc thủ công loại sản phẩm; khi thêm dòng sản phẩm bảo hiểm thú cưng, họ phải ALTER TABLE thêm 8 cột nữa vào bảng đã 60 cột; và validation trở thành ác mộng vì không thể đặt NOT NULL cho license_plate (vì hợp đồng du lịch không có).

Bài học rút ra: khi các "loại" của một entity có tập thuộc tính khác nhau rõ rệt, single table dẫn tới rừng cột NULL và mất khả năng ràng buộc. Tách subtype giúp mỗi loại có schema riêng, đặt NOT NULL đúng chỗ, và thêm sản phẩm mới chỉ là thêm một bảng con — không đụng bảng cha.

Tình huống 2 — Prudential Việt Nam và người được bảo hiểm không phải người mua

Trong một hợp đồng nhân thọ tại Prudential, chị Lan (35 tuổi) mua sản phẩm "PRU-An Vui" cho con trai 8 tuổi, và chỉ định chồng làm người thụ hưởng nếu có rủi ro. Ở đây có ba vai trò trên ba con người khác nhau: policyholder = chị Lan, insured = con trai, beneficiary = chồng.

Một BA thiếu kinh nghiệm sẽ chỉ tạo cột customer_id trên bảng policy và coi như xong — tức là gộp cả ba vai trò làm một. Khi bộ phận nghiệp vụ hỏi "cho tôi danh sách tất cả trẻ em đang được bảo hiểm dưới 15 tuổi", hệ thống chịu, vì nó không hề phân biệt người mua với người được bảo hiểm.

Cách đúng: bảng POLICYpolicyholder_id trỏ tới CUSTOMER; thêm policy_insured_id (hoặc bảng POLICY_INSURED nếu một hợp đồng có nhiều người được bảo hiểm, như bảo hiểm sức khỏe gia đình); và bảng trung gian POLICY_BENEFICIARY với tỷ lệ thụ hưởng. Cùng một bảng CUSTOMER được tham chiếu bởi nhiều vai trò — đây là ví dụ đẹp của self-referencing thông qua role mà bạn từng gặp ở các bài trước.

Bài học rút ra: đừng để giả định "người mua chính là người được bảo vệ" ăn sâu vào schema. Hãy tách vai trò (role) khỏi con người (party). Đây là nguyên tắc "Party Model" kinh điển trong bảo hiểm và ngân hàng.

Tình huống 3 — Công ty health-tech và bài toán lịch sử phí

Một startup insurtech ở TP.HCM bán bảo hiểm sức khỏe theo tháng qua app. Ban đầu họ để premium_amountpayment_status ngay trên bảng policy. Mỗi tháng khi khách trả tiền, họ UPDATE đè lên giá trị cũ. Đến khi kế toán cần đối soát "khách này tháng 3 đã trả chưa, trả bao nhiêu, ngày nào" thì dữ liệu đã bị ghi đè — không còn dấu vết.

Họ phải làm lại: tách bảng PREMIUM_PAYMENT riêng, mỗi kỳ đóng phí là một dòng với payment_id, policy_id, period (ví dụ 2026-03), amount, paid_date, status, payment_method. Nhờ vậy một hợp đồng có nhiều dòng thanh toán, giữ được toàn bộ lịch sử, và báo cáo doanh thu theo tháng trở nên đơn giản.

Bài học rút ra: những dữ liệu lặp lại theo thời gian (thanh toán, bồi thường, gia hạn) không bao giờ nên là cột trên bảng chính bị ghi đè. Chúng là entity riêng với quan hệ one-to-many. Mất lịch sử trong ngành bảo hiểm không chỉ bất tiện — nó có thể vi phạm yêu cầu kiểm toán và pháp lý.

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

Bây giờ ta dựng ERD hoàn chỉnh. Tôi dùng cú pháp Mermaid để bạn có thể dán trực tiếp vào dbdiagram hoặc công cụ hỗ trợ Mermaid mà chạy được.

Bước 1 — Liệt kê entity và phân nhóm. Chia làm ba nhóm: Party (CUSTOMER, AGENT), Product & Contract (INSURANCE_PRODUCT, POLICY, AUTO_POLICY, LIFE_POLICY, HEALTH_POLICY), Transaction (PREMIUM_PAYMENT, CLAIM, POLICY_BENEFICIARY).

Bước 2 — Định danh khóa chính. Dùng surrogate key (BIGINT AUTO_INCREMENT hoặc UUID) cho mọi entity: customer_id, policy_id... Riêng số hợp đồng hiển thị cho khách (policy_number) là natural key mang tính nghiệp vụ, để riêng và đặt UNIQUE — không dùng làm PK vì nó có thể đổi format theo thời gian.

Bước 3 — Vẽ supertype POLICY và các subtype. Ba bảng con dùng chung policy_id làm cả PK lẫn FK trỏ về POLICY (quan hệ 1-1). Thêm cột product_type trên POLICY làm "discriminator" để biết một hợp đồng thuộc subtype nào.

Bước 4 — Xử lý beneficiary bằng bảng trung gian. Vì many-to-many, tạo POLICY_BENEFICIARY(policy_id, customer_id, benefit_percentage, relationship).

Bước 5 — Ráp lại thành ERD:

erDiagram
    CUSTOMER ||--o{ POLICY : "sở hữu (policyholder)"
    AGENT ||--o{ POLICY : "bán"
    INSURANCE_PRODUCT ||--o{ POLICY : "được ký theo"
    POLICY ||--o{ PREMIUM_PAYMENT : "phát sinh"
    POLICY ||--o{ CLAIM : "có"
    POLICY ||--o| AUTO_POLICY : "chi tiết xe"
    POLICY ||--o| LIFE_POLICY : "chi tiết nhân thọ"
    POLICY ||--o| HEALTH_POLICY : "chi tiết sức khỏe"
    POLICY ||--o{ POLICY_BENEFICIARY : "chỉ định"
    CUSTOMER ||--o{ POLICY_BENEFICIARY : "thụ hưởng"

CUSTOMER { bigint customer_id PK string full_name string national_id "CCCD/CMND" date date_of_birth string phone string email string customer_type "individual/corporate" } AGENT { bigint agent_id PK string full_name string agent_code string branch } INSURANCE_PRODUCT { bigint product_id PK string product_name string product_type "auto/life/health" string description boolean is_active } POLICY { bigint policy_id PK string policy_number UK bigint policyholder_id FK bigint product_id FK bigint agent_id FK string product_type "discriminator" date start_date date end_date decimal sum_insured decimal premium_amount string premium_frequency "monthly/quarterly/annual" string status "active/lapsed/expired/cancelled" } AUTO_POLICY { bigint policy_id PK,FK string license_plate string vehicle_make string vehicle_model int manufacture_year decimal vehicle_value } LIFE_POLICY { bigint policy_id PK,FK bigint insured_id FK int coverage_term_years int premium_paying_term } HEALTH_POLICY { bigint policy_id PK,FK string benefit_plan decimal annual_limit boolean pre_existing_covered } PREMIUM_PAYMENT { bigint payment_id PK bigint policy_id FK string period "2026-03" decimal amount date due_date date paid_date string status "pending/paid/overdue" string payment_method } CLAIM { bigint claim_id PK bigint policy_id FK date incident_date date reported_date string description decimal claimed_amount decimal approved_amount string status "submitted/assessing/approved/rejected/paid" } POLICY_BENEFICIARY { bigint policy_id PK,FK bigint customer_id PK,FK decimal benefit_percentage string relationship }

Bước 6 — Rà ràng buộc nghiệp vụ. Ví dụ: end_date > start_date; tổng benefit_percentage của một policy_id phải bằng 100; approved_amount <= claimed_amount; AUTO_POLICY chỉ tồn tại khi POLICY.product_type = 'auto'. Những quy tắc này bạn ghi lại trong data dictionary, một phần triển khai bằng CHECK constraint, một phần bằng logic ứng dụng.

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

  • Gộp mọi vai trò con người vào một FK. Như tình huống Prudential: hãy tách policyholder, insured, beneficiary. Đây là lỗi số một.
  • Nhồi mọi loại sản phẩm vào một bảng đầy cột NULL. Dùng subtype/supertype khi thuộc tính khác nhau rõ rệt; hoặc cân nhắc JSON nếu thuộc tính quá biến động và bạn ít cần query theo chúng.
  • Ghi đè dữ liệu giao dịch. Premium và claim phải là entity riêng, one-to-many, giữ nguyên lịch sử.
  • Dùng policy_number làm khóa chính. Natural key nghiệp vụ dễ thay đổi format (đổi hệ thống, đổi chi nhánh). Dùng surrogate key làm PK, để policy_number là UNIQUE.
  • Quên discriminator. Cột product_type trên POLICY giúp biết cần join sang subtype nào mà không phải thử cả ba.
  • Mẹo về beneficiary_percentage. Đặt CHECK hoặc trigger để tổng tỷ lệ = 100 — ngành bảo hiểm cực kỳ nhạy cảm với việc "ai nhận bao nhiêu phần trăm".
  • Mẹo về temporal. Thêm created_at, updated_at cho mọi bảng, và cân nhắc bảng lịch sử POLICY_HISTORY cho những thay đổi hợp đồng (endorsement) — vì hợp đồng bảo hiểm có thể sửa đổi giữa chừng.

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

  • Mở rộng subtype: Công ty muốn thêm dòng bảo hiểm du lịch (travel). Hãy thiết kế bảng TRAVEL_POLICY (điểm đến, ngày đi, ngày về, số người) và chỉ ra bạn cần sửa gì ở INSURANCE_PRODUCT và discriminator — không được đụng vào ba subtype cũ.
  • Party Model đầy đủ: Vẽ lại phần con người theo mô hình Party thuần: một bảng PARTY và một bảng POLICY_PARTY_ROLE(policy_id, party_id, role_type) với role_type ∈ {policyholder, insured, beneficiary}. So sánh ưu/nhược điểm với thiết kế FK riêng lẻ ở trên.
  • Chuỗi claim: Bổ sung entity CLAIM_DOCUMENT (ảnh hiện trường, hóa đơn viện phí) và CLAIM_ASSESSMENT (lịch sử thẩm định của giám định viên). Xác định cardinality với CLAIM.
  • Truy vấn kiểm chứng: Viết bằng lời (không cần SQL hoàn chỉnh) cách lấy: tổng phí đã thu trong tháng 3/2026; danh sách hợp đồng xe sắp hết hạn trong 30 ngày; tỷ lệ claim được duyệt theo từng product_type. Nếu schema của bạn không trả lời được câu nào, hãy sửa schema.

Tóm tắt

ERD cho công ty bảo hiểm VN là bài lab tổng hợp vì nó ép bạn xử lý ba dòng sản phẩm khác biệt (auto, life, health) trong một hệ thống thống nhất. Ba quyết định thiết kế cốt lõi cần nhớ: (1) dùng subtype/supertype để bảng POLICY giữ thuộc tính chung còn AUTO_POLICY/LIFE_POLICY/HEALTH_POLICY giữ thuộc tính riêng, kèm cột discriminator; (2) tách vai trò con người — policyholder, insured, beneficiary là ba vai trò có thể trỏ về cùng bảng CUSTOMER, đừng gộp chúng làm một; (3) giao dịch là entity riêngPREMIUM_PAYMENTCLAIM phải one-to-many để giữ lịch sử, không bao giờ ghi đè. Cộng thêm surrogate key làm PK, natural key policy_number để UNIQUE, và các ràng buộc nghiệp vụ (ngày, tỷ lệ thụ hưởng, số tiền duyệt) ghi rõ trong data dictionary. Nắm chắc khung tư duy này, bạn không chỉ dựng được ERD bảo hiểm mà còn xử lý được mọi domain có "một entity nhiều biến thể" — ngân hàng, viễn thông, logistics đều theo cùng nguyên tắc.