Menu
ESC

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

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

Đang tải...

Bài 13 — Normalization — 2NF Partial Dependencies

Data Modeling and ERD Mastery Bài 13/60

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

Ở bài trước, chúng ta đã đưa dữ liệu về dạng chuẩn 1 (1NF) — nghĩa là mỗi ô chỉ chứa một giá trị nguyên tử, không còn nhóm lặp, mỗi dòng được nhận diện duy nhất bằng một khóa chính. Nhưng 1NF mới chỉ là "dọn nhà cửa cho gọn". Nó chưa giải quyết được một loại bệnh âm thầm nhưng cực kỳ phổ biến trong các bảng dữ liệu thực tế: partial dependency — phụ thuộc bộ phận.

Tôi muốn bạn hình dung thế này. Bạn là một BA (Business Analyst) đang thiết kế hệ thống cho một chuỗi cửa hàng. Bạn có một bảng ghi lại từng dòng sản phẩm trong từng đơn hàng. Mỗi khi khách đặt 3 món, bạn ghi 3 dòng. Và ở mỗi dòng đó, bên cạnh mã đơn hàng và mã sản phẩm, bạn còn ghi cả tên sản phẩm, giá niêm yết, tên nhà cung cấp... Trông thì tiện, nhưng bạn vừa gieo một quả bom hẹn giờ. Khi tên sản phẩm thay đổi, bạn phải sửa ở hàng trăm dòng. Sửa sót một dòng — dữ liệu mâu thuẫn ngay. Đó chính xác là vấn đề mà dạng chuẩn 2 (Second Normal Form — 2NF) sinh ra để chữa.

Với một BA, hiểu 2NF không phải để "khoe lý thuyết" với đội dev. Nó giúp bạn nhìn ra sớm — ngay từ giai đoạn mô hình hóa logic — những chỗ dữ liệu sẽ bị trùng lặp, bị mâu thuẫn (anomaly), và tốn công bảo trì về sau. Đây là kỹ năng tách bạch giữa một mô hình "chạy được" và một mô hình "bền vững". Bài này tập trung duy nhất vào 2NF và partial dependency. 3NF (transitive dependency) là chuyện của bài kế tiếp.

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

Định nghĩa 2NF

Một bảng đạt dạng chuẩn 2 (2NF) khi thỏa đồng thời hai điều kiện:

  • Bảng đã ở 1NF (giá trị nguyên tử, có khóa chính rõ ràng).
  • Mọi thuộc tính không phải khóa (non-key attribute) phải phụ thuộc đầy đủ vào toàn bộ khóa chính — không được phụ thuộc vào chỉ một phần của khóa chính.
Vế thứ hai chính là tinh thần của bài này. Cụm "phụ thuộc đầy đủ" (fully functional dependency) và mặt trái của nó — "phụ thuộc bộ phận" (partial dependency) — là trung tâm.

Functional dependency — nền tảng cần nhắc lại

Ta nói thuộc tính B phụ thuộc hàm vào thuộc tính A (viết là A → B) nếu: biết giá trị của A thì xác định được duy nhất một giá trị của B. Ví dụ MaSinhVien → HoTen: biết mã sinh viên là xác định được tên. Ngược lại không đúng — biết tên không suy ra được mã (trùng tên đầy nhan nhản ở Việt Nam, biết bao nhiêu "Nguyễn Văn An").

Partial dependency là gì?

Partial dependency chỉ xuất hiện khi khóa chính là khóa kết hợp (composite key) — tức gồm từ hai cột trở lên. Nếu khóa chính chỉ có một cột duy nhất, thì về mặt logic không thể có phụ thuộc bộ phận, và bảng đã ở 1NF thì tự động ở 2NF luôn. Đây là một mẹo quan trọng: 2NF chỉ có ý nghĩa khi bạn đang dùng composite key.

Giả sử khóa chính là (A, B). Partial dependency xảy ra khi một thuộc tính không-khóa C chỉ phụ thuộc vào A, hoặc chỉ phụ thuộc vào B, chứ không cần cả hai. Tức là A → C (chỉ một phần của khóa đã đủ xác định C). Khi đó C "phụ thuộc bộ phận" vào khóa, và bảng vi phạm 2NF.

Vì sao partial dependency gây hại?

Khi C chỉ phụ thuộc vào A, nhưng bạn lại lưu C trong bảng có khóa (A, B), thì giá trị của C sẽ bị lặp lại ở mỗi cặp (A, B) chia sẻ cùng một A. Lặp lại dẫn đến ba loại bất thường kinh điển:

  • Update anomaly (bất thường khi cập nhật): muốn đổi giá trị C cho một A, bạn phải sửa ở nhiều dòng. Sửa sót → mâu thuẫn.
  • Insertion anomaly (bất thường khi thêm): bạn không thể lưu thông tin về A nếu chưa có B đi kèm (vì B là một phần khóa, không được rỗng).
  • Deletion anomaly (bất thường khi xóa): xóa dòng cuối cùng chứa một A nào đó, bạn vô tình mất luôn thông tin C của A.
Mục tiêu của 2NF: tách mỗi nhóm phụ thuộc bộ phận ra một bảng riêng, nơi nó được phụ thuộc đầy đủ vào khóa của bảng mới.

Tình huống thực tế

Ví dụ 1 — Chuỗi cà phê The Coffee House và bảng chi tiết đơn hàng

Hãy tưởng tượng đội kỹ thuật của một chuỗi đồ uống như The Coffee House lưu chi tiết đơn hàng trong một bảng phẳng ChiTietDonHang:

MaDonMaSPTenSPGiaNiemYetSoLuongTenKhuyenMai
DH001SP10Trà đào cam sả45.0002Mua 1 tặng 1
DH001SP22Cà phê sữa đá35.0001Mua 1 tặng 1
DH002SP10Trà đào cam sả45.0003(rỗng)
DH003SP10Trà đào cam sả45.0001(rỗng)
Khóa chính ở đây là composite key (MaDon, MaSP) — vì một đơn có nhiều sản phẩm, và một sản phẩm xuất hiện ở nhiều đơn, nên phải cả hai cột mới nhận diện duy nhất một dòng.

Bây giờ soi từng thuộc tính không-khóa:

  • TenSPGiaNiemYet phụ thuộc vào cái gì? Chỉ cần MaSP là biết tên và giá. Tức MaSP → TenSP, GiaNiemYet. Đây là partial dependency — chúng chỉ phụ thuộc một phần (MaSP) của khóa (MaDon, MaSP).
  • TenKhuyenMai phụ thuộc vào MaDon (khuyến mãi gắn với cả đơn). Tức MaDon → TenKhuyenMai. Cũng là partial dependency.
  • SoLuong thì cần cả MaDonMaSP mới xác định được (đơn DH001 mua SP10 với số lượng 2). Đây mới là phụ thuộc đầy đủ — đúng chỗ.
Diễn giải: Để ý Trà đào cam sả / 45.000 bị lặp ba lần. Hôm nào marketing đổi giá trà đào lên 49.000, dev phải UPDATE nhiều dòng. Quên một dòng — báo cáo doanh thu sai lệch ngay. Và nếu cửa hàng vừa nhập một sản phẩm mới chưa ai mua, bạn không thể lưu nó vào đây vì chưa có MaDon (vi phạm insertion).

Cách chữa — tách bảng:

  • SanPham(MaSP PK, TenSP, GiaNiemYet) — gom các thuộc tính phụ thuộc vào MaSP.
  • DonHang(MaDon PK, TenKhuyenMai) — gom thuộc tính phụ thuộc vào MaDon.
  • ChiTietDonHang(MaDon, MaSP, SoLuong) với PK (MaDon, MaSP) — chỉ giữ lại thuộc tính phụ thuộc đầy đủ.
Bài học rút ra: Mỗi sự thật chỉ nên được ghi ở đúng một nơi. Tên và giá sản phẩm là sự thật về sản phẩm, không phải về dòng đơn hàng — nên nó thuộc về bảng SanPham.

Ví dụ 2 — Hệ thống đăng ký môn học của một trường đại học

Một trường đại học ở TP.HCM thiết kế bảng DangKy ghi nhận sinh viên đăng ký lớp học phần:

MaSVMaLopHPHoTenSVNganhHocTenMonHocSoTinChiDiemTongKet
SV001LHP301Trần MinhCNTTCơ sở dữ liệu38.5
SV001LHP402Trần MinhCNTTMạng máy tính37.0
SV002LHP301Lê HoaKế toánCơ sở dữ liệu39.0
Khóa chính: (MaSV, MaLopHP). Phân tích:

  • HoTenSV, NganhHoc chỉ phụ thuộc MaSV → partial dependency.
  • TenMonHoc, SoTinChi chỉ phụ thuộc MaLopHP → partial dependency.
  • DiemTongKet cần cả MaSV lẫn MaLopHP → phụ thuộc đầy đủ, đúng chỗ.
Diễn giải: Tên "Trần Minh" và ngành "CNTT" bị lặp ở mọi lớp em ấy đăng ký. Nếu Trần Minh chuyển ngành, ta phải sửa hàng chục dòng. Tệ hơn: một lớp học phần vừa mở nhưng chưa ai đăng ký thì không lưu được thông tin môn học và số tín chỉ — đó là deletion/insertion anomaly nguy hiểm với phòng đào tạo.

Cách chữa:

  • SinhVien(MaSV PK, HoTenSV, NganhHoc)
  • LopHocPhan(MaLopHP PK, TenMonHoc, SoTinChi)
  • DangKy(MaSV, MaLopHP, DiemTongKet) — PK (MaSV, MaLopHP).
Bài học rút ra: Khi bạn thấy một thuộc tính lặp lại theo "một bên" của khóa kết hợp, gần như chắc chắn đó là tín hiệu của partial dependency cần tách ra.

Ví dụ 3 — Khi tách 2NF lại là quyết định nghiệp vụ, không chỉ kỹ thuật

Một sàn thương mại điện tử kiểu Shopee/Tiki lưu bảng GioHang(MaKhach, MaSP, GiaTaiThoiDiemThem, TenSP), PK (MaKhach, MaSP).

Thoạt nhìn TenSP phụ thuộc bộ phận vào MaSP — tách ra theo 2NF là đúng. Nhưng cột GiaTaiThoiDiemThem thì khác: đây là giá tại đúng thời điểm khách thêm vào giỏ, nên nó cần cả MaKhachMaSP (và thực chất gắn với hành vi thêm giỏ của khách đó). Nó là phụ thuộc đầy đủ — phải giữ lại trong bảng giỏ.

Diễn giải: Đây là bẫy hay gặp. Nhiều bạn mới thấy có chữ "Gia" là vội tách sang bảng SanPham. Nhưng giá-lúc-thêm-giỏ và giá-niêm-yết là hai sự thật khác nhau. Giá niêm yết phụ thuộc MaSP (đưa sang SanPham); giá-lúc-thêm-giỏ phụ thuộc đầy đủ vào (MaKhach, MaSP) (giữ lại). Phân biệt được điều này đòi hỏi bạn hiểu nghiệp vụ, không chỉ nhìn tên cột.

Bài học rút ra: Chuẩn hóa không phải thao tác máy móc. Trước khi tách, hãy hỏi: "Thuộc tính này thực sự là sự thật về ai/cái gì?" Trả lời được câu đó thì xác định phụ thuộc mới chính xác.

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

Đây là quy trình tôi vẫn dùng khi review một bảng để đưa về 2NF:

Bước 1 — Xác nhận bảng đã ở 1NF. Không còn ô đa giá trị, không còn nhóm lặp, có khóa chính rõ ràng. Nếu chưa, quay lại xử lý 1NF trước.

Bước 2 — Xác định khóa chính và xem nó có phải composite key không. Nếu khóa chỉ một cột → bảng đã ở 2NF, dừng lại. Nếu khóa gồm nhiều cột → đi tiếp.

Bước 3 — Liệt kê toàn bộ thuộc tính không-khóa. Viết ra danh sách những cột không nằm trong khóa chính.

Bước 4 — Với mỗi thuộc tính không-khóa, đặt câu hỏi vàng: "Để xác định giá trị này, tôi có cần toàn bộ khóa chính không, hay chỉ cần một phần?" Nếu chỉ cần một phần → đánh dấu đó là partial dependency.

Bước 5 — Nhóm các thuộc tính theo phần khóa mà chúng phụ thuộc. Mọi thuộc tính chỉ phụ thuộc cột A gom thành một nhóm; mọi thuộc tính chỉ phụ thuộc cột B thành nhóm khác.

Bước 6 — Tách mỗi nhóm phụ thuộc bộ phận thành một bảng mới, với khóa chính là phần khóa mà nhóm đó phụ thuộc. Phần khóa đó trở thành PK ở bảng mới và đồng thời là foreign key ở bảng gốc.

Bước 7 — Giữ lại trong bảng gốc đúng những thuộc tính phụ thuộc đầy đủ vào toàn bộ composite key, cộng với khóa chính kết hợp.

Bước 8 — Kiểm tra lại: mỗi bảng mới giờ có còn partial dependency không? (Thường là không, vì khóa mới là khóa đơn.) Và bảng gốc đã sạch chưa?

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

Lỗi 1 — Tưởng mọi bảng đều phải lo 2NF. Không. Nếu khóa chính là khóa đơn (một cột), bảng ở 1NF đã tự động ở 2NF. Đừng tốn công soi partial dependency ở chỗ không thể có nó.

Lỗi 2 — Nhầm partial dependency với transitive dependency. Partial: non-key phụ thuộc một phần của khóa. Transitive (sẽ học ở bài 3NF): non-key phụ thuộc vào một non-key khác. Hai bệnh khác nhau, hai dạng chuẩn khác nhau. Ở 2NF, bạn chỉ săn loại thứ nhất.

Lỗi 3 — Tách theo tên cột thay vì theo phụ thuộc thật. Như ví dụ giỏ hàng: cùng chữ "Gia" nhưng giá-niêm-yết và giá-lúc-thêm-giỏ thuộc về hai bảng khác nhau. Luôn hỏi nghiệp vụ.

Lỗi 4 — Quên thiết lập foreign key sau khi tách. Sau khi đưa TenSP sang bảng SanPham, MaSP trong bảng chi tiết phải là FK trỏ về. Quên bước này thì dữ liệu mất tính toàn vẹn tham chiếu.

Mẹo — "Quy tắc một bên khóa". Khi nhìn dữ liệu mẫu, nếu bạn thấy một cột có giá trị lặp đi lặp lại mỗi khi một phần của khóa lặp lại (ví dụ cứ MaSP = SP10TenSP luôn là "Trà đào"), đó gần như chắc chắn là partial dependency. Đây là cách phát hiện nhanh bằng mắt thường mà không cần vẽ sơ đồ phụ thuộc đầy đủ.

Mẹo — Với BA, hãy chốt phụ thuộc ngay ở mô hình logic. Đừng để đến lúc dev dựng bảng vật lý mới phát hiện. Một sơ đồ phụ thuộc hàm rõ ràng trong data dictionary giúp cả đội cùng nhìn thấy chỗ cần tách.

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

Cho bảng BanHang của một nhà thuốc, đã ở 1NF, khóa chính (MaHoaDon, MaThuoc):

MaHoaDonMaThuocTenThuocDonViTinhSoLuongNgayBanTenDuocSi
HD01T100ParacetamolVỉ227/06/2026Phạm Lan
HD01T205Vitamin CHộp127/06/2026Phạm Lan
HD02T100ParacetamolVỉ528/06/2026Lê Huy
Yêu cầu:

  • Liệt kê tất cả phụ thuộc hàm bạn nhận ra trong bảng này.
  • Chỉ ra cột nào là partial dependency, cột nào phụ thuộc đầy đủ vào khóa (MaHoaDon, MaThuoc).
  • Tách bảng về 2NF, ghi rõ tên bảng, khóa chính và khóa ngoại của từng bảng.
  • Nêu một update anomaly cụ thể có thể xảy ra nếu không chuẩn hóa.
Gợi ý đáp án (tự kiểm sau khi làm): MaThuoc → TenThuoc, DonViTinh (partial); MaHoaDon → NgayBan, TenDuocSi (partial); SoLuong cần cả hai (đầy đủ). Kết quả tách: Thuoc(MaThuoc PK, TenThuoc, DonViTinh), HoaDon(MaHoaDon PK, NgayBan, TenDuocSi), ChiTietBan(MaHoaDon FK, MaThuoc FK, SoLuong) với PK (MaHoaDon, MaThuoc). Anomaly: đổi tên hiển thị "Paracetamol" thành "Paracetamol 500mg" phải sửa nhiều dòng — sót dòng nào thì báo cáo thuốc bị nhập nhằng.

Tóm tắt

  • 2NF = 1NF + không còn partial dependency. Mọi thuộc tính không-khóa phải phụ thuộc đầy đủ vào toàn bộ khóa chính.
  • Partial dependency chỉ xảy ra với composite key. Khóa đơn → bảng ở 1NF là tự động ở 2NF.
  • Câu hỏi vàng: "Thuộc tính này cần toàn bộ khóa, hay chỉ một phần?" Nếu chỉ một phần — tách ra bảng riêng.
  • Cách chữa: gom các thuộc tính phụ thuộc cùng một phần khóa thành bảng mới, lấy phần khóa đó làm PK, đặt FK ở bảng gốc, giữ lại trong bảng gốc đúng những thuộc tính phụ thuộc đầy đủ.
  • Lợi ích thực tế: giảm trùng lặp, loại bỏ update/insert/delete anomaly, dữ liệu dễ bảo trì. Với BA, chốt phụ thuộc ngay từ mô hình logic là cách phòng bệnh rẻ nhất.
Ở bài tiếp theo, chúng ta sẽ đi tiếp một tầng sâu hơn — 3NF và transitive dependency, nơi vấn đề không còn nằm ở khóa kết hợp mà ở chuyện một thuộc tính không-khóa lại "nuôi" một thuộc tính không-khóa khác.