Chuẩn hóa cơ sở dữ liệu: Từ ‘God Tables’ đến Clean Architecture (1NF đến 3NF)

Database tutorial - IT technology blog
Database tutorial - IT technology blog

Vấn đề: Cơn ác mộng ‘God Table’

Bất kỳ lập trình viên nào rồi cũng sẽ có lúc tạo ra một ‘God Table’—một thực thể duy nhất, khổng lồ, cố gắng theo dõi mọi thứ cùng một lúc. Bạn có thể bắt đầu với một bảng tính đơn giản để theo dõi dữ liệu đại học. Bạn tạo một bảng tên là Student_Courses để lưu tên sinh viên, địa chỉ, tên khóa học, giáo sư và vị trí văn phòng. Ban đầu nó có vẻ hiệu quả. Tuy nhiên, ngay khi bạn đạt đến 1.000 hàng, kiến trúc bắt đầu rạn nứt.

Việc cập nhật địa chỉ của một sinh viên không nên đòi hỏi một script để tìm và thay thế ở 15 hàng khác nhau. Nếu một giáo sư chuyển văn phòng, việc bỏ sót dù chỉ một bản ghi cũng tạo ra xung đột ‘nguồn sự thật’ (source of truth). Hơn nữa, bạn thậm chí không thể thêm một khóa học mới vào hệ thống cho đến khi có ít nhất một sinh viên đăng ký vì bảng yêu cầu phải có Student ID cho mỗi mục nhập. Những rào cản này được gọi là các bất thường khi cập nhật và thêm mới (update and insertion anomalies).

Cho dù bạn đang sử dụng PostgreSQL, MySQL hay SQL Server, chuẩn hóa (normalization) là kỹ năng phân biệt dự án của một người nghiệp dư với một hệ thống sản xuất chuyên nghiệp. Đó là quá trình tinh chỉnh cấu trúc dữ liệu của bạn để loại bỏ sự dư thừa và đảm bảo mọi thông tin chỉ nằm ở duy nhất một nơi.

Điểm bắt đầu: Một bảng chưa chuẩn hóa

Hãy xem xét cấu trúc có vấn đề thường thấy trong các bản mẫu giai đoạn đầu này:

-- Đây là một thiết kế lỏng lẻo (Chưa chuẩn hóa)
| StudentID | Name   | Courses                      | Professor       | Prof_Office |
|-----------|--------|------------------------------|-----------------|-------------|
| 101       | Alice  | CS101 (Java), CS102 (SQL)    | Dr. Smith       | Room 401    |
| 102       | Bob    | CS101 (Java)                 | Dr. Smith       | Room 401    |
| 103       | Charlie| CS103 (Python)               | Dr. Brown       | Room 502    |

Bảng này không đạt các tiêu chuẩn quan hệ cơ bản. Cột Courses chứa nhiều giá trị, khiến việc thực hiện một lệnh COUNT() đơn giản cho các lượt đăng ký trở nên gần như không thể nếu không có các thao tác tách chuỗi phức tạp. Hãy khắc phục điều này một cách có hệ thống.

Dạng chuẩn 1 (1NF): Giá trị nguyên tố (Atomic Values)

1NF hướng đến việc loại bỏ các danh sách và đảm bảo mỗi ô dữ liệu là không thể chia nhỏ. Bạn không thể lưu trữ các tập hợp hoặc các chuỗi cách nhau bởi dấu phẩy trong một cột duy nhất. Mỗi bản ghi cũng phải có thể nhận dạng duy nhất bằng một Khóa chính (Primary Key).

Danh sách kiểm tra 1NF:

  • Mỗi ô phải chứa đúng một giá trị duy nhất (tính nguyên tố).
  • Bảng phải có Khóa chính (Primary Key).
  • Không có các nhóm cột lặp lại (ví dụ: Course1, Course2, Course3).

Để đạt được 1NF, chúng ta chia cột Courses sao cho mỗi hàng đại diện cho một cặp sinh viên-khóa học duy nhất. Sau đó, chúng ta xác định một khóa chính tổng hợp (composite primary key) bằng cách sử dụng StudentIDCourseID.

-- Bảng ở dạng 1NF: Đã tách nhỏ giá trị, nhưng vẫn dư thừa
| StudentID | Name   | CourseID | CourseName | Professor | Prof_Office |
|-----------|--------|----------|------------|-----------|-------------|
| 101       | Alice  | CS101    | Java       | Dr. Smith | Room 401    |
| 101       | Alice  | CS102    | SQL        | Dr. Smith | Room 401    |
| 102       | Bob    | CS101    | Java       | Dr. Smith | Room 401    |
| 103       | Charlie| CS103    | Python     | Dr. Brown | Room 502    |

Mặc dù dữ liệu hiện đã ở dạng nguyên tố, sự dư thừa vẫn rất rõ ràng. Tên của Alice được lưu trữ hai lần. Nếu cô ấy đổi tên, chúng ta có nguy cơ bỏ sót một hàng. Sự kém hiệu quả này dẫn chúng ta đến giai đoạn tiếp theo.

Dạng chuẩn 2 (2NF): Loại bỏ phụ thuộc bộ phận (Partial Dependencies)

Một bảng đạt 2NF khi nó thỏa mãn 1NF và đảm bảo rằng mọi cột không phải khóa đều phụ thuộc vào toàn bộ khóa chính. Trong bảng hiện tại của chúng ta, khóa chính là (StudentID, CourseID).

Hãy tự hỏi: Name có phụ thuộc vào khóa học không? Không, nó chỉ phụ thuộc vào StudentID. CourseName có phụ thuộc vào sinh viên không? Không, nó chỉ phụ thuộc vào CourseID. Đây là các ‘Phụ thuộc bộ phận’ (Partial Dependencies). Chúng ta giải quyết vấn đề này bằng cách chia dữ liệu thành ba thực thể riêng biệt.

Phân rã 2NF:

Bảng: Students (Tập trung vào danh tính)

| StudentID | Name   |
|-----------|--------|
| 101       | Alice  |
| 102       | Bob    |
| 103       | Charlie|

Bảng: Courses (Tập trung vào chương trình học)

| CourseID | CourseName | Professor | Prof_Office |
|----------|------------|-----------|-------------|
| CS101    | Java       | Dr. Smith | Room 401    |
| CS102    | SQL        | Dr. Smith | Room 401    |
| CS103    | Python     | Dr. Brown | Room 502    |

Bảng: Enrollments (Xây dựng mối quan hệ)

| StudentID | CourseID |
|-----------|----------|
| 101       | CS101    |
| 101       | CS102    |
| 102       | CS101    |
| 103       | CS103    |

Giờ đây, việc cập nhật hồ sơ của Alice chỉ diễn ra ở đúng một hàng. Tuy nhiên, bảng Courses vẫn còn một thiếu sót. Văn phòng của Dr. Smith bị lặp lại cho mọi khóa học mà ông ấy dạy, điều này dẫn chúng ta đến 3NF.

Dạng chuẩn 3 (3NF): Loại bỏ phụ thuộc bắc cầu (Transitive Dependencies)

3NF yêu cầu các thuộc tính không phải khóa chỉ phụ thuộc duy nhất vào khóa chính, chứ không phụ thuộc vào các thuộc tính không phải khóa khác. Trong bảng Courses của chúng ta, Prof_Office phụ thuộc vào Professor, nhưng Professor không phải là khóa chính. Đây là một phụ thuộc bắc cầu (transitive dependency).

Nếu Dr. Smith quản lý 10 khóa học khác nhau, vị trí văn phòng của ông ấy sẽ được lưu trữ 10 lần. Để khắc phục điều này, chúng ta chuyển chi tiết về giáo sư vào bảng chuyên dụng của riêng họ.

Lược đồ 3NF cuối cùng:

Bảng: Professors

| ProfID | Professor | Prof_Office |
|--------|-----------|-------------|
| P1     | Dr. Smith | Room 401    |
| P2     | Dr. Brown | Room 502    |

Bảng: Courses

| CourseID | CourseName | ProfID |
|----------|------------|--------|
| CS101    | Java       | P1     |
| CS102    | SQL        | P1     |
| CS103    | Python     | P2     |

Thực hành: Triển khai 3NF trong SQL

Chuyển đổi các mô hình logic sang Ngôn ngữ định nghĩa dữ liệu (DDL) là nơi nhiều lập trình viên vấp ngã. Dưới đây là cấu trúc SQL để thực thi các quy tắc này bằng cách sử dụng Khóa ngoại (Foreign Keys) để đảm bảo tính toàn vẹn tối đa.

-- 1. Lưu trữ dữ liệu Giáo sư một lần duy nhất
CREATE TABLE Professors (
    ProfID VARCHAR(10) PRIMARY KEY,
    ProfessorName VARCHAR(100) NOT NULL,
    OfficeLocation VARCHAR(50)
);

-- 2. Làm sạch hồ sơ Sinh viên
CREATE TABLE Students (
    StudentID INT PRIMARY KEY,
    StudentName VARCHAR(100) NOT NULL
);

-- 3. Liên kết Khóa học với Giáo sư
CREATE TABLE Courses (
    CourseID VARCHAR(10) PRIMARY KEY,
    CourseName VARCHAR(100) NOT NULL,
    ProfID VARCHAR(10),
    FOREIGN KEY(ProfID) REFERENCES Professors(ProfID)
);

-- 4. Bảng trung gian cho quan hệ Nhiều-Nhiều
CREATE TABLE Enrollments (
    StudentID INT,
    CourseID VARCHAR(10),
    PRIMARY KEY (StudentID, CourseID),
    FOREIGN KEY (StudentID) REFERENCES Students(StudentID),
    FOREIGN KEY (CourseID) REFERENCES Courses(CourseID)
);

Cấu trúc này đảm bảo rằng bạn không thể gán một khóa học cho một giáo sư không tồn tại. Nó cũng giảm bớt chi phí lưu trữ; trong một hệ thống có 50.000 sinh viên, những thay đổi nhỏ này có thể tiết kiệm hàng trăm megabyte dữ liệu chuỗi dư thừa.

Sự đánh đổi: Khi nào nên dừng lại?

Mặc dù 3NF là tiêu chuẩn vàng cho các hệ thống giao dịch (OLTP), việc chuẩn hóa quá mức vẫn có thể xảy ra. Mỗi lần chia bảng đều yêu cầu thêm một thao tác JOIN. Nếu ứng dụng của bạn cần hiển thị một bảng điều khiển lấy dữ liệu từ 15 bảng khác nhau, hiệu suất truy vấn có thể bị ảnh hưởng.

Trong hầu hết các ứng dụng web, 3NF cung cấp sự cân bằng tốt nhất giữa an toàn dữ liệu và tốc độ. Nếu cuối cùng bạn chuyển sang lĩnh vực kho dữ liệu (OLAP), bạn thực sự có thể ‘phi chuẩn hóa’ (denormalize) dữ liệu trở lại thành các bảng phẳng để tăng tốc các thao tác đọc nặng. Tuy nhiên, đối với hầu hết các lập trình viên, bắt đầu with một schema 3NF sạch sẽ là cách an toàn nhất để ngăn chặn cuộc khủng hoảng toàn vẹn dữ liệu.

Tổng kết

Chuẩn hóa không chỉ là một bài tập lý thuyết cho các kỳ thi khoa học máy tính. Đó là một chiến lược thực tế để xây dựng phần mềm dễ bảo trì và mở rộng. Bằng cách đi qua 1NF, 2NF và 3NF, bạn đảm bảo cơ sở dữ liệu của mình luôn có thể dự đoán được ngay cả khi cơ sở người dùng tăng từ hàng chục lên hàng triệu.

Share: