Mở đầu — vì sao bài này quan trọng
Nếu bạn đang học để trở thành một Data BA (Business Analyst chuyên về dữ liệu), thì một hệ thống quản lý học tập — LMS (Learning Management System) — chính là "sân tập" hoàn hảo. Vì sao? Vì LMS chạm vào gần như mọi mẫu mô hình dữ liệu mà bạn đã học suốt khóa: quan hệ nhiều-nhiều (học viên ghi danh nhiều khóa), phân cấp (khóa học chứa chương, chương chứa bài học), dữ liệu thời gian (tiến độ học, thời điểm hoàn thành), và cả những bài toán "khó nhằn" như tính điểm, cấp chứng chỉ, hay theo dõi việc xem hết một video.
Ở Việt Nam, thị trường edtech đã trưởng thành đủ để bạn có mẫu tham chiếu thật: Topica từng là gã khổng lồ đào tạo trực tuyến, CoderSchool chuyên bootcamp lập trình, và chính Vietnamcos.com — nền tảng bạn đang học — cũng là một LMS. Mỗi nền tảng có đặc thù riêng nhưng chia sẻ chung một "xương sống" dữ liệu.
Đây là bài hands-on lab — nghĩa là bạn sẽ không chỉ đọc lý thuyết mà thực sự dựng một ERD hoàn chỉnh cho LMS, từ entity đầu tiên đến ràng buộc cuối cùng. Sau bài này, bạn sẽ tự tin ngồi vào một cuộc họp discovery với đội edtech và vẽ được sơ đồ dữ liệu ngay trên bảng trắng. Đây là kỹ năng mà nhà tuyển dụng Data BA đánh giá cực cao: khả năng chuyển yêu cầu nghiệp vụ mơ hồ thành mô hình dữ liệu rõ ràng, chạy được.
Khái niệm cốt lõi
Trước khi vẽ, ta cần thống nhất phạm vi (scope) của LMS mà lab này xây dựng. Một LMS thương mại điển hình ở Việt Nam phải trả lời được các câu hỏi nghiệp vụ sau:
- Học viên nào đã ghi danh khóa nào, và trả bao nhiêu tiền?
- Học viên đang học đến bài nào, đã hoàn thành bao nhiêu phần trăm khóa?
- Bài kiểm tra chấm điểm ra sao, học viên đạt hay trượt?
- Khi nào cấp chứng chỉ, và chứng chỉ có mã xác thực không?
- Giảng viên nào dạy khóa nào, và được chia doanh thu thế nào?
Nhóm entity cốt lõi (Core entities)
Từ các câu hỏi trên, ta rút ra các entity chính. Tôi chia thành bốn nhóm để dễ nhớ:
Nhóm con người (Actors): users (bảng gốc chứa mọi tài khoản), với vai trò phân hóa thành học viên (student) và giảng viên (instructor). Ở đây bạn có một lựa chọn thiết kế quan trọng — dùng một bảng users chung với cột role, hay tách students và instructors riêng. Với LMS Việt Nam, tôi khuyên dùng bảng users chung vì một người vừa có thể là học viên khóa này, vừa là giảng viên khóa khác (rất phổ biến ở cộng đồng như MindX hay các nền tảng mentor).
Nhóm nội dung (Content): courses (khóa học) → sections (chương/phần) → lessons (bài học). Đây là quan hệ phân cấp một-nhiều lồng nhau. Mỗi lesson có thể là video, bài đọc, hoặc quiz.
Nhóm ghi danh & tiến độ (Enrollment & Progress): enrollments (bảng nối giữa học viên và khóa), lesson_progress (theo dõi từng bài đã học), quiz_attempts (lượt làm bài kiểm tra).
Nhóm giao dịch & chứng nhận (Commerce & Certification): payments (thanh toán), certificates (chứng chỉ), reviews (đánh giá khóa học).
Các quan hệ then chốt
Điểm mấu chốt của LMS nằm ở bảng enrollments — đây là bảng nối (junction table) hiện thực hóa quan hệ nhiều-nhiều giữa users và courses. Một học viên ghi danh nhiều khóa; một khóa có nhiều học viên. Nhưng enrollments không chỉ là bảng nối "trơn" — nó mang theo thuộc tính riêng: ngày ghi danh, giá tại thời điểm mua, trạng thái (đang học / hoàn thành / hết hạn), phần trăm tiến độ.
Đây là bài học vàng: khi quan hệ nhiều-nhiều có dữ liệu đi kèm, bảng nối trở thành một entity liên kết (associative entity) thực thụ, có khóa chính riêng và có thể được các bảng khác tham chiếu tới. Ví dụ, certificates không tham chiếu thẳng đến courses, mà tham chiếu đến enrollment_id — vì một chứng chỉ luôn gắn với một lần ghi danh cụ thể, không phải khóa chung chung.
Tình huống thực tế
Tình huống 1: CoderSchool và bài toán "xem hết video mới tính hoàn thành"
CoderSchool đào tạo bootcamp data science và web development. Đội sản phẩm của họ có một yêu cầu nghiệp vụ tưởng đơn giản: "Học viên phải xem hết 90% thời lượng video thì bài mới được đánh dấu hoàn thành." Ban đầu, dev định thêm một cột boolean is_completed vào bảng lessons. Data BA (chính là bạn trong tình huống này) đã chặn lại ngay.
Diễn giải: Cột is_completed đặt ở lessons là sai altitude — vì trạng thái hoàn thành khác nhau theo từng học viên, không phải thuộc tính của bản thân bài học. Đúng ra, ta cần bảng lesson_progress với khóa tổ hợp gợi ý (enrollment_id, lesson_id), cộng các cột watched_seconds, completed_at. Với 5.000 học viên và 200 bài học, bảng này có tối đa 1 triệu dòng — hoàn toàn nằm trong tầm xử lý.
Bài học rút ra: Luôn hỏi "thuộc tính này thuộc về ai?". Nếu một thuộc tính thay đổi theo cặp (học viên × bài học), nó phải nằm ở bảng liên kết, không phải ở bảng thực thể gốc. Đây là lỗi kinh điển mà bài "Attributes" và "Relationships" trước đó đã cảnh báo, nay bạn gặp lại trong thực chiến.
Tình huống 2: Topica và giá khóa học thay đổi theo thời gian
Topica có hàng trăm khóa, và giá khóa thay đổi liên tục theo chương trình khuyến mãi — ví dụ khóa "Tiếng Anh giao tiếp" giá gốc 3.990.000đ, dịp Tết giảm còn 1.990.000đ. Một học viên mua vào tháng 1 với giá 1.990.000đ; ba tháng sau kế toán chạy báo cáo doanh thu và thấy giá khóa hiện tại là 3.990.000đ. Con số lệch, báo cáo sai.
Diễn giải: Lỗi ở đây là dữ liệu doanh thu bị suy ra từ courses.price (giá hiện tại) thay vì được "đóng băng" tại thời điểm giao dịch. Giải pháp: bảng enrollments phải có cột price_paid — lưu đúng số tiền học viên đã trả. Đồng thời payments lưu amount, paid_at, payment_method (Momo, VNPay, chuyển khoản). Giá trong courses chỉ dùng để hiển thị và bán mới, còn số liệu tài chính phải bất biến theo lịch sử.
Bài học rút ra: Với dữ liệu tiền bạc, đừng bao giờ để báo cáo tài chính phụ thuộc vào giá trị "sống" có thể thay đổi. Hãy snapshot (chụp lại) giá trị tại thời điểm phát sinh. Đây là nguyên tắc bạn sẽ đào sâu ở Bài 27 về Money & Currency, nhưng LMS là nơi nó lộ ra rõ nhất.
Tình huống 3: Vietnamcos.com và chứng chỉ có thể xác thực công khai
Vietnamcos.com muốn cấp chứng chỉ mà nhà tuyển dụng có thể quét mã QR để xác thực thật giả. Yêu cầu: mỗi chứng chỉ có một mã duy nhất, không đoán được, và tra cứu được qua URL công khai vietnamcos.com/verify/{code}.
Diễn giải: Ta cần bảng certificates với certificate_code là một chuỗi UUID hoặc mã ngẫu nhiên (ví dụ VC-2026-A3F9K2), đặt ràng buộc UNIQUE. Chứng chỉ tham chiếu enrollment_id (một chứng chỉ = một lần ghi danh hoàn thành), có issued_at, và trạng thái is_revoked để xử lý trường hợp thu hồi (ví dụ phát hiện gian lận thi cử). Không dùng ID tự tăng làm mã xác thực công khai vì nó đoán được — người ta chỉ cần đổi số là dò ra chứng chỉ khác.
Bài học rút ra: Khi một khóa (natural key hoặc mã) phải phơi bày ra bên ngoài hệ thống, hãy dùng surrogate key không đoán được thay vì số tự tăng. An toàn dữ liệu bắt đầu từ khâu thiết kế schema.
Hướng dẫn từng bước
Giờ ta dựng ERD thực tế. Tôi khuyên dùng dbdiagram.io vì cú pháp DBML gọn và bạn xuất SQL trực tiếp được.
Bước 1 — Liệt kê entity và khóa chính. Bắt đầu bằng danh sách phẳng: users, courses, sections, lessons, enrollments, lesson_progress, quizzes, quiz_attempts, payments, certificates, reviews. Mỗi bảng có id là surrogate key (BIGINT auto-increment).
Bước 2 — Vẽ nhóm nội dung trước (dễ nhất). Quan hệ phân cấp rõ ràng:
courses (1) ──< sections (1) ──< lessons
sections.course_id là FK trỏ về courses.id. lessons.section_id trỏ về sections.id. Thêm cột order_index ở cả sections và lessons để giữ thứ tự hiển thị — LMS luôn cần sắp xếp bài theo trình tự học.
Bước 3 — Vẽ nhóm con người và ghi danh. Đây là trái tim của mô hình:
users (1) ──< enrollments >── (1) courses
enrollments có user_id, course_id, enrolled_at, price_paid, status, progress_percent. Đặt ràng buộc UNIQUE(user_id, course_id) để một người không ghi danh trùng một khóa hai lần.
Bước 4 — Nối tiến độ và kiểm tra.
enrollments (1) ──< lesson_progress >── (1) lessons
enrollments (1) ──< quiz_attempts >── (1) quizzes
lesson_progress khóa tổ hợp logic (enrollment_id, lesson_id). quiz_attempts lưu score, passed, attempted_at — cho phép nhiều lượt làm lại.
Bước 5 — Gắn thương mại và chứng nhận.
enrollments (1) ──< payments
enrollments (1) ──── (0..1) certificates
Quan hệ enrollment–certificate là một-một tùy chọn: chỉ khi hoàn thành mới có chứng chỉ. reviews nối users và courses (thường qua enrollment_id để đảm bảo chỉ người đã học mới đánh giá được).
Bước 6 — Thêm ràng buộc nghiệp vụ. status dùng ENUM ('active','completed','expired','refunded'). progress_percent có CHECK giữa 0 và 100. price_paid là DECIMAL(12,0) cho VND (không phần lẻ). passed suy từ score >= passing_score của quiz.
Bước 7 — Rà soát và xuất SQL. Đọc lại từng quan hệ, tự hỏi "cardinality này đúng chưa?". Rồi xuất DDL và mang đi hỏi review từ đội dev.
Lỗi thường gặp & mẹo
Lỗi 1 — Đặt tiến độ ở sai bảng. Như tình huống CoderSchool: nhồi is_completed, progress vào lessons hay courses. Nhớ: dữ liệu phụ thuộc vào từng học viên phải nằm ở bảng liên kết.
Lỗi 2 — Bỏ quên snapshot giá. Không có price_paid ở enrollments, để báo cáo doanh thu đọc thẳng courses.price. Kết quả: số liệu tài chính sai lệch mỗi khi đổi giá.
Lỗi 3 — Certificate trỏ thẳng vào course. Nếu certificates.course_id thay vì enrollment_id, bạn mất liên kết với lần học cụ thể — không biết cấp cho ai, khi nào, với tiến độ ra sao. Luôn trỏ chứng chỉ về enrollment.
Lỗi 4 — Quên order_index. Nhiều bạn mới cứ nghĩ ID tự tăng đủ để sắp thứ tự bài. Sai. Khi giảng viên chèn thêm bài vào giữa khóa, ID mới sẽ lớn hơn, làm rối trình tự. Cột order_index riêng cho phép sắp xếp linh hoạt.
Lỗi 5 — Cho phép ghi danh trùng. Thiếu UNIQUE(user_id, course_id) khiến một học viên click mua hai lần tạo hai bản ghi enrollment, phá vỡ báo cáo và tính tiền hai lần.
Mẹo hay:
- Với LMS đa vai trò, dùng
userschung + bảngcourse_instructors(nối nhiều-nhiều nếu một khóa có nhiều giảng viên đồng dạy — rất phổ biến ở bootcamp). - Đặt
deleted_at(soft delete) ởcoursesvàlessonsthay vì xóa cứng, vì học viên cũ vẫn cần truy cập nội dung đã mua. - Tách
quizzeskhỏilessonsbằng cách cholesson.type= 'quiz' và một FK tùy chọn — giữ cấu trúc bài học đồng nhất.
Bài tập thực hành
Hãy dựng ERD hoàn chỉnh cho một LMS dạy kỹ năng nghề nghiệp (bối cảnh Việt Nam) với các yêu cầu sau. Dùng dbdiagram.io hoặc draw.io.
- Entity bắt buộc: users, courses, sections, lessons, enrollments, lesson_progress, quizzes, quiz_attempts, payments, certificates, reviews.
- Yêu cầu nghiệp vụ cần mô hình hóa:
course_instructors).
- Học viên chỉ đánh giá được khóa mình đã ghi danh.
- Chứng chỉ có mã xác thực công khai không đoán được và có thể bị thu hồi.
- Doanh thu báo cáo phải chính xác kể cả khi giá khóa thay đổi.
- Một quiz cho phép làm lại tối đa 3 lần, chỉ lấy điểm cao nhất.- Sản phẩm nộp:
lesson_progress và certificates đều trỏ về enrollment_id thay vì trỏ thẳng về course/lesson gốc.- Câu hỏi mở rộng (nâng cao): Nếu nền tảng muốn hỗ trợ "lớp học theo cohort" (một nhóm học viên bắt đầu cùng ngày, học theo lịch cố định như CoderSchool), bạn sẽ thêm entity nào? Gợi ý: nghĩ tới
cohortsvà cách nó chen vào giữaenrollmentsvàcourses.
Tóm tắt
Trong lab này, bạn đã dựng một ERD hoàn chỉnh cho hệ thống LMS Việt Nam — từ nhóm nội dung phân cấp (courses → sections → lessons), qua trái tim của mô hình là bảng liên kết enrollments, đến các nhánh tiến độ, thanh toán và chứng nhận. Ba bài học lớn nhất: (1) dữ liệu phụ thuộc vào từng học viên phải nằm ở bảng liên kết chứ không phải bảng gốc; (2) giá trị tài chính phải được snapshot tại thời điểm giao dịch qua price_paid; và (3) các thực thể phái sinh như chứng chỉ và tiến độ nên trỏ về enrollment_id để giữ đúng ngữ cảnh "ai, khóa nào, lần nào".
enrollments chính là associative entity mạnh mẽ nhất trong toàn bộ schema — hãy ghi nhớ mẫu này, vì bạn sẽ gặp lại nó ở mọi domain có quan hệ nhiều-nhiều mang dữ liệu, từ e-commerce đến ngân hàng. Ở bài kế tiếp, ta sẽ chuyển sang kỹ năng điều phối workshop data modeling — cách dẫn dắt cả một đội cùng xây mô hình như bạn vừa làm một mình hôm nay.