Là kỹ sư phần mềm, chúng ta hằng ngày đều làm việc với việc trừu tượng hóa dữ liệu. Cố gắng tìm hiểu các khái niệm mơ hồ trong thế giới thực và trừu tượng hóa chúng thành thông tin có cấu trúc như biến, hàm, lớp, struct, v.v. để phần mềm có thể xử lý. Làm việc với dữ liệu hầu như luôn liên quan đến việc lưu trữ chúng vào một loại kho lưu trữ dữ liệu nào đó, một trong những loại lưu trữ đó là cơ sở dữ liệu quan hệ, thường được gọi là cơ sở dữ liệu SQL. Trong bài viết này, chúng ta sẽ đi qua các dạng chuẩn của cơ sở dữ liệu quan hệ để tìm hiểu chúng là gì, tại sao chúng tốt và tại sao chúng có thể không tốt. Chúng ta sẽ chỉ đề cập đến ba dạng chuẩn đầu tiên vì chúng là phổ biến nhất trong thế giới kỹ thuật phần mềm hiện đại.

Dạng chuẩn thứ nhất

Bây giờ chúng ta sẽ đi sâu vào nền tảng của các dạng chuẩn, dạng chuẩn thứ nhất (1NF). Dạng chuẩn thứ nhất chỉ được thỏa mãn khi không có cột nào trong bảng chứa nhiều giá trị. Một cách chính thức hơn, tất cả các cột chỉ phải chứa một giá trị nguyên tử duy nhất. Không được tồn tại bất kỳ thông tin có ý nghĩa nào khác về dữ liệu bằng cách lấy một tập hợp con của một cột duy nhất. Có một số loại cột không nguyên tử:

Nhóm lặp lại

Một nhóm lặp lại đề cập đến một cột trong một bảng chứa một tập hợp, danh sách hoặc mảng các giá trị. Cột có thể ở bất kỳ định dạng nào, miễn là nó được hiểu là một danh sách, thì đó là một nhóm lặp lại. Ví dụ dưới đây chứa một nhóm lặp lại, tức là cột không nguyên tử, vi phạm 1NF.

[user_information]
------------------------------------------------
| user_id | phone_numbers        | signup_date |
------------------------------------------------
|       1 | (+84) 001, (+84) 002 |  2025-04-21 |
|       2 | (+10) 012            |  2025-04-21 |
|       3 | null                 |  2025-04-21 |
|       4 | (+11) 002            |  2025-04-21 |
|       5 |                      |  2025-04-21 |
------------------------------------------------

phone_numbers chứa một giá trị được phân tách bằng dấu phẩy của nhiều số điện thoại. Định dạng này ổn miễn là chúng ta không chạy bất kỳ truy vấn nào trên đó, nhưng điều đó thường không xảy ra. Cuối cùng, chúng ta sẽ muốn chạy một truy vấn để kiểm tra, chẳng hạn, có bao nhiêu số điện thoại bắt đầu bằng (+01) hoặc để kiểm tra xem một số điện thoại cụ thể đã được sử dụng hay chưa. Việc thực hiện các truy vấn như vậy không hề đơn giản, thường liên quan đến việc khớp và xử lý chuỗi.

Ngoài ra, định dạng này có thể dẫn đến dư thừa dữ liệu. Ví dụ, nếu số điện thoại được phép chia sẻ giữa nhiều người dùng (tài khoản), cùng một số điện thoại phải được lưu trữ ở nhiều nơi, làm cho nó trở nên dư thừa, chiếm nhiều không gian hơn. Vậy, làm thế nào chúng ta có thể chuẩn hóa điều này? Hãy xem bảng dưới đây.

[user_information]
-------------------------
| user_id | signup_date |
-------------------------
|       1 |  2025-04-21 |
|       2 |  2025-04-21 |
|       3 |  2025-04-21 |
|       4 |  2025-04-21 |
|       5 |  2025-04-21 |
-------------------------

[user_phone_numbers]
----------------------------
| user_id | phone_number   |
----------------------------
|       1 | (+84) 001      |
|       1 | (+84) 002      |
|       2 | (+10) 012      |
|       4 | (+11) 002      |
----------------------------

Bây giờ chúng ta đã tách cột phone_numbers thành một bảng riêng biệt. Hai bảng được kết nối bằng user_id, giúp giảm dư thừa dữ liệu và thỏa mãn 1NF. Ban đầu, 1NF đề cập đến các nhóm lặp lại là các cột giữ một mảng làm giá trị của chúng, nhưng nó cũng có thể bao gồm các trường hợp có một nhóm các cột đại diện cho cùng một thuộc tính, như ví dụ dưới đây.

[user_information]
-----------------------------------------------------------
| user_id | phone_number_1 | phone_number_2 | signup_date |
-----------------------------------------------------------
|       1 | (+84) 001      | (+84) 002      |  2025-04-21 |
|       2 | (+10) 012      | null           |  2025-04-21 |
|       3 |                | null           |  2025-04-21 |
|       4 | (+11) 002      | null           |  2025-04-21 |
|       5 | null           |                |  2025-04-21 |
-----------------------------------------------------------

Ví dụ có hai cột tên là phone_number_1phone_number_2, đại diện cho cùng một thuộc tính của bảng, đó là số điện thoại. Tuy nhiên, chúng là các cột có giá trị đơn, vì vậy nói một cách chính xác, nó không vi phạm 1NF. Nhưng nó được coi là một anti-pattern trong thiết kế dữ liệu. Thiết kế này chỉ cho phép hai số điện thoại, nếu chúng ta muốn nhiều hơn thì sao? Nó cũng sẽ làm cho dữ liệu trở nên thưa thớt, trong đó chỉ một số ít người dùng có hai số điện thoại, và phần lớn chỉ có một, gây lãng phí không gian lưu trữ. Vì lý do này, một số tài liệu cũng coi đây là một vi phạm 1NF.

Bảng là một thuộc tính

[user_information]
-------------------------------------------------
| user_id | name     | children                  |
-------------------------------------------------
|         |          |                          |
|         |          |  ----------------------  |
|         |          |  | user_id | name     |  |
|       1 | John Doe |  ----------------------  |
|         |          |  |       3 | Doe Doe  |  |
|         |          |  ----------------------  |
|           |            |                            |
| --------- | ---------- | -------------------------- |
|           |            |                            |
|           |            | ----------------------     |
|           |            |                            | user_id | name    |  |
| 1         | Jane Doe   | ----------------------     |
|           |            |                            | 3       | Doe Doe |  |
|           |            | ----------------------     |
|           |            |                            |
| --------- | ---------- | -------------------------- |
| 3         | Doe Doe    | null                       |
-------------------------------------------------

Một ví dụ khác về cột có nhiều giá trị là sử dụng một bảng khác hoặc một tập hợp con của một bảng làm cột. Nhìn vào ví dụ trên, bạn sẽ nghĩ rằng điều này là không thể, bởi vì hầu hết (nếu không phải tất cả) các cơ sở dữ liệu quan hệ đều không cho phép hành vi này. Bạn đã đúng, ví dụ trên từng là không thể, nhưng bây giờ với ngày càng nhiều cơ sở dữ liệu quan hệ cho phép JSON như một kiểu dữ liệu, về mặt khái niệm, có thể sử dụng một bảng làm cột. Hãy xem ví dụ dưới đây, mặc dù ở một định dạng khác, chúng đại diện cho cùng một ý nghĩa.

[user_information]
------------------------------------------------------
| user_id | name     | children                       |
------------------------------------------------------
|       1 | John Doe | {"id": 3, "name": "Doe Doe" } |
|       2 | Jane Doe | {"id": 3, "name": "Doe Doe" } |
|       3 | Doe Doe  | null                          |
------------------------------------------------------

Lưu trữ dữ liệu như trên vi phạm 1NF vì children là một cột không nguyên tử gây ra sự dư thừa dữ liệu. Khi chúng ta cần cập nhật dữ liệu của Doe Doe, chúng ta cũng cần cập nhật dữ liệu của Doe Doe cho John DoeJane Doe. Nếu không được thực hiện cẩn thận, nó có thể gây ra sự không nhất quán trong dữ liệu. Để tuân thủ 1NF, chúng ta có thể mô hình lại bảng như ví dụ sau.

[user_information]
----------------------
| user_id | name     |
----------------------
|       1 | John Doe |
|       2 | Jane Doe |
|       3 | Doe Doe  |
----------------------

[user_children]
----------------------
| user_id | child_id |
----------------------
|       1 |        3 |
|       2 |        3 |
----------------------

Bây giờ chúng ta có một bảng mới đại diện cho mối quan hệ giữa các thực thể. Mỗi khi dữ liệu của Doe Doe thay đổi, chúng ta không phải lo lắng về việc cập nhật cùng một dữ liệu cho John Doe hoặc Jane Doe. Hơn nữa, bây giờ chúng ta có thể dễ dàng truy vấn hoặc sửa đổi các mối quan hệ mà không cần đến việc khớp và/hoặc phân tích chuỗi phức tạp. Chúng ta cũng có thể mô hình hóa một bảng mịn hơn như dưới đây (lưu ý rằng mịn hơn != tốt hơn).

[user_information]
----------------------
| user_id | name     |
----------------------
|       1 | John Doe |
|       2 | Jane Doe |
|       3 | Doe Doe  |
----------------------

[user_relationships]
---------------------------------------
| user_id | relation  | other_user_id |
---------------------------------------
|       1 | parent_of |             3 |
|       2 | parent_of |             3 |
|       3 |  child_of |             1 |
|       3 |  child_of |             2 |
---------------------------------------

Điều đó nói lên rằng, việc có các cột JSON như đã đề cập ở trên vi phạm 1NF. Tuy nhiên, đó không hoàn toàn là một điều tồi tệ. Trong một số trường hợp sử dụng, việc phi chuẩn hóa dữ liệu để đổi lấy sự cải thiện về tốc độ đọc là thích hợp hơn. Kỹ thuật này là nhúng dữ liệu, lưu trữ một bản ghi nhanh (snapshot) của bất kỳ dữ liệu liên quan nào cùng với dữ liệu chính để bỏ qua việc join hoặc cần nhiều truy vấn, nhưng có thể gây ra sự không nhất quán do các bản ghi có thể bị cũ hơn so với dữ liệu thực tế.

Số lượng thuộc tính không cố định

Với JSON, có một cách khác để vi phạm 1NF: các bảng có số lượng thuộc tính thay đổi.

[user_information]
--------------------------------------------------------------------------------------
| user_id | name     | misc_info                                                     |
--------------------------------------------------------------------------------------
|       1 | John Doe | {"date_of_birth": "2000-01-01"}                               |
|       2 | Jane Doe | {"date_of_birth": "2000-01-01", "hobbies": ["read", "write"]} |
|       3 | Doe Doe  | {"hobbies": ["running"]}                                      |
--------------------------------------------------------------------------------------

Với ví dụ trên, chúng ta có thể nói rằng John Doe có ba thuộc tính giống như Jane DoeDoe Doe, đó là user_id, namemisc_info không? Hay là John Doe có ba thuộc tính: user_id, namedate_of_birth trong khi Jane Doe có bốn thuộc tính: user_id, name, date_of_birthhobbies? Trong trường hợp đầu tiên, chúng ta coi misc_info là một giá trị nguyên tử duy nhất, vì vậy không có vi phạm 1NF. Nhưng trong trường hợp thứ hai, đó là một vi phạm rõ ràng, làm cho bảng phức tạp hơn nhiều để truy vấn và quản lý, và dễ bị sai lệch dữ liệu.

Với tất cả những điều đã nói, chúng ta có thể bị cám dỗ để nghĩ rằng JSON là xấu. Nhưng cũng đã được giải thích rằng nó hoàn toàn phụ thuộc vào cách chúng ta sử dụng nó. Nếu chúng ta sử dụng một cột JSON như một giá trị nguyên tử, thì nói chung là ổn, nhưng nếu chúng ta bắt đầu coi mỗi cặp khóa/giá trị trong JSON là một thuộc tính độc lập (ví dụ: coi date_of_birth bên trong misc_info như thể nó là một cột date_of_birth thông thường cho người dùng), nó có thể gây ra các vấn đề lâu dài khi mô hình dữ liệu phát triển và trở nên phức tạp hơn khi hệ thống phát triển.

Mặt tốt

Toàn bộ vấn đề của 1NF là loại bỏ các nhóm lặp lại và các cột có nhiều giá trị. Các ví dụ đã cho thấy rõ ràng (tôi hy vọng bạn nghĩ vậy!) rằng việc sử dụng một cấu trúc như vậy có thể dẫn đến một mô hình dữ liệu phức tạp về mặt khái niệm, và các truy vấn phức tạp sau này trong quá trình phát triển. Ngoài ra, 1NF giúp giải quyết các vấn đề không nhất quán bằng cách giảm dư thừa dữ liệu, ít nơi chúng ta cần cập nhật cho cùng một mẩu dữ liệu, cơ hội cao hơn là mẩu dữ liệu đó nhất quán trong toàn bộ cơ sở dữ liệu (thông tin của Doe Doe là như nhau bất kể chúng ta nhìn vào bảng nào).

Đây sẽ là điểm sẽ được lặp đi lặp lại khi nói về lợi ích của các dạng chuẩn trong bài viết này. Bởi vì sau tất cả, lý do chính cho sự tồn tại của chúng là để làm điều đó, cải thiện tính nhất quán của dữ liệu bằng cách giảm dư thừa dữ liệu và các dị thường dữ liệu (dị thường chèn, dị thường cập nhật, dị thường xóa).

Mặt xấu

Mặc dù nó giúp ngăn ngừa sự không nhất quán và đơn giản hóa thiết kế mô hình dữ liệu, 1NF không phải là không có thiếu sót. Như chúng ta đã đề cập, đôi khi việc nhúng dữ liệu trực tiếp sẽ hiệu quả hơn là tạo các tham chiếu dữ liệu, điều mà 1NF ngầm khuyến khích. Trên thực tế, một số cơ sở dữ liệu NoSQL thực sự khuyến khích nhúng hơn là tham chiếu, MongoDB là một ví dụ như vậy. Dạng chuẩn thứ nhất cũng không thân thiện để sử dụng với các cấu trúc dữ liệu dạng cây, thường yêu cầu các phép tự nối (self-join) phức tạp cùng với việc triển khai các kỹ thuật như liệt kê đường dẫn (path enumeration), tập hợp lồng nhau (nested set), danh sách kề (adjency list) hoặc nhiều truy vấn đến cơ sở dữ liệu và gộp lại ở tầng ứng dụng.

Tuy nhiên, những lời chỉ trích này không chỉ dành riêng cho 1NF mà còn áp dụng cho các dạng chuẩn cao hơn. Bởi vì các dạng chuẩn khuyến khích việc chia dữ liệu thành các bảng nguyên tử bất cứ khi nào có thể, khiến chúng thường yêu cầu nhiều phép JOIN, có thể làm chậm các truy vấn đọc. Hơn nữa, bằng cách sử dụng các khóa tham chiếu, việc mở rộng quy mô (phân mảnh) dữ liệu thành nhiều vị trí vật lý/logic trong khi vẫn đảm bảo tính toàn vẹn của các khóa là rất phức tạp.

Phụ thuộc hàm

Tôi thấy khó để đi vào các dạng chuẩn tiếp theo mà không hiểu đầy đủ về phụ thuộc hàm là gì. Suy cho cùng, đó là bản chất mà các dạng chuẩn thứ hai và thứ ba đang cố gắng củng cố. Hãy xem nó là gì.

Về mặt hình thức, một thuộc tính A phụ thuộc hàm vào một thuộc tính B (B -> A), chỉ khi tại bất kỳ thời điểm nào, chỉ tồn tại một ánh xạ duy nhất từ B đến A, có nghĩa là khi biết B chúng ta luôn có thể suy ra A là gì.

[books]
----------------------------------------------------------------
| isbn   | author   | title                                    |
----------------------------------------------------------------
| 000001 | John Doe | John Doe's Introduction to Life          |
| 000002 | John Doe | John Doe's Introduction to Life (Vol. 2) |
| 000003 | Jane Doe | How to Laugh!                            |
| 000004 | Jane Doe | The Memory.                              |
----------------------------------------------------------------

Trong ví dụ trên, chúng ta có thể thấy, title phụ thuộc hàm vào isbn (isbn -> title) bởi vì với bất kỳ isbn nào, chúng ta có thể dễ dàng suy ra title. Tương tự, author cũng phụ thuộc hàm vào isbn (isbn -> author). Nhưng title không phụ thuộc hàm vào author, tại sao? Bởi vì với bất kỳ author nào, chúng ta không thể xác định duy nhất một title. Chẳng hạn, bằng cách tham chiếu đến Jane Doe, chúng ta có thể nhận được How to Laugh! hoặc The Memory làm title. Đương nhiên, chúng ta có thể nói rằng authortitle phụ thuộc hàm vào isbn hoặc isbn -> [author, title], chúng ta có thể suy ra authortitle bằng cách sử dụng isbn.

Phụ thuộc hàm đầy đủ

Về mặt hình thức, giả sử rằng một thuộc tính A đã phụ thuộc hàm vào một thuộc tính (hoặc tập hợp các thuộc tính) B (B -> A). Để A phụ thuộc hàm đầy đủ vào B, không được có trường hợp nào A cũng phụ thuộc hàm vào một tập hợp con của B.

Trong thực tế, phụ thuộc hàm đầy đủ được sử dụng trong các bối cảnh có khóa phức hợp. Để một thuộc tính phụ thuộc hàm đầy đủ vào khóa phức hợp, nó không được cung cấp bất kỳ thông tin nào cho bất kỳ tập hợp con nào của khóa phức hợp.

[ratings]
-----------------------------------------
| isbn   | user_id | user_name | rating |
-----------------------------------------
| 000001 |   00002 | A. Readr  |      4 |
| 000001 |   00001 | Avid R.   |      5 |
| 000002 |   00002 | A. Readr  |      3 |
-----------------------------------------

Hãy xem ví dụ trên, giả sử rằng isbnuser_id là một khóa phức hợp. Chúng ta có thể nói rằng rating phụ thuộc hàm vào isbnuser_id (isbn, user_id -> rating) bởi vì với bất kỳ sự kết hợp nào của isbnuser_id, chúng ta chỉ có thể nhận được một giá trị duy nhất của rating. Hơn nữa, rating được coi là phụ thuộc hàm đầy đủ vào isbnuser_id, bởi vì bằng cách chỉ sử dụng độc lập isbn hoặc user_id, chúng ta sẽ nhận được nhiều giá trị của rating. Một cuốn sách có isbn000001 có hai xếp hạng là 45 bởi hai người dùng khác nhau, và user_id00002 cũng cho hai xếp hạng là 43 cho hai cuốn sách khác nhau.

Phụ thuộc hàm riêng phần

Về mặt hình thức, giả sử rằng một thuộc tính A đã phụ thuộc hàm vào một thuộc tính (hoặc tập hợp các thuộc tính) B (B -> A). Để A phụ thuộc hàm riêng phần vào B, nó không được phụ thuộc hàm đầy đủ vào B.

[ratings]
-----------------------------------------
| isbn   | user_id | user_name | rating |
-----------------------------------------
| 000001 |   00002 | A. Readr  |      4 |
| 000001 |   00001 | Avid R.   |      5 |
| 000002 |   00002 | A. Readr  |      3 |
-----------------------------------------

Vẫn sử dụng cùng một ví dụ trong phần phụ thuộc hàm đầy đủ, dưới cùng một giả định rằng isbnuser_id là một khóa phức hợp, nhưng bây giờ hãy tập trung vào user_name thay vì rating. Giống như với rating, chúng ta có thể nói rằng user_name phụ thuộc hàm vào isbnuser_id (isbn, user_id -> user_name) bởi vì với bất kỳ sự kết hợp nào của isbnuser_id, chúng ta chỉ có thể nhận được một giá trị duy nhất của user_name. Nhưng không giống như rating, user_name không phụ thuộc hàm đầy đủ vào isbnuser_id. Bởi vì chỉ bằng cách sử dụng user_id, một tập hợp con của khóa phức hợp isbnuser_id, chúng ta có thể suy ra user_name. Nói cách khác, user_name cũng phụ thuộc hàm vào user_id, đây là một phụ thuộc hàm riêng phần.

Dạng chuẩn thứ hai

Một bảng thỏa mãn dạng chuẩn thứ hai (2NF) chỉ khi:

  • Nó đã thỏa mãn dạng chuẩn thứ nhất.
  • Không có phụ thuộc riêng phần nào giữa khóa và bất kỳ cột không phải khóa nào trong đó.

Tại sao dạng chuẩn thứ hai không cho phép phụ thuộc hàm riêng phần? Bởi vì nó gây ra sự dư thừa dữ liệu, hay còn gọi là dữ liệu trùng lặp. Hãy xem lại ví dụ được mô tả trong phần phụ thuộc hàm.

[ratings]
-----------------------------------------
| isbn   | user_id | user_name | rating |
-----------------------------------------
| 000001 |   00002 | A. Readr  |      4 |
| 000001 |   00001 | Avid R.   |      5 |
| 000002 |   00002 | A. Readr  |      3 |
-----------------------------------------

Như đã thảo luận trước đó, chúng ta có thể thấy rằng user_name phụ thuộc riêng phần vào user_id, điều này làm cho nó vi phạm 2NF. Điều tồi tệ về điều này là nó cho phép dữ liệu user_name trở nên dư thừa. Khi chúng ta cố gắng cập nhật tên của một người dùng, giả sử user_id00002, chúng ta sẽ cần cập nhật tên trong nhiều bản ghi. Đây là một dị thường cập nhật. Chúng ta có thể tranh luận rằng, trong trường hợp này, sự dư thừa dữ liệu không phải là một vấn đề thực sự, bởi vì nó sẽ không gây ra các vấn đề không nhất quán vì truy vấn SQL cho một bản cập nhật như vậy được đảm bảo là nguyên tử.

UPDATE 'ratings'
SET
    'user_name' = "A whole new name"
WHERE
    'user_id' = '00002';

Nhưng các vấn đề có thể vượt ra ngoài các vấn đề không nhất quán, chúng có thể dẫn đến mất dữ liệu. Ví dụ, hãy xem xét người dùng 00099. Vì họ chưa xếp hạng bất kỳ cuốn sách nào trong bảng chưa được chuẩn hóa, chúng ta không có cách nào để biết user_name của họ. Đây là một dị thường chèn. Tương tự, nếu người dùng 00001 xóa xếp hạng duy nhất của họ, chúng ta sẽ mất thông tin rằng user_name của họ là Avid R.. Đây là một dị thường xóa.

Trên một quan điểm thực tế hơn, một dị thường cập nhật cũng có thể làm chậm tốc độ ghi. Vì nhiều bản ghi cần được cập nhật, cơ sở dữ liệu có thể cần tải nhiều tệp vật lý và cập nhật tất cả chúng, dẫn đến nhiều hàng cần được khóa hơn, nhiều trang hoặc chỉ mục cần được quét hơn, sử dụng nhiều tài nguyên hơn (CPU, RAM). Với tất cả những điều đã nói, làm thế nào để chúng ta chuyển đổi ví dụ này để nó thỏa mãn 2NF?

[ratings]
-----------------------------
| isbn   | user_id | rating |
-----------------------------
| 000001 |   00002 |      4 |
| 000001 |   00001 |      5 |
| 000002 |   00002 |      3 |
-----------------------------

[user_information]
-----------------------
| user_id | user_name |
-----------------------
|   00001 | Avid R.   |
|   00002 | A. Readr  |
|   00099 | Anon U.   |
-----------------------

Bây giờ chúng ta tách bảng trước đó thành hai bảng riêng biệt, một bảng lưu trữ các xếp hạng, một bảng khác lưu trữ thông tin người dùng. Với cấu trúc này, chúng ta loại bỏ sự phụ thuộc riêng phần giữa user_iduser_name, làm cho chúng thỏa mãn 2NF. Lợi ích là bây giờ mỗi khi chúng ta cần cập nhật thông tin người dùng, chúng ta chỉ cần cập nhật một bản ghi trong bảng thông tin người dùng, loại bỏ các dị thường cập nhật. Và ngay cả khi không có xếp hạng, chúng ta vẫn biết thông tin của người dùng 00099, điều này loại bỏ dị thường chèn. Dị thường xóa cũng đã được loại bỏ vì người dùng 00001 bây giờ có thể xóa xếp hạng của họ mà không gây ra bất kỳ mất mát thông tin người dùng nào.

Vì 2NF khuyến khích việc chia một bảng duy nhất thành nhiều bảng để loại bỏ sự phụ thuộc riêng phần. Nó có thể có tác động đến hiệu suất đọc. Chúng ta giả định rằng với mỗi xếp hạng, chúng ta cũng cần biết tên của người đã thực hiện xếp hạng. Việc lưu trữ user_name cùng với các xếp hạng là hợp lý, đặc biệt nếu xếp hạng này được truy vấn rất thường xuyên, loại bỏ nhu cầu join các bảng thường xuyên. Chúng ta có thể thiết kế các bảng như sau.

[ratings_with_user_name]
-----------------------------------------
| isbn   | user_id | user_name | rating |
-----------------------------------------
| 000001 |   00002 | A. Readr  |      4 |
| 000001 |   00001 | Avid R.   |      5 |
| 000002 |   00002 | A. Readr  |      3 |
-----------------------------------------

[user_information]
-----------------------
| user_id | user_name |
-----------------------
|   00001 | Avid R.   |
|   00002 | A. Readr  |
|   00099 | Anon U.   |
-----------------------

Với cấu trúc này, nó đảm bảo không có dị thường chèn hoặc xóa, nhưng nó lại giới thiệu lại dị thường cập nhật. Sự đánh đổi của một cấu trúc phi chuẩn hóa như vậy là nó có thể cải thiện hiệu suất đọc trong khi có thể làm giảm hiệu suất ghi và có thể dẫn đến dữ liệu không nhất quán. Cuối cùng, nó phụ thuộc vào trường hợp sử dụng để chúng ta quyết định cách tiếp cận tốt nhất cho loại đánh đổi này.

Dạng chuẩn thứ ba

Một bảng thỏa mãn dạng chuẩn thứ ba (3NF) khi:

  • Nó ở dạng chuẩn thứ hai
  • Không có phụ thuộc bắc cầu nào giữa cột khóa và bất kỳ cột không phải khóa nào

Một phụ thuộc bắc cầu có thể được hiểu là khi một cột không phải khóa phụ thuộc hàm vào một cột không phải khóa khác, mà cột đó lại phụ thuộc hàm vào cột khóa. Một cách chính thức hơn, nếu chúng ta có một bảng với các thuộc tính A, BCC là khóa, thì nếu:

  • Nếu không phải khóa A phụ thuộc hàm vào không phải khóa B (B -> A)
  • Và không phải khóa B phụ thuộc hàm vào khóa C (C -> B)
  • Thì không phải khóa A phụ thuộc bắc cầu vào khóa C (C -> B -> A)

Để đơn giản hóa, 3NF đảm bảo rằng trong một bảng, không có cột không phải khóa nào có thể phụ thuộc hàm vào một cột không phải khóa khác. Các vấn đề mà 3NF cố gắng giải quyết? Giống như 2NF, hãy xem một ví dụ để hiểu.

[book_with_publisher]
--------------------------------------------------------------------------------------------
| isbn   | title                                    | publisher_id | publisher_name        |
--------------------------------------------------------------------------------------------
| 000001 | John Doe's Introduction to Life          |        P0001 | First Time Publishing |
| 000002 | John Doe's Introduction to Life (Vol. 2) |        P0002 | Pub. Ltd.             |
| 000003 | How to Laugh!                            |        P0001 | First Time Publishing |
| 000004 | The Memory.                              |        P0003 | Yet Another Publisher |
--------------------------------------------------------------------------------------------

Trong bảng book_with_publisher, với isbn là khóa. Nó vi phạm 3NF bằng cách có publisher_name, một cột không phải khóa, phụ thuộc hàm vào publisher_id, một cột không phải khóa khác. Ví dụ này cũng gặp phải cùng một tập hợp các vấn đề được đề cập trong 2NF, đó là:

  • Dị thường chèn: Nếu một nhà xuất bản chưa bao giờ xuất bản bất kỳ cuốn sách nào, không thể biết thông tin của nó
  • Dị thường xóa: Nếu cuốn sách có isbn000002 bị xóa, thông tin của nhà xuất bản P0002 sẽ bị mất
  • Dị thường cập nhật: Nếu publisher_name cần thay đổi, chúng ta sẽ cần cập nhật nhiều bản ghi
  • Dư thừa dữ liệu: Cùng một tên nhà xuất bản được lưu trữ ở nhiều nơi

Để thỏa mãn 3NF, chúng ta cần loại bỏ sự phụ thuộc bắc cầu của isbn -> publisher_id -> publisher_name bằng cách chuẩn hóa bảng thành hai bảng sau:

[books]
--------------------------------------------------------------------
| isbn   | title                                    | publisher_id |
--------------------------------------------------------------------
| 000001 | John Doe's Introduction to Life          |        P0001 |
| 000002 | John Doe's Introduction to Life (Vol. 2) |        P0002 |
| 000003 | How to Laugh!                            |        P0001 |
| 000004 | The Memory.                              |        P0003 |
--------------------------------------------------------------------

[publishers]
----------------------------------------
| publisher_id | publisher_name        |
----------------------------------------
|        P0001 | First Time Publishing |
|        P0002 | Pub. Ltd.             |
|        P0003 | Yet Another Publisher |
|        P0004 | Anon Publishing       |
----------------------------------------

Như chúng ta đã thấy với 1NF và 2NF, việc chuẩn hóa này cải thiện tính toàn vẹn của dữ liệu và giảm sự dư thừa. Tuy nhiên, điều đáng nhớ là sự đánh đổi phổ biến: nhiều bảng hơn có nghĩa là nhiều phép join hơn cho các truy vấn nhất định, điều này có thể ảnh hưởng đến hiệu suất đọc. Quyết định khi nào và mức độ chuẩn hóa thường phụ thuộc vào việc cân bằng các yếu tố này cho trường hợp sử dụng cụ thể của bạn.

Tài liệu tham khảo