Menu
ESC

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

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

Đang tải...

Bài 18 — Hands-on: ERD cho Hệ Thống Ngân Hàng VN

Data Modeling and ERD Mastery Bài 18/60

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

Cho đến bài này, bạn đã đi qua một chặng đường dài: từ entity, attribute, primary key, relationship, cardinality cho đến ba tầng mô hình (conceptual, logical, physical) và các quy tắc chuẩn hóa 1NF–3NF. Đó là lý thuyết. Nhưng một Business Analyst giỏi không phải người thuộc lòng định nghĩa — mà là người ngồi xuống trước một bài toán nghiệp vụ hỗn độn và vẽ ra được một ERD đúng, đủ, dùng được.

Bài 18 là buổi thực hành đầu tiên ở quy mô thật. Chúng ta sẽ thiết kế ERD cho một core banking system — hệ thống lõi của ngân hàng Việt Nam. Tôi chọn ngành ngân hàng có chủ đích, vì đây là một trong những domain khó nhất mà BA Việt Nam thường gặp: dữ liệu nhạy cảm, quy tắc nghiệp vụ chặt chẽ theo quy định Ngân hàng Nhà nước, và sai sót một số dư tài khoản có thể thành sự cố pháp lý. Nếu bạn dựng được ERD ngân hàng tử tế, bạn dựng được ERD cho gần như mọi ngành.

Điều tôi muốn bạn rút ra sau bài này không chỉ là một sơ đồ đẹp, mà là tư duy chuyển hóa: nghe khách hàng kể nghiệp vụ → bóc tách thành entity và relationship → kiểm tra bằng các quy tắc đã học → bảo vệ thiết kế của mình khi bị chất vấn. Đó chính là công việc thật của bạn ngoài đời.

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

Bối cảnh nghiệp vụ ngân hàng lõi

Core banking là phần "ruột" xử lý tài khoản, giao dịch và số dư. Khi một khách hàng gửi tiết kiệm, vay vốn, hay chuyển khoản, mọi thứ cuối cùng đều quy về việc ghi nhận vào core. Một số đặc thù bạn phải nắm trước khi vẽ:

  • Một khách hàng có thể có nhiều tài khoản thuộc nhiều loại khác nhau (thanh toán, tiết kiệm, vay).
  • Mỗi giao dịch phải để lại dấu vết bất biến — ngân hàng không bao giờ "sửa" một giao dịch đã ghi, mà ghi một giao dịch điều chỉnh ngược lại (reversal).
  • Số dư là kết quả, không phải dữ liệu gốc — về lý thuyết, số dư = tổng các giao dịch. Nhưng trong thực tế ta vẫn lưu số dư hiện tại để truy vấn nhanh (đây là một quyết định denormalization có chủ đích, đã học ở Bài 15).

Bộ entity cốt lõi

Dựa trên scenario, ta xác định các entity trung tâm sau:

Customer (Khách hàng) — cá nhân hoặc tổ chức mở tài khoản. Các attribute điển hình: customer_id (surrogate key), full_name, national_id (CCCD/CMND), date_of_birth, phone, customer_type (cá nhân/doanh nghiệp), kyc_status. Lưu ý CCCD là một candidate key tự nhiên nhưng ta vẫn dùng surrogate key vì số CCCD có thể đổi (đổi từ 9 số sang 12 số là ví dụ kinh điển ở VN).

Account (Tài khoản) — đây là entity trung tâm nhất. Một tài khoản thuộc về một khách hàng (hoặc nhiều, với tài khoản đồng sở hữu). Attribute: account_id, account_number, account_type (savings/current/loan), currency (VND/USD), current_balance, status (active/frozen/closed), opened_date.

Ở đây có một quyết định thiết kế quan trọng: savings, current và loan có nên là một bảng Account chung hay tách riêng? Đây chính là bài toán Subtype/Supertype (sẽ học sâu ở Bài 20). Trong bài này ta dùng cách tiếp cận thực dụng: một supertype Account chứa thuộc tính chung, và các subtype SavingsAccount (lãi suất, kỳ hạn), LoanAccount (số tiền vay gốc, lãi suất vay, lịch trả nợ) chứa thuộc tính riêng.

Transaction (Giao dịch) — ghi nhận mọi biến động tiền. Attribute: transaction_id, account_id, transaction_type (debit/credit), amount, transaction_datetime, description, reference_number, status. Đây là entity append-only (chỉ thêm, không sửa, không xóa).

Branch (Chi nhánh)Employee (Nhân viên) — bối cảnh phụ trợ: tài khoản được mở tại chi nhánh nào, nhân viên nào phụ trách.

Card (Thẻ) — thẻ ghi nợ/tín dụng gắn với tài khoản.

Các relationship chính

  • Customer 1—N Account (một khách hàng nhiều tài khoản). Nếu tính cả đồng sở hữu thì là M—N, cần bảng trung gian AccountHolder.
  • Account 1—N Transaction (một tài khoản nhiều giao dịch).
  • Branch 1—N Account (một chi nhánh quản lý nhiều tài khoản).
  • Account 1—N Card (một tài khoản có thể có nhiều thẻ).
  • Customer M—N Account qua AccountHolder khi có tài khoản đồng sở hữu.

Tình huống thực tế

Ví dụ 1 — Tài khoản đồng sở hữu ở một ngân hàng TMCP

Một ngân hàng thương mại cổ phần tầm trung (giả định tên VietPhuBank) triển khai dự án thay core mới. Ban đầu, đội thiết kế vẽ quan hệ Customer 1—N Account, nghĩ rằng mỗi tài khoản chỉ thuộc một người. Sản phẩm chạy được vài tháng thì phòng nghiệp vụ phản ánh: tài khoản tiết kiệm của hai vợ chồng cần đứng tên cả hai, và tài khoản công ty cần nhiều người đồng ký duyệt.

Diễn giải: Quan hệ thực ra là M—N. Một tài khoản có thể nhiều chủ; một người có thể là chủ nhiều tài khoản. Đội phải bổ sung bảng trung gian AccountHolder(account_id, customer_id, holder_role) với holder_role là primary/secondary/authorized. Việc này khiến phải migrate dữ liệu cũ và sửa hàng loạt báo cáo.

Bài học: Khi nghe từ "đồng sở hữu", "đồng ký", "nhiều người cùng...", phản xạ của bạn phải là M—N và một associative entity. Đừng mặc định 1—N chỉ vì trường hợp phổ biến nhất là một người một tài khoản.

Ví dụ 2 — Số dư sai vì xóa giao dịch

Tại một công ty fintech ở TP.HCM cung cấp ví điện tử, lập trình viên trong giai đoạn đầu thiết kế bảng transaction cho phép UPDATEDELETE để "sửa lỗi nhập liệu". Khi đối soát cuối tháng với ngân hàng đối tác, tổng số dư của hệ thống lệch khoảng 47 triệu đồng so với sao kê ngân hàng. Truy vết mất ba ngày mới phát hiện một nhân viên vận hành đã xóa vài giao dịch nghi ngờ trùng, nhưng số dư current_balance đã được cộng trừ trước đó nên không khớp lại được.

Diễn giải: Vấn đề gốc nằm ở mô hình dữ liệu: giao dịch tài chính phải immutable (bất biến). Muốn hủy một giao dịch, ta ghi một giao dịch reversal mới với amount ngược dấu và một trường reverses_transaction_id trỏ về giao dịch gốc — đây là một quan hệ self-referencing (sẽ học kỹ ở Bài 21). Như vậy mọi biến động đều có dấu vết kép, đối soát được.

Bài học: Trong domain tài chính, ERD phải mã hóa luôn quy tắc "không xóa, chỉ bù trừ". Đó là một quyết định mô hình hóa, không chỉ là quy ước vận hành. Một ERD tốt khiến hành vi sai trở nên khó thực hiện.

Ví dụ 3 — Gộp ba loại tài khoản vào một bảng phẳng

Một ngân hàng số (digital bank) mới thành lập muốn ra mắt nhanh, nên thiết kế một bảng account duy nhất với đủ cột cho cả ba loại: vừa có interest_rate, term_months (cho tiết kiệm), vừa có loan_principal, repayment_schedule (cho vay), vừa có overdraft_limit (cho thanh toán). Kết quả: với mỗi bản ghi, hơn một nửa số cột luôn NULL. Báo cáo lọc dữ liệu trở nên rối, ràng buộc nghiệp vụ khó áp (làm sao bắt buộc term_months NOT NULL chỉ khi là tiết kiệm?).

Diễn giải: Đây là anti-pattern "one big table". Giải pháp là supertype/subtype: bảng account chung + ba bảng con. Mỗi bảng con chỉ chứa cột thuộc về nó, và có thể áp NOT NULL đúng chỗ.

Bài học: Khi bạn thấy nhiều cột NULL theo từng "loại", đó là tín hiệu mạnh cần tách subtype. NULL hàng loạt không phải lỗi nhỏ — nó báo hiệu mô hình đang gộp những thứ vốn khác nhau.

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

Đây là quy trình tôi muốn bạn lặp lại mỗi khi dựng ERD cho một domain mới.

Bước 1 — Liệt kê entity từ danh từ nghiệp vụ. Đọc kỹ scenario, gạch chân mọi danh từ quan trọng: khách hàng, tài khoản, giao dịch, chi nhánh, thẻ, nhân viên. Đây là ứng viên entity. Loại bỏ những danh từ thực ra là attribute (ví dụ "số điện thoại" là attribute của Customer, không phải entity).

Bước 2 — Xác định primary key cho từng entity. Áp kiến thức Bài 6: ưu tiên surrogate key (customer_id, account_id) cho các bảng trung tâm. Ghi chú lại candidate key tự nhiên (CCCD, số tài khoản) để áp ràng buộc UNIQUE.

Bước 3 — Vẽ relationship và xác định cardinality. Với từng cặp entity, hỏi hai câu: "Một A liên kết tối đa bao nhiêu B?" và ngược lại. Customer–Account: một khách hàng nhiều tài khoản → 1—N (hoặc M—N nếu có đồng sở hữu). Account–Transaction: 1—N. Dùng ký hiệu Crow's Foot đã học ở Bài 8.

Bước 4 — Xử lý quan hệ M—N bằng associative entity. Mỗi M—N phải biến thành một bảng trung gian. Ở đây là AccountHolder cho quan hệ đồng sở hữu. Bảng này có khóa chính kép (account_id, customer_id) và có thể mang attribute riêng như holder_role.

Bước 5 — Quyết định supertype/subtype cho Account. Tách Account thành supertype + ba subtype SavingsAccount, CurrentAccount, LoanAccount. Supertype giữ thuộc tính chung và quan hệ chung (tới Customer, Transaction, Branch).

Bước 6 — Bổ sung entity cho audit và bất biến. Đảm bảo Transaction là append-only và thêm trường reverses_transaction_id cho reversal. Cân nhắc một bảng AccountBalanceSnapshot để chốt số dư cuối ngày phục vụ đối soát.

Bước 7 — Kiểm tra chuẩn hóa. Rà từng bảng theo 1NF–3NF. Ví dụ: đừng lưu branch_name trong bảng Account (đó là transitive dependency qua branch_id — vi phạm 3NF, đã học Bài 14). Chỉ lưu branch_id, lấy tên từ bảng Branch.

Bước 8 — Đối chiếu lại với nghiệp vụ. Đem ERD đi "diễn lại" vài kịch bản: mở tài khoản, gửi tiền, chuyển khoản, đóng tài khoản. Nếu một kịch bản không thực hiện được trên sơ đồ của bạn, sơ đồ còn thiếu.

Một phác thảo bằng cú pháp dbdiagram (đã làm quen ở Bài 16) cho phần lõi:

Table customer {
  customer_id   int [pk]
  national_id   varchar [unique]
  full_name     varchar
  customer_type varchar
}

Table account { account_id int [pk] account_number varchar [unique] account_type varchar currency varchar current_balance decimal branch_id int [ref: > branch.branch_id] status varchar }

Table account_holder { account_id int [ref: > account.account_id] customer_id int [ref: > customer.customer_id] holder_role varchar }

Table transaction { transaction_id int [pk] account_id int [ref: > account.account_id] transaction_type varchar amount decimal transaction_datetime datetime reverses_transaction_id int [ref: > transaction.transaction_id] }

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

Lỗi 1 — Lưu số dư mà không lưu giao dịch. Một số thiết kế chỉ có current_balance mà không có lịch sử giao dịch đầy đủ. Khi cần đối soát hoặc giải trình với khách hàng, không có cách nào tái dựng. Luôn coi current_balance là cache của Transaction, không phải nguồn sự thật duy nhất.

Lỗi 2 — Dùng số CCCD/CMND làm primary key. Số định danh thay đổi (CMND 9 số → CCCD 12 số → căn cước mới), và đôi khi bị nhập sai. Dùng surrogate key, đặt UNIQUE cho CCCD.

Lỗi 3 — Quên currency trong Account và Transaction. Ngân hàng VN xử lý nhiều ngoại tệ. Nếu thiếu trường currency, bạn sẽ cộng nhầm 100 USD với 100.000 VND (đề tài này sẽ đào sâu ở Bài 27).

Lỗi 4 — Gộp địa chỉ chi nhánh, tên chi nhánh vào bảng Account. Vi phạm 3NF. Chỉ giữ khóa ngoại branch_id.

Mẹo: Khi phân vân một thứ là entity hay attribute, hãy hỏi: "Nó có thuộc tính riêng và có thể tồn tại độc lập không?" Thẻ ngân hàng có số thẻ, ngày hết hạn, trạng thái → là entity. Màu thẻ → chỉ là attribute.

Mẹo: Với mọi entity tài chính, mặc định nghĩ tới audit và bất biến ngay từ đầu, đừng để dành "tính sau". Thêm trường audit về sau thường kéo theo migration đau đớn.

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

  • Vẽ ERD lõi. Dùng draw.io hoặc dbdiagram, dựng ERD cho năm entity: Customer, Account, Transaction, Branch, Card. Đặt primary key, foreign key, và ký hiệu cardinality theo Crow's Foot.
  • Thêm đồng sở hữu. Bổ sung bảng AccountHolder để hỗ trợ tài khoản đồng sở hữu của hai vợ chồng. Xác định khóa chính kép và attribute holder_role.
  • Mô hình hóa reversal. Thêm trường reverses_transaction_id và viết một câu kịch bản: khách hàng bị trừ nhầm 500.000đ, hãy mô tả các bản ghi Transaction được tạo để hoàn tiền mà không xóa giao dịch gốc.
  • Tách subtype. Chuyển bảng Account phẳng thành supertype Account + ba subtype SavingsAccount (interest_rate, term_months), CurrentAccount (overdraft_limit), LoanAccount (loan_principal, repayment_schedule). Áp NOT NULL đúng chỗ.
  • Tự kiểm 3NF. Rà ERD của bạn, chỉ ra ít nhất một transitive dependency tiềm ẩn và sửa lại.
Hãy lưu kết quả — bạn sẽ tái dùng tư duy này ở Bài 19 (E-commerce) và Bài 38 (Insurance).

Tóm tắt

Trong bài này, chúng ta đã biến lý thuyết data modeling thành một ERD ngân hàng thực chiến. Những điểm cốt lõi cần nhớ:

  • Bộ entity lõi của core banking: Customer, Account (savings/current/loan), Transaction, Branch, Card, với Account là trung tâm.
  • Quan hệ Customer–Account là 1—N, nhưng thành M—N khi có đồng sở hữu, đòi hỏi associative entity AccountHolder.
  • Ba loại tài khoản nên tách bằng supertype/subtype, không gộp vào một bảng phẳng đầy NULL.
  • Giao dịch phải bất biến: không sửa, không xóa, chỉ ghi reversal — đây là quyết định mô hình hóa, không phải quy ước vận hành.
  • Số dư là cache của lịch sử giao dịch; luôn giữ được khả năng tái dựng để đối soát.
  • Quy trình tám bước — từ liệt kê danh từ đến diễn lại kịch bản nghiệp vụ — là khung làm việc bạn dùng lại cho mọi domain.
ERD ngân hàng khó vì nó không tha thứ cho sự cẩu thả. Nhưng chính vì thế, khi bạn làm chủ được nó, bạn đã có tư duy đủ chắc để bước vào bất kỳ bài toán dữ liệu nào trong sự nghiệp BA của mình.