Trong bài này
Câu lạc bộ muốn đổi tên lớp Python thành Python cơ bản. Người quản lý mở danh sách đăng ký và thấy nhiều dòng cùng lưu tên lớp Python. Nếu chỉ sửa một chỗ, phụ huynh sẽ nhìn thấy hai tên cho cùng một lớp. Vấn đề bắt đầu từ cách lưu thông tin, trước cả lúc viết báo cáo.
Chép tên lớp nhiều lần khiến một lần sửa thành nhiều việc
Trước tiên, hãy xem một thiết kế khác với bộ dữ liệu chuẩn. Bảng flat_regs dưới đây gộp tên học viên và tên lớp vào mỗi đăng ký. Bảng này dùng ba đăng ký có thật trong dữ liệu mẫu để bạn dễ đối chiếu. Đây là bảng minh họa riêng, không thay đổi bảng enrollments của bộ dữ liệu chung.
Bất thường cập nhật (update anomaly) xảy ra khi một sự thật được lưu nhiều nơi và bị sửa không đồng bộ. Tên của lớp mã 1 phải thống nhất, nhưng bảng gộp lưu tên ấy trên nhiều dòng. Người quản lý chỉ sửa dòng của An:
Dữ liệu đầu vào
flat_regs3 dòng| student_id | student_name | class_id | class_name | status |
|---|---|---|---|---|
| 1 | An | 1 | Python | active |
| 1 | An | 3 | Database | active |
| 2 | Bình | 1 | Python | active |
Câu lệnh
UPDATE flat_regs
SET class_name = 'Python cơ bản'
WHERE student_id = 1 AND class_id = 1;Bảng flat_regs sau khi chạy
flat_regs1 dòng thay đổi| student_id | student_name | class_id | class_name | status |
|---|---|---|---|---|
| 1 | An | 1 | Python cơ bản | active |
| 1 | An | 3 | Database | active |
| 2 | Bình | 1 | Python | active |
Sau thao tác, dòng của An mang tên Python cơ bản. Dòng của Bình vẫn mang tên Python dù cùng class_id = 1. Người đọc không biết tên nào là tên hiện tại.
Câu lệnh chạy được, nhưng chỉ sửa một trong hai nơi lưu tên lớp. Vì vậy, cùng một lớp lại có hai tên khác nhau.
Bạn chưa cần tự viết UPDATE ở bài này. Hãy đọc nó như ba bước: chọn bảng cần sửa, đặt tên mới, giới hạn dòng được sửa. Bài 26 sẽ dạy cách thực hiện cập nhật an toàn. Ở đây, câu lệnh giúp bạn thấy hậu quả của thiết kế bảng gộp.
| Sự thật | Bảng gộp lưu ở đâu? | Rủi ro |
|---|---|---|
Tên học viên mã 1 | Mỗi đăng ký của người ấy | Sửa thiếu một bản sao |
Tên lớp mã 1 | Mỗi đăng ký vào lớp ấy | Cùng lớp có nhiều tên |
| Trạng thái đăng ký | Đúng cặp người–lớp | Có thể khác giữa các cặp |
Một giá trị lặp không phải lúc nào cũng gây lỗi. Điều cần hỏi là những ô đó có cùng mô tả một sự thật hay không. Các ô lưu tên của lớp mã 1 phải có cùng một tên. Hai trạng thái của hai đăng ký khác nhau thì được phép khác nhau.
Tách lớp khỏi đăng ký để lớp tồn tại độc lập
Giả sử quản lý muốn mở lớp mới nhưng chưa có học viên đăng ký. Nếu chỉ có bảng gộp, người ấy phải tìm một dòng đăng ký để đặt tên lớp. Với cách lưu này, quản lý phải có học viên đăng ký trước thì mới ghi được tên lớp. Trong thực tế, lớp có thể được mở trước khi ai đăng ký.
Bất thường thêm (insertion anomaly) là việc không lưu được một sự thật mới vì thiếu một sự thật khác chưa cần có. Nếu thêm học viên giả để có dòng lưu tên lớp, bạn vẫn chưa giải quyết được vấn đề của bảng gộp. Nó khiến danh sách học viên và số đăng ký đều sai.
Người quản lý thử giữ tên lớp Robotics bằng một dòng chưa có học viên. Ta dùng bản sao của bảng gộp và thêm dòng giữ chỗ đó:
Dữ liệu đầu vào
flat_regs3 dòng| student_id | student_name | class_id | class_name | status |
|---|---|---|---|---|
| 1 | An | 1 | Python | active |
| 1 | An | 3 | Database | active |
| 2 | Bình | 1 | Python | active |
Câu lệnh
INSERT INTO flat_regs
VALUES (NULL, NULL, 4, 'Robotics', NULL);Bảng flat_regs sau khi chạy
flat_regs1 dòng thay đổi| student_id | student_name | class_id | class_name | status |
|---|---|---|---|---|
| 1 | An | 1 | Python | active |
| 1 | An | 3 | Database | active |
| 2 | Bình | 1 | Python | active |
| NULL | NULL | 4 | Robotics | NULL |
Dòng mới có mã lớp 4, nhưng mã và tên học viên đều là NULL. Bảng gộp cho phép lưu dòng này dù chưa có ai đăng ký.
Để đọc lại bảng vừa thêm, ví dụ tiếp theo dùng bản sao có cùng ba đăng ký và dòng giữ chỗ:
Hãy đếm các dòng trong bảng gộp theo tên lớp:
Dữ liệu đầu vào
flat_regs4 dòng| student_id | student_name | class_id | class_name | status |
|---|---|---|---|---|
| 1 | An | 1 | Python | active |
| 1 | An | 3 | Database | active |
| 2 | Bình | 1 | Python | active |
| NULL | NULL | 4 | Robotics | NULL |
Câu lệnh
SELECT class_name, COUNT(*) AS registrations
FROM flat_regs
GROUP BY class_name
ORDER BY class_name;Kết quả
| class_name | registrations |
|---|---|
| Database | 1 |
| Python | 2 |
| Robotics | 1 |
Lớp Robotics có registrations = 1 dù chưa ai đăng ký. COUNT(*) đếm cả dòng giữ chỗ vì đó vẫn là một dòng trong bảng. Lưu lớp độc lập sẽ giúp mở lớp mới mà không tạo một đăng ký giả.
Ở bộ dữ liệu chuẩn, classes có một dòng cho mỗi lớp. Truy vấn sau đọc danh sách lớp mà không cần hỏi ai đã đăng ký:
Dữ liệu đầu vào
classes3 dòng · chọn 2 cột| id | name |
|---|---|
| 1 | Python |
| 2 | Scratch |
| 3 | Database |
Câu lệnh
SELECT id, name
FROM classes
ORDER BY id;Kết quả
| id | name |
|---|---|
| 1 | Python |
| 2 | Scratch |
| 3 | Database |
Kết quả trả ba dòng của ba lớp. Bạn không phải đọc tên học viên để lấy tên lớp. Khi mở lớp mới, nơi cần thêm dòng là classes. Bảng đăng ký chỉ nhận dòng sau khi có người thực sự đăng ký vào lớp đó.
Lớp mớiTạo dòng trong bảng lớpHọc viên chọn lớpXác định hai mã có thậtĐăng kýLưu cặp trong bảng đăng ký
Sơ đồ cho thấy thông tin lớp và đăng ký được lưu vào hai lúc khác nhau. Bạn có thể lưu lớp ngay khi mở lớp. Khi một học viên chọn lớp, bạn mới lưu cặp mã học viên–lớp vào bảng đăng ký. Mã lớp liên kết hai bảng mà không buộc bạn tạo lớp và đăng ký cùng lúc.
Tên lớp cũng không nên biến thành một cột mới cho mỗi lớp học. Cách đó khiến mở lớp đồng nghĩa với đổi cấu trúc bảng học viên. Để thêm một lớp mới, bạn chỉ cần thêm một dòng vào bảng lớp. Bảng liên kết tiếp tục dùng cùng hai cột mã như trước.
Xóa một đăng ký không có nghĩa là xóa lớp
Bất thường xóa (deletion anomaly) xảy ra khi xóa một sự thật lại làm mất một sự thật khác còn cần giữ. Trong bảng gộp minh họa, tên lớp Database chỉ xuất hiện ở đăng ký của An. Hủy đăng ký ấy sẽ xóa luôn nơi duy nhất đang giữ tên lớp.
Cách xóa dưới đây chỉ nhằm minh họa vấn đề của bảng gộp:
Dữ liệu đầu vào
flat_regs3 dòng| student_id | student_name | class_id | class_name | status |
|---|---|---|---|---|
| 1 | An | 1 | Python | active |
| 1 | An | 3 | Database | active |
| 2 | Bình | 1 | Python | active |
Câu lệnh
DELETE FROM flat_regs
WHERE student_id = 1 AND class_id = 3;Bảng flat_regs sau khi chạy
flat_regs1 dòng thay đổi · 1 dòng bị xóa| student_id | student_name | class_id | class_name | status |
|---|---|---|---|---|
| 1 | An | 1 | Python | active |
| 2 | Bình | 1 | Python | active |
Sau khi xóa, bảng còn hai dòng của lớp mã 1. Không còn dòng nào lưu tên Database. Nếu đó là nơi duy nhất lưu lớp, phần mềm sẽ không biết lớp mã 3 còn tồn tại. Bạn chỉ muốn hủy đăng ký, nhưng thao tác đó lại xóa mất cả thông tin lớp.
Với thiết kế tách bảng, tên Database vẫn có một dòng riêng trong classes. Câu lệnh dưới đọc chính dòng đó trên bộ dữ liệu chuẩn:
Dữ liệu đầu vào
classes3 dòng| id | name |
|---|---|
| 1 | Python |
| 2 | Scratch |
| 3 | Database |
Câu lệnh
SELECT id, name
FROM classes
WHERE id = 3;Kết quả
| id | name |
|---|---|
| 3 | Database |
Kết quả là lớp Database với mã 3. Ví dụ này dùng bộ dữ liệu chuẩn, tách biệt với bảng gộp vừa xóa. Mỗi ví dụ chạy trên một bản sao dữ liệu riêng.
Ở thiết kế chuẩn, bạn vẫn tìm thấy tên lớp trong bảng riêng của lớp. Hủy một quan hệ không cần xóa dòng lớp hoặc đổi mã lớp.
Khi đánh giá một bảng, bạn hãy tưởng tượng việc xóa dòng cuối của từng loại quan hệ. Thông tin nào sẽ biến mất cùng nó? Nếu lớp vẫn cần tồn tại sau khi học viên cuối cùng rời lớp, lớp cần nơi lưu độc lập. Bạn có thể dùng câu hỏi này để phát hiện thông tin dễ bị mất ngay khi thiết kế bảng, dù mới có vài dòng.
Lưu mỗi sự thật ở đối tượng mà nó mô tả
Trong bộ dữ liệu chuẩn, mỗi loại thông tin có một bảng phù hợp để lưu. Tên học viên thuộc students, tên lớp thuộc classes, còn trạng thái học thuộc enrollments. Cách tổ chức này là bước đầu của chuẩn hóa (normalization). Chuẩn hóa giúp giảm việc lưu cùng một thông tin ở nhiều nơi, nhờ đó tránh các bất thường vừa gặp.
| Thông tin cần lưu | Nơi lưu | Mã xác định sự thật |
|---|---|---|
| Tên người học | students.name | students.id |
| Tên lớp | classes.name | classes.id |
| Trạng thái tham gia | enrollments.status | Cặp student_id, class_id |
Bạn chưa cần học các dạng chuẩn chi tiết ở đây. Bài 22 sẽ quay lại cách kiểm sự phụ thuộc giữa các cột. Trước mắt, bạn chỉ cần hỏi: đây là trạng thái học của ai và trong lớp nào? Nếu câu trả lời cần hai mã, thông tin đó thuộc quan hệ giữa hai đối tượng.
Ta đếm đăng ký của từng học viên trong bảng đã tách:
Dữ liệu đầu vào
enrollments7 dòng| student_id | class_id | status |
|---|---|---|
| 1 | 1 | active |
| 1 | 3 | active |
| 2 | 1 | active |
| 3 | 2 | active |
| 3 | 3 | active |
| 4 | 2 | paused |
| 5 | 1 | active |
Câu lệnh
SELECT student_id, COUNT(*) AS registrations
FROM enrollments
GROUP BY student_id
ORDER BY student_id;Kết quả
| student_id | registrations |
|---|---|
| 1 | 2 |
| 2 | 1 |
| 3 | 2 |
| 4 | 1 |
| 5 | 1 |
Người mã 1 và người mã 3 đều có hai đăng ký. Các mã còn lại trong kết quả có một đăng ký. Mã 6 không xuất hiện vì Lan chưa có quan hệ nào trong bảng này. Hai đăng ký của An không phải hai bản sao của học viên An.
Muốn biết hai dòng có trùng hay không, bạn cần xem mỗi dòng đang mô tả điều gì. Nếu một dòng là một người, lặp cùng mã người là vấn đề. Nếu một dòng là một cặp người–lớp, lặp mã người ở hai lớp là đúng. Vì thế, nhìn thấy giá trị giống nhau chưa đủ để quyết định gộp hoặc xóa dòng.
Bạn cũng có thể kiểm các trạng thái mà không chép tên người vào bảng đăng ký:
Dữ liệu đầu vào
enrollments7 dòng| student_id | class_id | status |
|---|---|---|
| 1 | 1 | active |
| 1 | 3 | active |
| 2 | 1 | active |
| 3 | 2 | active |
| 3 | 3 | active |
| 4 | 2 | paused |
| 5 | 1 | active |
Câu lệnh
SELECT status, COUNT(*) AS n
FROM enrollments
GROUP BY status
ORDER BY status;Kết quả
| status | n |
|---|---|
| active | 6 |
| paused | 1 |
Kết quả có sáu đăng ký active và một đăng ký paused. Đây là số quan hệ theo trạng thái, không phải bảy người khác nhau. Nếu báo cáo cần tên người bên cạnh tên lớp, bài 14 sẽ ghép các bảng bằng mã. Dù thông tin nằm ở các bảng riêng, bạn vẫn có thể ghép chúng lại khi đọc báo cáo.
Chuẩn hóa không có nghĩa mọi truy vấn sẽ nhanh hơn hoặc càng nhiều bảng càng tốt. Mục đích là lưu thông tin ở nơi phù hợp, giảm các bản sao phải sửa cùng lúc và liên kết các bảng bằng mã. Một số báo cáo sẽ cần thêm bước ghép bảng. Đổi lại, người quản lý có thể sửa đúng một dòng tên lớp thay vì dò mọi đăng ký.
Lỗi thường gặp
| Lỗi | Vì sao sai | Cách sửa |
|---|---|---|
| Chép tên lớp vào mọi đăng ký | Một lần đổi tên có nhiều nơi cần sửa | Giữ tên trong classes |
| Tạo học viên giả để mở lớp | Ghi một quan hệ chưa có thật | Lưu lớp độc lập |
| Xóa bớt các đăng ký cùng người | Người có thể học nhiều lớp | Kiểm cả cặp khóa |
| Đặt trạng thái học vào tên lớp | Mỗi đăng ký có trạng thái riêng | Lưu trong enrollments |
Tóm tắt
- Bất thường cập nhật xảy ra khi bạn sửa thiếu các bản sao, khiến cùng thông tin có nhiều giá trị khác nhau.
- Bất thường thêm khiến bạn không lưu được lớp mới nếu chưa có học viên đăng ký.
- Bất thường xóa khiến việc hủy một đăng ký làm mất cả thông tin lớp mà bạn vẫn cần giữ.
- Lưu tên học viên ở bảng học viên, tên lớp ở bảng lớp và liên kết hai bảng qua đăng ký bằng mã.