Mở đầu — vì sao bài này quan trọng
Là một BA (Business Analyst), bạn không cần phải tự tay viết câu lệnh SQL phức tạp hay tối ưu chỉ mục cho database. Nhưng nếu bạn không hiểu các loại quan hệ giữa dữ liệu, bạn sẽ rất khó làm tốt phần việc cốt lõi của mình: mô tả đúng nghiệp vụ, viết user story chính xác, và phản biện thiết kế của đội kỹ thuật khi cần.
Hãy tưởng tượng tình huống này. Bạn viết một dòng yêu cầu nghe rất vô hại: "Mỗi khách hàng có một số điện thoại." Đội dev đọc xong, thiết kế bảng customers với một cột phone duy nhất. Ba tháng sau, doanh nghiệp triển khai bán hàng đa kênh, và một khách hàng giờ có số di động, số cố định ở công ty, số Zalo Business. Hệ thống không lưu được. Người ta phải sửa cấu trúc database đang chạy với hàng triệu bản ghi — một việc tốn kém và rủi ro. Gốc rễ của sự cố này không nằm ở code, mà nằm ở chỗ bạn — người viết yêu cầu — đã không nhận ra đây thực ra là một quan hệ "một khách hàng có NHIỀU số điện thoại".
Quan hệ dữ liệu (data relationships) chính là cách chúng ta mô tả các thực thể nghiệp vụ liên kết với nhau như thế nào: một đơn hàng thuộc về ai, một sản phẩm nằm trong bao nhiêu danh mục, một học viên đăng ký bao nhiêu khóa học. Có ba loại quan hệ nền tảng mà bất kỳ BA nào cũng phải nắm vững: 1:1 (một-một), 1:N (một-nhiều) và M:N (nhiều-nhiều). Hiểu được ba loại này, bạn sẽ "đọc" được mô hình dữ liệu của cả một hệ thống, và quan trọng hơn, bạn viết yêu cầu theo cách mà đội kỹ thuật triển khai đúng ngay từ đầu.
Khái niệm cốt lõi
Trước khi đi vào ba loại quan hệ, ta cần thống nhất vài thuật ngữ. Thực thể (entity) là một đối tượng nghiệp vụ mà ta cần lưu trữ thông tin: Khách hàng, Đơn hàng, Sản phẩm, Nhân viên. Trong database, mỗi thực thể thường tương ứng với một bảng (table), mỗi dòng là một bản ghi cụ thể. Khóa chính (primary key) là cột định danh duy nhất cho mỗi dòng (thường là id). Khóa ngoại (foreign key) là cột trong bảng này trỏ tới khóa chính của bảng khác — đây chính là "sợi dây" tạo nên quan hệ.
Câu hỏi cốt lõi để xác định loại quan hệ rất đơn giản, bạn chỉ cần hỏi hai chiều: "Một A có thể liên kết với bao nhiêu B?" và ngược lại "Một B có thể liên kết với bao nhiêu A?". Trả lời được hai câu này là bạn đã xác định được loại quan hệ.
One-to-One (1:1) — Một-một
Quan hệ 1:1 nghĩa là một bản ghi ở bảng A ứng với đúng một bản ghi ở bảng B, và ngược lại. Ví dụ kinh điển: mỗi user có đúng một hồ sơ chi tiết (profile), mỗi đơn hàng có đúng một hóa đơn (invoice), mỗi nhân viên có đúng một thẻ chấm công.
CREATE TABLE users (
id BIGINT PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL
);CREATE TABLE user_profiles (
user_id BIGINT PRIMARY KEY, -- vừa là khóa chính, vừa là khóa ngoại
full_name VARCHAR(255),
date_of_birth DATE,
address TEXT,
FOREIGN KEY (user_id) REFERENCES users(id)
);
Điểm mấu chốt kỹ thuật: khóa ngoại user_id ở bảng user_profiles được đặt là UNIQUE (ở đây nó là PRIMARY KEY nên đã unique sẵn). Chính ràng buộc UNIQUE này ngăn không cho một user có hai profile.
Một câu hỏi BA hay gặp: nếu là 1:1 thì sao không gộp hết vào một bảng cho gọn? Có ba lý do chính đáng để tách: (1) Bảo mật — tách thông tin nhạy cảm như số CMND, thông tin lương ra bảng riêng để phân quyền truy cập chặt hơn; (2) Hiệu năng — nếu profile có nhiều trường ít dùng, tách ra giúp bảng chính nhẹ, truy vấn nhanh; (3) Tính tùy chọn — không phải user nào cũng có profile (ví dụ tài khoản vừa đăng ký chưa điền thông tin).
One-to-Many (1:N) — Một-nhiều
Đây là loại quan hệ phổ biến nhất, chiếm phần lớn các quan hệ bạn gặp. Một bản ghi ở "phía một" liên kết với nhiều bản ghi ở "phía nhiều", nhưng mỗi bản ghi ở phía nhiều chỉ thuộc về một bản ghi phía một. Ví dụ: một khách hàng có nhiều đơn hàng, nhưng mỗi đơn hàng chỉ thuộc về một khách hàng. Một danh mục chứa nhiều sản phẩm, mỗi sản phẩm thuộc một danh mục.
CREATE TABLE customers (
id BIGINT PRIMARY KEY,
name VARCHAR(255)
);CREATE TABLE orders (
id BIGINT PRIMARY KEY,
customer_id BIGINT NOT NULL, -- khóa ngoại, KHÔNG unique
total_amount DECIMAL(12,2),
created_at TIMESTAMP,
FOREIGN KEY (customer_id) REFERENCES customers(id)
);
Quy tắc vàng cần nhớ: khóa ngoại luôn nằm ở phía "nhiều". Bảng orders (phía nhiều) chứa cột customer_id trỏ về customers (phía một). Và khác với 1:1, cột customer_id ở đây KHÔNG có ràng buộc UNIQUE — đó chính là điều cho phép một khách hàng xuất hiện trong nhiều đơn hàng.
Many-to-Many (M:N) — Nhiều-nhiều
Quan hệ M:N nghĩa là một bản ghi ở A liên kết với nhiều bản ghi ở B, và ngược lại một bản ghi ở B cũng liên kết với nhiều bản ghi ở A. Ví dụ: một học viên đăng ký nhiều khóa học, và một khóa học có nhiều học viên. Một bài viết có nhiều thẻ (tag), và một thẻ gắn cho nhiều bài viết. Một sản phẩm thuộc nhiều danh mục, một danh mục chứa nhiều sản phẩm.
Đây là loại quan hệ "bẫy" nhất với BA, vì database quan hệ KHÔNG thể biểu diễn trực tiếp M:N. Bạn không thể nhét một danh sách course_id vào một cột của bảng students. Giải pháp là tạo một bảng trung gian (junction table / join table / bảng nối) đứng giữa, biến một quan hệ M:N thành hai quan hệ 1:N.
CREATE TABLE students (
id BIGINT PRIMARY KEY,
name VARCHAR(255)
);CREATE TABLE courses (
id BIGINT PRIMARY KEY,
title VARCHAR(255)
);
CREATE TABLE enrollments ( -- bảng trung gian
student_id BIGINT NOT NULL,
course_id BIGINT NOT NULL,
enrolled_at TIMESTAMP,
progress_percent INT DEFAULT 0, -- dữ liệu của chính mối quan hệ
PRIMARY KEY (student_id, course_id),
FOREIGN KEY (student_id) REFERENCES students(id),
FOREIGN KEY (course_id) REFERENCES courses(id)
);
Hãy chú ý đến trường enrolled_at và progress_percent. Đây là một insight quan trọng cho BA: bảng trung gian không chỉ để "nối", nó là nơi tự nhiên để lưu các thuộc tính của chính mối quan hệ. Tiến độ học, ngày ghi danh, điểm số — những dữ liệu này không thuộc về học viên cũng không thuộc về khóa học, mà thuộc về sự kiện "học viên X ghi danh khóa Y".
Tình huống thực tế
Tình huống 1: Tiki và bài toán sản phẩm đa danh mục
Một sàn thương mại điện tử như Tiki ban đầu thiết kế quan hệ sản phẩm — danh mục theo kiểu 1:N: mỗi sản phẩm thuộc một danh mục duy nhất. Cột category_id nằm trong bảng products. Mọi thứ chạy ổn cho đến khi đội marketing muốn một chiếc tai nghe Sony vừa xuất hiện trong danh mục "Điện tử > Tai nghe", vừa nằm trong chiến dịch "Quà tặng sinh nhật", lại vừa thuộc "Sản phẩm bán chạy tuần".
Với thiết kế 1:N cũ, một sản phẩm chỉ có một category_id nên không thể làm được. BA dự án nhận ra bản chất nghiệp vụ đã thay đổi: một sản phẩm cần thuộc NHIỀU danh mục, một danh mục chứa NHIỀU sản phẩm — đây là M:N. Giải pháp là tạo bảng trung gian product_categories(product_id, category_id). Sau khi đổi, một sản phẩm có thể nằm trong 5, 10 danh mục tùy ý mà không cần đụng đến cấu trúc bảng products.
Bài học rút ra: Khi nghe doanh nghiệp nói "tôi muốn món này xuất hiện ở nhiều chỗ", đèn báo M:N phải sáng lên trong đầu bạn. Đừng để một quan hệ vốn là M:N bị thiết kế nhầm thành 1:N, vì sửa lại khi hệ thống đã chạy là rất đắt.
Tình huống 2: VNPAY và quan hệ 1:1 giữa giao dịch và biên lai điện tử
Tại một công ty cổng thanh toán giả định lấy cảm hứng từ VNPAY, mỗi giao dịch thành công bắt buộc sinh đúng một biên lai điện tử (e-receipt) chứa mã số thuế, mã QR đối soát và chữ ký số. Đội kỹ thuật ban đầu định nhét toàn bộ thông tin biên lai vào bảng transactions. BA phản đối, và lý do của cô ấy là kinh điển cho quan hệ 1:1.
Thứ nhất, bảng transactions xử lý hàng chục triệu bản ghi mỗi ngày, cần truy vấn cực nhanh; nhồi thêm 15 cột biên lai làm bảng phình to và chậm đi. Thứ hai, dữ liệu biên lai (chữ ký số, mã đối soát) là dữ liệu pháp lý nhạy cảm, cần phân quyền truy cập khác với dữ liệu giao dịch thông thường. Vì vậy họ tách thành bảng transaction_receipts với transaction_id là khóa chính kiêm khóa ngoại UNIQUE. Ràng buộc UNIQUE đảm bảo về mặt kỹ thuật rằng một giao dịch không bao giờ sinh ra hai biên lai — một yêu cầu kiểm toán bắt buộc.
Bài học rút ra: Quan hệ 1:1 không phải để "chia bảng cho vui". Nó là công cụ để tách dữ liệu theo tần suất sử dụng, theo mức độ bảo mật, và để đảm bảo một ràng buộc nghiệp vụ "chỉ có đúng một" được thực thi ở tầng database, không phụ thuộc vào việc lập trình viên có nhớ kiểm tra hay không.
Tình huống 3: Grab và quan hệ tài xế — cuốc xe bị hiểu nhầm
Ở một nền tảng gọi xe kiểu Grab, BA mới vào nghề mô tả: "Một tài xế có nhiều cuốc xe, một cuốc xe có một tài xế — đây là 1:N." Đúng về mặt logic, nhưng chưa đủ. Khi đào sâu, đội phát hiện một cuốc xe trong giờ cao điểm có thể được gán lần lượt cho nhiều tài xế: tài xế A nhận rồi hủy, hệ thống gán lại cho tài xế B, rồi C mới nhận thành công. Nếu cần lưu lịch sử "cuốc xe này đã từng được chào mời cho những tài xế nào", thì quan hệ giữa "lời mời" (ride offers) và tài xế lại trở thành M:N.
Họ tách thành hai mô hình: quan hệ "cuốc xe đã hoàn thành — tài xế thực hiện" là 1:N (mỗi cuốc hoàn thành có đúng một tài xế), còn quan hệ "cuốc xe — các tài xế từng được chào" là M:N qua bảng ride_offers(ride_id, driver_id, offered_at, response). Phân biệt này cho phép đội phân tích tỷ lệ tài xế từ chối, tối ưu thuật toán phân phối cuốc.
Bài học rút ra: Cùng hai thực thể có thể có nhiều hơn một quan hệ với nhau, và mỗi quan hệ có loại riêng. Đừng dừng lại ở câu trả lời đầu tiên; hãy hỏi "còn cách nào A và B liên kết nữa không?".
Hướng dẫn từng bước
Đây là quy trình bạn có thể áp dụng cho mọi cặp thực thể khi phân tích yêu cầu:
Bước 1 — Liệt kê các thực thể. Đọc kỹ mô tả nghiệp vụ, gạch chân các danh từ chính: Khách hàng, Đơn hàng, Sản phẩm, Khóa học... Mỗi danh từ này thường là một thực thể (một bảng).
Bước 2 — Đặt câu hỏi hai chiều cho từng cặp. Với mỗi cặp thực thể A và B, hỏi: "Một A liên kết với tối đa bao nhiêu B?" và "Một B liên kết với tối đa bao nhiêu A?". Ghi câu trả lời dưới dạng "một" hoặc "nhiều".
Bước 3 — Suy ra loại quan hệ. Một–một là 1:1; một–nhiều (hoặc nhiều–một) là 1:N; nhiều–nhiều là M:N.
Bước 4 — Xác định nơi đặt khóa ngoại. Với 1:N, khóa ngoại nằm ở phía "nhiều". Với 1:1, khóa ngoại nằm ở bảng phụ kèm ràng buộc UNIQUE. Với M:N, tạo bảng trung gian chứa hai khóa ngoại.
Bước 5 — Tìm thuộc tính của chính mối quan hệ. Đặc biệt với M:N: có dữ liệu nào "thuộc về sự kiện liên kết" không (ngày, số lượng, trạng thái, tiến độ)? Nếu có, chúng nằm trong bảng trung gian.
Bước 6 — Vẽ sơ đồ ERD và xác nhận với stakeholder. Dùng một sơ đồ thực thể–quan hệ (ERD) đơn giản, mô tả lại bằng ngôn ngữ nghiệp vụ và hỏi người dùng: "Đúng là một khách hàng có thể có nhiều địa chỉ giao hàng phải không ạ?". Xác nhận sớm rẻ hơn sửa lỗi muộn rất nhiều.
Lỗi thường gặp & mẹo
Lỗi 1 — Bỏ sót quan hệ M:N, ép thành 1:N. Đây là lỗi tốn kém nhất. Dấu hiệu nhận biết: khi bạn định viết "lưu danh sách X vào một cột", gần như chắc chắn bạn đang gặp M:N hoặc 1:N cần bảng riêng, chứ không phải một cột.
Lỗi 2 — Nhầm "hiện tại là một" với "mãi mãi là một". Hôm nay mỗi khách hàng có một số điện thoại, nhưng nghiệp vụ tương lai có thể cần nhiều. Hãy hỏi stakeholder về khả năng mở rộng: "Trong tương lai một khách hàng có thể cần lưu nhiều số điện thoại không?". Trả lời "có thể" thì nên thiết kế 1:N ngay từ đầu.
Lỗi 3 — Quên các thuộc tính của bảng trung gian. Nhiều BA chỉ nghĩ bảng nối là "hai cái id dán vào nhau" mà bỏ quên ngày ghi danh, số lượng, đơn giá tại thời điểm mua. Riêng với đơn hàng, order_items phải lưu unit_price tại thời điểm đặt, vì giá sản phẩm thay đổi theo thời gian.
Lỗi 4 — Không nghĩ đến hành vi khi xóa. Khi xóa một khách hàng, các đơn hàng của họ xử lý thế nào? Xóa theo, chặn không cho xóa, hay để mồ côi? Đây là câu hỏi nghiệp vụ (cascade rules) mà BA cần làm rõ, không phải để dev tự quyết.
Mẹo: Mỗi khi nghe các cụm từ "nhiều... cho mỗi", "thuộc về", "bao gồm", "gắn thẻ", "đăng ký", hãy dừng lại và chạy ngay câu hỏi hai chiều ở Bước 2. Phản xạ này sẽ giúp bạn bắt đúng loại quan hệ trong vài giây.
Bài tập thực hành
Hãy phân tích một hệ thống thư viện trường đại học với mô tả sau, rồi tự trả lời:
> "Mỗi sinh viên có một thẻ thư viện. Sinh viên mượn nhiều cuốn sách; mỗi đầu sách có nhiều bản in vật lý. Mỗi tác giả viết nhiều cuốn sách, và một cuốn sách có thể do nhiều tác giả đồng viết. Mỗi lượt mượn ghi lại ngày mượn và ngày phải trả."
- Liệt kê các thực thể có trong mô tả.
- Xác định loại quan hệ (1:1, 1:N, hay M:N) cho từng cặp: Sinh viên–Thẻ thư viện; Đầu sách–Bản in; Tác giả–Sách; Sinh viên–Bản in (qua lượt mượn).
- Với các quan hệ M:N, hãy đặt tên bảng trung gian và liệt kê các cột bạn cho rằng nó cần có.
- "Ngày mượn" và "ngày phải trả" nên nằm ở bảng nào? Vì sao?
book_authors); việc mượn sách là M:N giữa sinh viên và bản in, cần bảng loans(student_id, copy_id, borrowed_at, due_date). Ngày mượn và ngày phải trả thuộc về chính lượt mượn nên nằm trong bảng loans.Tóm tắt
Ba loại quan hệ dữ liệu là bộ công cụ nền tảng giúp bạn đọc và thiết kế mô hình dữ liệu như một BA chuyên nghiệp. Quan hệ 1:1 dùng để tách dữ liệu theo bảo mật hoặc hiệu năng, với khóa ngoại UNIQUE ở bảng phụ. Quan hệ 1:N là phổ biến nhất, với khóa ngoại luôn nằm ở phía "nhiều". Quan hệ M:N không thể biểu diễn trực tiếp mà cần một bảng trung gian, và bảng này thường là nơi lý tưởng để lưu các thuộc tính của chính mối quan hệ.
Công cụ quan trọng nhất bạn mang đi từ bài này không phải là cú pháp SQL, mà là phản xạ đặt câu hỏi hai chiều cho mọi cặp thực thể, và thói quen hỏi stakeholder về khả năng mở rộng trong tương lai trước khi chốt thiết kế. Nắm vững điều này, bạn sẽ viết yêu cầu chính xác hơn, phản biện thiết kế kỹ thuật tự tin hơn, và tránh được những lỗi cấu trúc dữ liệu tốn kém mà rất khó sửa khi hệ thống đã vận hành.