Menu
ESC

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

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

Đang tải...

Bài 7 — Database Schema Design — Chuẩn 3NF

Technical BA Masterclass Bài 7/60

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

Hãy tưởng tượng bạn là Technical BA cho một dự án thương mại điện tử. Đội dev gửi cho bạn một file Excel "thiết kế bảng đơn hàng" với cấu trúc thế này: một dòng đơn hàng chứa luôn tên khách, số điện thoại, địa chỉ, và một ô duy nhất ghi "Áo thun x2, Quần jean x1, Mũ x3". Mọi thứ trông gọn gàng — cho đến ngày sếp hỏi: "Tháng này bán được bao nhiêu cái mũ?". Lúc đó cả đội mới ngớ người, vì không có cách nào query ra con số đó mà không phải viết code bóc tách chuỗi ký tự.

Đây chính là lý do schema design quan trọng với Technical BA. Bạn không phải là người gõ lệnh CREATE TABLE, nhưng bạn là người định nghĩa dữ liệu nào cần lưu, ràng buộc nào phải tôn trọng, và quan hệ giữa các thực thể nghiệp vụ. Một schema thiết kế tồi sẽ khiến mọi báo cáo về sau trở thành cực hình, dữ liệu mâu thuẫn lẫn nhau, và mỗi thay đổi nhỏ kéo theo hàng loạt bug. Một schema chuẩn hóa tốt thì ngược lại: dữ liệu nhất quán, dễ mở rộng, và spec của bạn được dev tin tưởng.

Trong bài này, chúng ta sẽ đi sâu vào chuẩn hóa (normalization) — cụ thể là ba dạng chuẩn đầu tiên 1NF, 2NF, 3NF — công cụ tư duy nền tảng để bạn đánh giá và thiết kế schema. Đây không phải lý thuyết hàn lâm; đây là bộ lọc thực dụng giúp bạn phát hiện ra những "quả bom hẹn giờ" trong thiết kế dữ liệu trước khi chúng phát nổ trên production.

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

Chuẩn hóa là quá trình tổ chức dữ liệu trong cơ sở dữ liệu để giảm dư thừa (redundancy)tránh bất thường khi cập nhật (update anomaly). Nói đơn giản: mỗi sự thật chỉ nên được lưu ở đúng một nơi. Khi một thông tin bị lặp lại ở nhiều dòng, sớm muộn chúng sẽ "lệch pha" nhau và bạn không biết đâu là dữ liệu đúng.

Trước khi vào từng dạng chuẩn, cần nắm ba khái niệm:

  • Primary Key (khóa chính): cột hoặc tập cột định danh duy nhất mỗi dòng. Ví dụ order_id.
  • Composite Key (khóa phức hợp): khóa chính gồm nhiều cột ghép lại, ví dụ (order_id, product_id).
  • Functional Dependency (phụ thuộc hàm): A → B nghĩa là "biết A thì xác định được B". Ví dụ product_id → product_name: biết mã sản phẩm thì xác định được tên sản phẩm.

1NF — Mỗi ô một giá trị nguyên tử

Một bảng đạt First Normal Form (1NF) khi mỗi ô (cell) chỉ chứa một giá trị nguyên tử (atomic) — không phải danh sách, không phải nhóm lặp lại.

Quay lại ví dụ đầu bài, ô "Áo thun x2, Quần jean x1, Mũ x3" vi phạm 1NF nghiêm trọng. Cũng vi phạm 1NF là kiểu thiết kế "nhiều cột giống nhau" như product_1, product_2, product_3 — vì bạn không bao giờ biết khách sẽ mua bao nhiêu món, và việc query trên các cột này là ác mộng.

Cách sửa: tách mỗi sản phẩm trong đơn thành một dòng riêng trong bảng order_items, mỗi dòng có order_id, product_id, quantity. Bây giờ câu hỏi "bán được bao nhiêu mũ?" chỉ là một câu SUM(quantity) WHERE product_id = ....

2NF — Loại bỏ phụ thuộc một phần

Một bảng đạt Second Normal Form (2NF) khi nó đã ở 1NF mọi cột không phải khóa đều phụ thuộc vào toàn bộ khóa chính, chứ không chỉ một phần của nó.

2NF chỉ có ý nghĩa khi bảng có khóa phức hợp. Hãy xét bảng order_items với khóa (order_id, product_id) và các cột:

order_idproduct_idquantityproduct_nameproduct_price
Vấn đề: product_nameproduct_price chỉ phụ thuộc vào product_id, không phụ thuộc vào order_id. Đây là phụ thuộc một phần (partial dependency). Hậu quả: nếu sản phẩm "Áo thun" xuất hiện trong 5.000 đơn hàng, tên của nó bị lưu lại 5.000 lần. Đổi tên sản phẩm? Bạn phải update 5.000 dòng, và chỉ cần sót một dòng là dữ liệu mâu thuẫn.

Cách sửa: chuyển product_name, product_price sang bảng products riêng (khóa product_id). Bảng order_items chỉ giữ (order_id, product_id, quantity).

3NF — Loại bỏ phụ thuộc bắc cầu

Một bảng đạt Third Normal Form (3NF) khi nó đã ở 2NF không có cột không-khóa nào phụ thuộc vào một cột không-khóa khác. Kiểu phụ thuộc này gọi là phụ thuộc bắc cầu (transitive dependency): khóa → cột A → cột B.

Ví dụ bảng employees:

employee_idnamedepartment_iddepartment_name
Ở đây employee_id → department_id → department_name. Cột department_name không phụ thuộc trực tiếp vào nhân viên, mà phụ thuộc qua department_id. Nếu phòng "Kinh doanh" có 200 nhân viên, tên phòng bị lưu 200 lần. Đổi tên phòng thành "Kinh doanh & Marketing"? Lại phải update 200 dòng.

Cách sửa: tách thành bảng departments (department_id, department_name) và bảng employees chỉ giữ department_id làm khóa ngoại (foreign key).

Một câu thần chú kinh điển để nhớ 3NF: mỗi cột không-khóa phải phụ thuộc vào "the key, the whole key, and nothing but the key" — phụ thuộc vào khóa (1NF/quan hệ), toàn bộ khóa (2NF), và chỉ khóa mà thôi (3NF).

Denormalization — khi nào cố tình "phá luật"

3NF là mục tiêu mặc định, nhưng không phải tín điều tuyệt đối. Đôi khi vì hiệu năng đọc, ta cố tình lưu dư thừa — gọi là denormalization. Ví dụ: lưu total_amount trong bảng orders thay vì tính lại từ order_items mỗi lần. Điểm mấu chốt là: denormalization phải là quyết định có ý thức, được ghi rõ trong spec kèm cơ chế đảm bảo đồng bộ — chứ không phải hệ quả của việc thiết kế cẩu thả.

Tình huống thực tế

Ví dụ 1: Sàn TMĐT "ShopViet" và quả bom giá sản phẩm

ShopViet (giả định) là một sàn thương mại điện tử quy mô vừa ở TP.HCM, khoảng 80.000 đơn/tháng. Phiên bản đầu, đội dev gấp gáp nên thiết kế bảng order_items lưu thẳng product_nameproduct_price vào từng dòng (vi phạm 2NF).

Ban đầu mọi thứ ổn. Vấn đề nổ ra khi marketing chạy chương trình đổi tên và đổi giá sản phẩm theo mùa. Một bạn vận hành sửa giá "Áo khoác mùa đông" từ 350.000đ xuống 290.000đ, nhưng chỉ sửa trong bảng products. Các đơn cũ đã chốt thì giữ giá cũ — điều này đúng về nghiệp vụ. Nhưng vì giá còn bị lưu lặp trong order_items mà không có quy tắc rõ ràng, một số báo cáo lấy giá từ products, số khác lấy từ order_items, dẫn đến doanh thu báo cáo lệch nhau hơn 400 triệu đồng trong một tháng. Kế toán và vận hành cãi nhau cả tuần không ra con số đúng.

Bài học: Technical BA ở đây đáng lẽ phải phân biệt rõ hai loại dữ liệu. Giá hiện tại của sản phẩm thuộc về bảng products (chuẩn 3NF, không lưu lặp). Nhưng giá tại thời điểm mua là một sự thật nghiệp vụ riêng — nó phải được "đóng băng" (snapshot) vào order_items một cách có chủ đích, và spec phải ghi rõ "đây là giá lịch sử, không bao giờ thay đổi theo products". Đây là ranh giới tinh tế giữa lỗi thiết kế và denormalization hợp lý — và chính BA là người phải nhìn ra.

Ví dụ 2: Ngân hàng số và bảng khách hàng "béo phì"

Một ngân hàng số tại Việt Nam (giả định, gọi là VBank) có bảng customers chứa luôn branch_id, branch_name, branch_address, branch_manager. Mỗi khách hàng được gán về một chi nhánh mở tài khoản. Đây là phụ thuộc bắc cầu điển hình: customer_id → branch_id → branch_name.

Với 3 triệu khách hàng và chỉ 120 chi nhánh, thông tin mỗi chi nhánh bị lặp lại trung bình 25.000 lần. Khi ngân hàng sáp nhập và đổi tên một loạt chi nhánh, đội kỹ thuật phải chạy một batch update khổng lồ trên 3 triệu dòng — khóa bảng (lock) làm hệ thống chậm hẳn trong giờ cao điểm, và phát sinh một số dòng update lỗi khiến tên chi nhánh không đồng nhất giữa các báo cáo compliance.

Bài học: Khi tách branches thành bảng riêng (3NF), việc đổi tên 120 chi nhánh chỉ là update 120 dòng — gần như tức thì, không lock hệ thống. Là Technical BA, khi bạn thấy trong spec một bảng "thực thể chính" lại chứa nhiều thuộc tính mô tả một thực thể khác (chi nhánh, phòng ban, nhà cung cấp...), đó là tín hiệu đỏ của phụ thuộc bắc cầu. Hãy đặt câu hỏi: "Thông tin này thực sự thuộc về ai?".

Ví dụ 3: Startup logistics và cột địa chỉ nhồi nhét

Một startup giao hàng ở Hà Nội thiết kế bảng deliveries với một cột duy nhất address kiểu text, ví dụ "Số 12, ngõ 45, Đống Đa, Hà Nội". Lúc đầu ổn, nhưng khi đội phân tích muốn báo cáo "tỷ lệ giao thành công theo quận", họ phát hiện không thể nhóm theo quận vì mọi thứ nằm chung một chuỗi — có dòng ghi "Đống Đa", dòng ghi "Q. Đống Đa", dòng ghi "quận Đống Đa". Đây là dạng vi phạm tinh thần 1NF: một ô gộp nhiều giá trị logic (số nhà, ngõ, quận, thành phố) lẽ ra phải tách riêng và chuẩn hóa.

Bài học: Khi viết spec cho dữ liệu sẽ dùng để phân tích hoặc lọc, BA cần xác định mức độ nguyên tử cần thiết. Nếu nghiệp vụ cần báo cáo theo quận/thành phố, thì districtcity phải là cột riêng (lý tưởng là tham chiếu đến bảng danh mục địa giới hành chính), không nhồi vào một cột text tự do.

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

Đây là quy trình thực dụng để bạn chuẩn hóa một thiết kế dữ liệu khi viết spec:

  • Liệt kê các thực thể nghiệp vụ và thuộc tính. Ghi ra tất cả dữ liệu cần lưu (khách hàng, đơn hàng, sản phẩm...) cùng các trường của chúng, chưa cần quan tâm bảng nào.
  • Áp 1NF — tách giá trị không nguyên tử. Quét xem có ô nào chứa danh sách, nhóm lặp (item1, item2...), hay chuỗi gộp nhiều thông tin không. Tách chúng thành dòng riêng hoặc cột riêng tùy nghiệp vụ.
  • Xác định khóa chính cho từng bảng. Hỏi: "Cái gì định danh duy nhất một dòng?". Nếu là khóa phức hợp, ghi nhận lại để áp 2NF.
  • Áp 2NF — kiểm tra phụ thuộc một phần. Với mỗi bảng có khóa phức hợp, hỏi từng cột không-khóa: "Cột này có phụ thuộc vào toàn bộ khóa không, hay chỉ một phần?". Nếu chỉ một phần, tách nó ra bảng riêng.
  • Áp 3NF — săn phụ thuộc bắc cầu. Với mỗi cột không-khóa, hỏi: "Cột này phụ thuộc trực tiếp vào khóa, hay phụ thuộc qua một cột không-khóa khác?". Nếu phụ thuộc bắc cầu, tách thực thể đó ra bảng riêng và dùng khóa ngoại.
  • Vẽ ERD và xác định quan hệ. Thể hiện các quan hệ 1-1, 1-nhiều, nhiều-nhiều bằng khóa ngoại và bảng trung gian. Bảng nhiều-nhiều (như order_items) chính là sản phẩm tự nhiên của quá trình chuẩn hóa.
  • Rà soát điểm cần denormalize có chủ đích. Hỏi đội dev/DBA: có truy vấn nào quá nặng vì phải JOIN nhiều bảng không? Nếu có, cân nhắc lưu dư thừa một cách có kiểm soát — và ghi rõ trong spec lý do cùng cơ chế đồng bộ.

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

  • Nhầm "giá hiện tại" với "giá lịch sử". Đây là lỗi kinh điển (xem Ví dụ 1). Luôn hỏi: dữ liệu này có cần snapshot tại thời điểm giao dịch không? Nếu có, đó là denormalization hợp lệ, không phải lỗi 2NF.
  • Chuẩn hóa quá đà (over-normalization). Tách mọi thứ thành hàng chục bảng nhỏ khiến mỗi truy vấn phải JOIN 8-10 bảng, hiệu năng tệ và spec khó đọc. 3NF là điểm cân bằng tốt cho hầu hết hệ thống giao dịch (OLTP); đừng cố lên các dạng chuẩn cao hơn nếu không có lý do rõ ràng.
  • Dùng khóa nghiệp vụ làm khóa chính. Tránh lấy số điện thoại, email, hay CMND/CCCD làm khóa chính — chúng có thể thay đổi và gây rắc rối khi cập nhật. Mẹo: dùng khóa kỹ thuật (surrogate key) như ID tự tăng hoặc UUID, đặt các trường nghiệp vụ làm ràng buộc unique.
  • Quên hệ thống phân tích có nhu cầu khác. Mẹo quan trọng: 3NF tối ưu cho hệ thống giao dịch (OLTP), nhưng kho dữ liệu phân tích (OLAP) thường cố tình denormalize để query nhanh. Đừng áp dụng máy móc 3NF cho mọi loại hệ thống — hiểu mục đích sử dụng dữ liệu trước.
  • Mẹo giao tiếp với dev: Khi bạn nghi một bảng vi phạm chuẩn hóa, đừng phán "bảng này sai". Hãy hỏi: "Nếu tên chi nhánh đổi, ta phải update bao nhiêu dòng?". Câu hỏi về update anomaly luôn thuyết phục hơn lý thuyết suông.

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

Cho một bảng course_enrollments (đăng ký khóa học) thiết kế ban đầu như sau, mỗi dòng là một lượt đăng ký:

enrollment_idstudent_namestudent_emailcourse_idcourse_nameinstructor_nameinstructor_phonecourses_taken_list
Trong đó courses_taken_list ghi dạng "BA101, SQL201, API301".

Yêu cầu:

  • Chỉ ra (các) vi phạm 1NF trong bảng này và cách sửa.
  • Giả sử nghiệp vụ cho biết một khóa học chỉ có một giảng viên cố định (course_id → instructor_name). Hãy xác định phụ thuộc bắc cầu vi phạm 3NF.
  • Thiết kế lại thành tập bảng đạt 3NF: liệt kê tên các bảng, cột của từng bảng, khóa chính và khóa ngoại.
  • Câu hỏi mở rộng: Nếu sau này nghiệp vụ thay đổi — một khóa học có thể có nhiều giảng viên đồng giảng — thiết kế của bạn cần thay đổi thế nào?
Gợi ý: Bạn sẽ cần ít nhất các bảng students, courses, instructors, và một bảng quan hệ enrollments. Cột courses_taken_list không nên tồn tại — nó là thông tin có thể suy ra (derive) từ chính bảng enrollments.

Tóm tắt

  • Chuẩn hóa là tổ chức dữ liệu để mỗi sự thật chỉ lưu một nơi, nhằm giảm dư thừa và tránh bất thường khi cập nhật (update anomaly).
  • 1NF: mỗi ô một giá trị nguyên tử — không danh sách, không nhóm lặp.
  • 2NF: 1NF + mọi cột không-khóa phụ thuộc vào toàn bộ khóa chính (chống phụ thuộc một phần, chỉ liên quan khi có khóa phức hợp).
  • 3NF: 2NF + không có phụ thuộc bắc cầu — cột không-khóa không phụ thuộc vào cột không-khóa khác. Thần chú: "the key, the whole key, and nothing but the key".
  • Denormalization là cố tình lưu dư thừa vì hiệu năng — hợp lệ khi là quyết định có chủ đích, ghi rõ trong spec, kèm cơ chế đồng bộ.
  • Vai trò Technical BA: bạn không gõ CREATE TABLE, nhưng bạn là người phát hiện "quả bom" dư thừa dữ liệu, phân biệt giá hiện tại với giá lịch sử, và đặt đúng câu hỏi về update anomaly để hướng dẫn đội dev thiết kế schema bền vững.