Buổi 7: Mối quan hệ giữa bảng & Normal Form
Đến buổi này, các bạn đã biết truy vấn từng bảng riêng lẻ. Nhưng dữ liệu của một công ty chứng khoán không nằm trong một bảng: khách hàng có nhiều tài khoản, tài khoản đặt nhiều lệnh, lệnh khớp thành nhiều giao dịch... Thiết kế cách các bảng liên hệ với nhau quyết định hệ thống có đáng tin cậy hay không: số dư có bị lệch, một khách hàng có bị "nhân bản" thành hai người, báo cáo cuối ngày có khớp với sổ lệnh hay không.
Buổi này nặng lý thuyết hơn các buổi trước, vì vậy mọi khái niệm đều được gắn với bộ dữ liệu mẫu của khóa học (xem mô tả đầy đủ ở Buổi 1, mục "Bộ dữ liệu mẫu của khóa học") và có ví dụ chạy được. Cách học của buổi: học xong phần nào, làm bài tập phần đó ngay - khung "Thực hành" sẽ hiện ngay dưới phần lý thuyết tương ứng.
Quy ước: "hôm nay" của bộ dữ liệu là 2026-09-25. Mọi ví dụ chạy được trên cả SQL Server (T-SQL) và MySQL 8; chỗ nào khác nhau sẽ có tiêu đề nhỏ SQL Server / MySQL. Ví dụ có nút "Chạy thử" là các câu truy vấn chạy độc lập được trên bộ dữ liệu mẫu.
Mục tiêu buổi học
- Sau buổi này bạn có thể phân biệt và mô hình hóa quan hệ 1-1, 1-n, n-n (bảng trung gian) trên nghiệp vụ chứng khoán.
- Sau buổi này bạn có thể hiểu và khai báo PRIMARY KEY, FOREIGN KEY, composite key, UNIQUE bằng
CREATE TABLE/ALTER TABLE. - Sau buổi này bạn có thể chuẩn hóa một bảng phẳng qua 1NF, 2NF, 3NF và nói được lợi ích lẫn cái giá phải trả.
- Sau buổi này bạn có thể đọc và vẽ ER Diagram (Entity, Attribute, Relationship, cardinality) cho hệ thống chứng khoán.
- Sau buổi này bạn có thể dùng
INFORMATION_SCHEMAđể liệt kê khóa và dò dòng mồ côi (vi phạm toàn vẹn khóa ngoài).
Bản đồ buổi học
| Phần | Nội dung | Thời lượng gợi ý |
|---|---|---|
| Mở đầu | Vì sao thiết kế quan hệ quan trọng; ôn bộ dữ liệu mẫu | 4 phút |
| 1. Quan hệ giữa bảng | 1-1, 1-n, n-n, quan hệ đệ quy (lý thuyết + demo) | 10 phút |
| 2. Khóa (lý thuyết) | PK, FK, composite key, UNIQUE, khai báo bằng DDL, INFORMATION_SCHEMA |
14 phút |
| Thực hành bài 7.1 | Xác định khóa và kiểm tra mô hình (5 bài, gồm chữa bài) | 43 phút |
| 2.7 Toàn vẹn khóa ngoài | Dò dòng mồ côi | 5 phút |
| Thực hành bài 7.2 | Tìm khóa ngoài, kiểm tra toàn vẹn (4 bài, gồm chữa bài) | 30 phút |
| 3. Chuẩn hóa (lý thuyết) | Dị thường, 1NF → 2NF → 3NF, lợi ích/đánh đổi | 15 phút |
| Thực hành bài 7.3 | Thiết kế bảng 1-1, n-n, chuẩn hóa về 3NF (3 bài, gồm chữa bài) | 31 phút |
| 4. ER Diagram | Thành phần, cardinality, đọc sơ đồ, checklist | 7 phút |
| Vẽ ER + tự kiểm tra | Vẽ trên SSMS/Workbench (không chấm tự động) | 15 phút |
| Tổng kết | Tóm tắt, sai lầm thường gặp, giao bài về nhà | 6 phút |
| Tổng | Lý thuyết + demo ~61 phút, thực hành ~119 phút (khoảng 66%) | 180 phút |
1. Khái niệm mối quan hệ (Relationships)
Một quan hệ thể hiện rằng các dòng của bảng này "gắn" với các dòng của bảng kia. Trong CSDL quan hệ, sự gắn kết đó được lưu bằng giá trị: một cột ở bảng con chứa giá trị khóa chính của bảng cha (đó chính là khóa ngoài, phần 2). Đặc trưng của một quan hệ là bản số (cardinality) - mỗi bên có thể có tối đa bao nhiêu dòng.
| Loại | Đọc là | Cách hiện thực | Ví dụ chứng khoán |
|---|---|---|---|
| 1-1 | một - một | FK + UNIQUE (hoặc FK là PK) | Một khách hàng - một người đại diện; một khách hàng - một dòng CustomerStats |
| 1-n | một - nhiều | FK đặt ở bảng phía "n" | Một khách hàng - nhiều tài khoản; một lệnh - nhiều giao dịch khớp |
| n-n | nhiều - nhiều | Bảng trung gian có 2 FK | Nhiều tài khoản - nhiều mã chứng khoán (qua Portfolio) |
Mẹo: đặt câu hỏi hai chiều. "Một khách hàng có tối đa bao nhiêu tài khoản?" (nhiều) và "Một tài khoản thuộc tối đa bao nhiêu khách hàng?" (một) → quan hệ 1-n. Nếu cả hai chiều đều trả lời "nhiều" → n-n; cả hai đều "một" → 1-1.
1.1. Quan hệ One-to-One (1-1): Một khách hàng - Một người đại diện
Mỗi khách hàng tổ chức (ngân hàng, quỹ, công ty) đăng ký đúng một người đại diện giao dịch; mỗi người đại diện chỉ đại diện cho một khách hàng. Cách hiện thực: bảng con có cột khóa ngoài và cột đó là duy nhất (UNIQUE), hoặc chính khóa ngoài là khóa chính của bảng con.
-- T-SQL (MySQL: đổi NVARCHAR thành VARCHAR)
CREATE TABLE CustomerRep (
rep_id INT NOT NULL PRIMARY KEY,
customer_id INT NOT NULL UNIQUE, -- UNIQUE biến 1-n thành 1-1
rep_name NVARCHAR(100) NOT NULL,
rep_title NVARCHAR(50) NULL,
FOREIGN KEY (customer_id) REFERENCES Customers(customer_id)
);Trong bộ dữ liệu mẫu đã có sẵn một quan hệ 1-1 thực thụ: CustomerStats có customer_id vừa là khóa chính vừa là khóa ngoài trỏ Customers. Mỗi khách hàng có nhiều nhất một dòng thống kê:
SELECT s.customer_id, s.trade_count, s.total_trade_value
FROM CustomerStats s WHERE s.customer_id IN (2, 5, 6) ORDER BY 1;Bằng chứng "1-1" bằng số liệu: số dòng bằng số khách hàng khác nhau (không khách nào có hai dòng thống kê).
SELECT COUNT(*) AS so_dong, COUNT(DISTINCT customer_id) AS so_khach FROM CustomerStats;Khi nào tách 1-1 thay vì để chung một bảng? (1) tách thông tin ít dùng/nhạy cảm (hồ sơ KYC) ra bảng riêng để phân quyền; (2) tách phần dữ liệu "thống kê" thay đổi liên tục khỏi phần danh tính ổn định; (3) chỉ một phần khách hàng mới có dữ liệu đó (người đại diện chỉ áp dụng cho khách hàng tổ chức).
1.2. Quan hệ One-to-Many (1-n): Một khách hàng - Nhiều tài khoản
Đây là quan hệ phổ biến nhất. Khóa ngoài nằm ở bảng phía "n" (bảng con), trỏ về khóa chính của bảng phía "1" (bảng cha). Trong hệ thống của chúng ta có cả một chuỗi 1-n:
Customers 1 ---- n Accounts 1 ---- n Orders 1 ---- n Trades
n ---- 1 Securities 1 ---- n TradesĐếm số tài khoản mỗi khách hàng (khách hàng 10 chưa mở tài khoản nào - nhớ "n" có thể bằng 0):
SELECT c.customer_id, c.full_name, COUNT(a.account_id) AS so_tk
FROM Customers c LEFT JOIN Accounts a ON a.customer_id = c.customer_id
WHERE c.customer_id IN (1, 2, 6, 10, 31) GROUP BY c.customer_id, c.full_name ORDER BY 1;Một lệnh (Orders) có thể khớp ra 0, 1 hoặc nhiều giao dịch (Trades): lệnh đã hủy/đang chờ chưa có giao dịch nào.
SELECT o.order_id, o.symbol, o.status, t.trade_id
FROM Orders o LEFT JOIN Trades t ON t.order_id = o.order_id WHERE o.account_id = 2 ORDER BY o.order_id;Ví dụ nghiệp vụ: lệnh 105 (PVS) đã
CANCELLEDnêntrade_idlàNULL; lệnh 110 (IDC) mớiPARTIAL- khớp một phần. Khi lệnh khớp thành nhiều đợt, cùng mộtorder_idsẽ xuất hiện ở nhiều dòngTrades: đó chính là "một lệnh - nhiều giao dịch".
Quan hệ đệ quy (tự tham chiếu). Một bảng có thể là cha của chính nó: Customers.referrer_id trỏ về Customers.customer_id (khách hàng được ai giới thiệu). Đây vẫn là 1-n (một người giới thiệu - nhiều người được giới thiệu) nhưng cả hai đầu là cùng một bảng. Buổi 8 sẽ dùng self join để đọc nó.
1.3. Quan hệ Many-to-Many (n-n): Nhiều khách hàng - Nhiều sản phẩm
Một tài khoản nắm giữ nhiều mã; một mã được nhiều tài khoản nắm giữ. Không thể đặt khóa ngoài vào bảng nào: nếu đặt symbol vào Accounts, mỗi tài khoản chỉ có 1 mã; nếu đặt account_id vào Securities, mỗi mã chỉ thuộc 1 tài khoản. Cách giải duy nhất là bảng trung gian (còn gọi bảng giao / junction table / associative table) chứa hai khóa ngoài và tách n-n thành hai quan hệ 1-n:
Accounts 1 ---- n Portfolio n ---- 1 Securities
(account_id, symbol) = khóa chính képPortfolio chính là bảng trung gian, và nó còn mang thuộc tính của mối quan hệ (quantity, avg_cost - thuộc về cặp tài khoản-mã chứ không riêng bên nào):
SELECT p.account_id, p.symbol, p.quantity, p.avg_cost
FROM Portfolio p WHERE p.account_id = 3 ORDER BY p.symbol;Đếm theo chiều ngược lại (mỗi mã có bao nhiêu tài khoản nắm giữ) - đây là "phía n" thứ hai của quan hệ n-n:
SELECT symbol, COUNT(*) AS so_tai_khoan FROM Portfolio GROUP BY symbol ORDER BY COUNT(*) DESC, symbol;Bảng sau tóm tắt vì sao n-n bắt buộc cần bảng trung gian:
| Cách làm | Kết quả |
|---|---|
Nhồi nhiều mã vào một cột ('VCB,FPT,HPG') |
Vi phạm 1NF, không JOIN được, không ràng buộc được |
Thêm symbol1, symbol2, symbol3... vào Accounts |
Giới hạn số mã, nhiều NULL, truy vấn "ai giữ VCB?" phải xét từng cột |
Bảng trung gian (account_id, symbol) |
Không giới hạn, có khóa chính kép chống trùng, có FK bảo vệ cả hai phía |
Bài thực hành 7.3b yêu cầu bạn tự thiết kế bảng trung gian Watchlist (khách hàng theo dõi mã) theo đúng mẫu này:
-- T-SQL (MySQL: symbol VARCHAR(10))
CREATE TABLE Watchlist (
customer_id INT NOT NULL,
symbol NVARCHAR(10) NOT NULL,
added_date DATE NOT NULL,
PRIMARY KEY (customer_id, symbol), -- khóa kép: không theo dõi trùng một mã
FOREIGN KEY (customer_id) REFERENCES Customers(customer_id),
FOREIGN KEY (symbol) REFERENCES Securities(symbol)
);Cảnh báo: lưu nhiều giá trị vào một ô (
'VCB,FPT,HPG'trong cộtwatch_symbols) thay vì dùng bảng trung gian là lỗi thiết kế kinh điển - vi phạm 1NF (phần 3), không thể JOIN, không thể ràng buộc.
2. Khóa (Key) trong Database
2.1. Primary Key (Khóa chính): Định danh duy nhất
Khóa chính định danh duy nhất một dòng trong bảng. Yêu cầu: không trùng, không NULL, ổn định (không đổi theo thời gian). Mỗi bảng có đúng một khóa chính (có thể gồm nhiều cột).
Hai kiểu chọn khóa:
- Khóa tự nhiên (natural key): dữ liệu nghiệp vụ, ví dụ
Securities.symbol(mã chứng khoán); - Khóa đại diện (surrogate key): số vô nghĩa sinh tự động, ví dụ
Customers.customer_id,AuditLog.log_id. Số thứ tự tự tăng: SQL ServerIDENTITY(1,1), MySQLAUTO_INCREMENT.
Vì sao Customers dùng customer_id làm PK mà customer_code chỉ là UNIQUE? Vì mã khách hàng là quy ước nghiệp vụ, có thể bị đổi định dạng; còn customer_id không bao giờ đổi và nhỏ gọn khi làm khóa ngoài ở hàng triệu dòng lệnh.
2.2. Foreign Key (Khóa ngoài): Tham chiếu đến bảng khác
Khóa ngoài là cột (hoặc bộ cột) ở bảng con có giá trị phải tồn tại trong khóa chính (hoặc khóa UNIQUE) của bảng cha, hoặc là NULL nếu cột cho phép NULL. Nhờ nó CSDL từ chối dữ liệu vô lý:
INSERT INTO Accounts (account_id, customer_id, account_no, account_type, balance, opened_date, status)
VALUES (999, 12345, 'ACC99999', 'CASH', 0, '2026-09-25', 'ACTIVE'); -- khách hàng 12345 không tồn tại- SQL Server:
The INSERT statement conflicted with the FOREIGN KEY constraint ...(lỗi 547); - MySQL:
ERROR 1452 ... Cannot add or update a child row: a foreign key constraint fails.
Ngược lại, xóa khách hàng đang có tài khoản cũng bị chặn (Cannot delete or update a parent row). Hành vi này có thể tùy biến bằng ON DELETE / ON UPDATE:
| Tùy chọn | Khi xóa dòng cha | Dùng khi |
|---|---|---|
NO ACTION / RESTRICT (mặc định) |
Chặn, báo lỗi | Hầu hết trường hợp nghiệp vụ tài chính |
CASCADE |
Xóa luôn các dòng con | Dữ liệu phụ thuộc hoàn toàn (vd chi tiết tạm) |
SET NULL |
Đặt cột FK của dòng con thành NULL | FK cho phép NULL, con vẫn có ý nghĩa khi mất cha |
Cảnh báo: trong nghiệp vụ chứng khoán không nên
CASCADElên lệnh/giao dịch - dữ liệu giao dịch phải lưu vết vĩnh viễn (và phải giữ cho thanh tra). Thay vào đó dùng cột trạng thái (status = 'CLOSED') để "đóng" thay vì xóa.
SQL Server vs MySQL: khóa ngoài không tự tạo index trên cột con ở SQL Server (MySQL/InnoDB thì có). Nên tự tạo index cho các cột FK hay JOIN như
Orders.account_id(sẽ nói ở Buổi 8).
2.3. Composite Key: Khóa gồm nhiều cột
Khi một cột không đủ định danh dòng, ta ghép nhiều cột. Bộ dữ liệu có hai ví dụ điển hình:
| Bảng | Khóa chính kép | Ý nghĩa |
|---|---|---|
MarketPrices |
(symbol, trade_date) |
Mỗi mã có đúng một dòng giá mỗi ngày |
Portfolio |
(account_id, symbol) |
Mỗi tài khoản nắm giữ mỗi mã đúng một dòng |
Với khóa kép, từng cột vẫn có thể lặp lại, chỉ tổ hợp là duy nhất. Vì vậy Portfolio.account_id là khóa ngoài 1-n (một tài khoản, nhiều dòng), không phải 1-1, dù nó nằm trong khóa chính (bài 7.1e).
Lưu ý: thứ tự cột trong khóa kép có ý nghĩa:
PRIMARY KEY (account_id, symbol)tạo index hỗ trợ tìm theoaccount_id(hoặc cả hai) rất nhanh, nhưng tìm chỉ theosymbolthì không tận dụng được index này. Hãy đặt cột hay lọc nhất lên trước.
2.4. Unique Key: Giá trị duy nhất nhưng không là PK
UNIQUE cấm trùng lặp nhưng không là định danh chính, và một bảng có thể có nhiều ràng buộc UNIQUE: Customers.customer_code, Accounts.account_no.
| PRIMARY KEY | UNIQUE | |
|---|---|---|
| Số lượng mỗi bảng | Đúng 1 | Nhiều |
| Cho phép NULL | Không | Có |
| Thường là mục tiêu của FK | Có | Có thể |
SQL Server vs MySQL (NULL trong UNIQUE): SQL Server chỉ cho một giá trị NULL trong cột UNIQUE (hai NULL coi là trùng); MySQL cho nhiều NULL. Vì thế cột
Customers.email(5 khách chưa có email) không đặt UNIQUE được trên SQL Server nếu không dùng thêm filtered indexWHERE email IS NOT NULL.
2.5. Khai báo khóa bằng CREATE TABLE và ALTER TABLE
Có hai chỗ để khai báo khóa: ngay trong CREATE TABLE, hoặc thêm sau bằng ALTER TABLE ... ADD CONSTRAINT. Nên đặt tên ràng buộc rõ ràng (PK_, FK_, UQ_) để dễ đọc thông báo lỗi và dễ xóa/sửa về sau.
-- T-SQL (MySQL: NVARCHAR -> VARCHAR)
CREATE TABLE OrderFee (
order_id INT NOT NULL,
fee_type NVARCHAR(20) NOT NULL,
fee_amount DECIMAL(18,2) NOT NULL,
CONSTRAINT PK_OrderFee PRIMARY KEY (order_id, fee_type), -- khóa kép
CONSTRAINT FK_OrderFee_Orders FOREIGN KEY (order_id)
REFERENCES Orders(order_id) ON DELETE NO ACTION
);
-- Thêm sau khi bảng đã có: UNIQUE và FOREIGN KEY
ALTER TABLE Customers ADD CONSTRAINT UQ_Customers_code UNIQUE (customer_code);
ALTER TABLE Accounts ADD CONSTRAINT FK_Accounts_Customers
FOREIGN KEY (customer_id) REFERENCES Customers(customer_id);| Việc | SQL Server | MySQL |
|---|---|---|
| Xóa khóa ngoài | ALTER TABLE Accounts DROP CONSTRAINT FK_Accounts_Customers; |
ALTER TABLE Accounts DROP FOREIGN KEY FK_Accounts_Customers; |
| Xóa UNIQUE | DROP CONSTRAINT UQ_Customers_code |
DROP INDEX UQ_Customers_code |
| Xóa PRIMARY KEY | DROP CONSTRAINT PK_OrderFee |
DROP PRIMARY KEY |
| Kiểu chuỗi Unicode | NVARCHAR(n) |
VARCHAR(n) (mặc định utf8mb4) |
Lưu ý: thêm khóa ngoài lên bảng đã có dữ liệu sẽ thất bại nếu đang tồn tại dòng mồ côi (mục 2.7). Vì vậy quy trình an toàn là: dò mồ côi → sửa/xóa → rồi mới
ADD CONSTRAINT.
2.6. Xem khóa bằng INFORMATION_SCHEMA
Cả SQL Server và MySQL có bộ view chuẩn INFORMATION_SCHEMA mô tả cấu trúc CSDL - công cụ quan trọng để kiểm tra mô hình bằng câu lệnh (đúng tinh thần các bài 7.1, 7.2):
| View | Nội dung |
|---|---|
TABLES |
Danh sách bảng/view (TABLE_TYPE = 'BASE TABLE' là bảng thật) |
COLUMNS |
Cột, kiểu dữ liệu, có cho NULL không |
TABLE_CONSTRAINTS |
Ràng buộc: CONSTRAINT_TYPE = PRIMARY KEY / UNIQUE / FOREIGN KEY / CHECK |
KEY_COLUMN_USAGE |
Cột nào thuộc ràng buộc nào (ORDINAL_POSITION = thứ tự trong khóa) |
REFERENTIAL_CONSTRAINTS |
Quan hệ khóa ngoài → ràng buộc được tham chiếu |
Liệt kê cột khóa chính của mọi bảng (nối TABLE_CONSTRAINTS với KEY_COLUMN_USAGE):
SQL Server
SELECT k.TABLE_NAME, k.COLUMN_NAME
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE k
ON k.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA AND k.CONSTRAINT_NAME = tc.CONSTRAINT_NAME AND k.TABLE_NAME = tc.TABLE_NAME
WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
ORDER BY LOWER(k.TABLE_NAME), k.ORDINAL_POSITION;MySQL: thêm AND tc.TABLE_SCHEMA = DATABASE() vì INFORMATION_SCHEMA của MySQL chứa tất cả CSDL trên máy chủ (bỏ dòng này sẽ ra dòng trùng lặp); DATABASE() là CSDL hiện tại.
SELECT k.TABLE_NAME, k.COLUMN_NAME
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE k
ON k.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA AND k.CONSTRAINT_NAME = tc.CONSTRAINT_NAME AND k.TABLE_NAME = tc.TABLE_NAME
WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY' AND tc.TABLE_SCHEMA = DATABASE()
ORDER BY LOWER(k.TABLE_NAME), k.ORDINAL_POSITION;Kết quả (một phần): Accounts account_id, Customers customer_id, MarketPrices symbol, MarketPrices trade_date, Portfolio account_id, Portfolio symbol... - chú ý MarketPrices và Portfolio xuất hiện hai dòng vì có khóa kép.
Liệt kê khóa ngoài kèm bảng cha - hai cách khác nhau:
SQL Server: không có sẵn cột bảng cha trong KEY_COLUMN_USAGE; phải nối qua REFERENTIAL_CONSTRAINTS.UNIQUE_CONSTRAINT_NAME:
SELECT k.TABLE_NAME, k.COLUMN_NAME, pk.TABLE_NAME AS ref_table, pk.COLUMN_NAME AS ref_column
FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc
JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE k ON k.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE pk ON pk.CONSTRAINT_NAME = rc.UNIQUE_CONSTRAINT_NAME
AND pk.ORDINAL_POSITION = k.ORDINAL_POSITION
ORDER BY LOWER(k.TABLE_NAME), LOWER(k.COLUMN_NAME);MySQL: dùng thẳng REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME trong KEY_COLUMN_USAGE (lọc REFERENCED_TABLE_NAME IS NOT NULL):
SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME AS ref_table, REFERENCED_COLUMN_NAME AS ref_column
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = DATABASE() AND REFERENCED_TABLE_NAME IS NOT NULL
ORDER BY LOWER(TABLE_NAME), LOWER(COLUMN_NAME);Kết quả rút gọn: Accounts.customer_id → Customers.customer_id, Orders.account_id → Accounts.account_id, Trades.order_id → Orders.order_id, Customers.referrer_id → Customers.customer_id...
Lưu ý khi chấm bài: tên ràng buộc (
CONSTRAINT_NAME) do hệ quản trị tự sinh (SQL Server:PK__Customers__..., MySQL:PRIMARY,Accounts_ibfk_1) và tên schema cũng khác nhau, nên đừng in chúng ra khi cần kết quả giống nhau trên hai hệ; hãy lọc/JOIN theoTABLE_NAME,COLUMN_NAME.
Mẹo: trong SSMS, cách nhanh nhất để xem khóa của một bảng là mở rộng bảng trong Object Explorer → Keys (biểu tượng chìa khóa vàng là PK, xám là FK/UNIQUE) hoặc chạy
EXEC sp_help 'Accounts'. Trong MySQL Workbench: chuột phải bảng → Alter Table → tab Foreign Keys, hoặc chạySHOW CREATE TABLE Accounts;.
Vừa học xong PK, FK, composite key, UNIQUE và cách đọc chúng từ INFORMATION_SCHEMA, hãy làm bài 7.1 để tự xác định khóa và kiểm tra một mô hình dữ liệu.
2.7. Kiểm tra toàn vẹn khóa ngoài: tìm dòng "mồ côi"
Dòng mồ côi (orphan) là dòng ở bảng con có giá trị khóa ngoài không tồn tại ở bảng cha. Nếu ràng buộc FK đang bật thì không thể có; nhưng với dữ liệu nhập từ hệ thống cũ, hoặc khi bảng con chưa từng khai báo FK, mồ côi hoàn toàn có thể xảy ra (ví dụ tài khoản trỏ tới khách hàng đã bị xóa khỏi danh mục). Truy vấn tiêu chuẩn: NOT EXISTS.
-- số tài khoản mà customer_id không có trong Customers (dữ liệu sạch: 0)
SELECT COUNT(*) AS so_dong_mo_coi
FROM Accounts a
WHERE NOT EXISTS (SELECT 1 FROM Customers c WHERE c.customer_id = a.customer_id);Muốn xem chi tiết các dòng mồ côi thay vì đếm, bỏ COUNT(*) và SELECT a.account_id, a.customer_id.
Với khóa ngoài cho phép NULL (như Customers.referrer_id), phải loại giá trị NULL, vì NULL nghĩa là "không có cha", không phải mồ côi:
SELECT COUNT(*) AS so_dong_mo_coi
FROM Customers c
WHERE c.referrer_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM Customers r WHERE r.customer_id = c.referrer_id);Cảnh báo: dùng
NOT IN (SELECT ...)khi tập con có NULL:x NOT IN (1, 2, NULL)cho kết quả UNKNOWN với mọix, khiến truy vấn trả về rỗng (thiếu dòng). Ví dụ dưới đây "tìm khách hàng chưa từng giới thiệu ai", hai cách cho hai kết quả khác nhau:
SELECT
(SELECT COUNT(*) FROM Customers WHERE customer_id NOT IN (SELECT referrer_id FROM Customers)) AS not_in,
(SELECT COUNT(*) FROM Customers c
WHERE NOT EXISTS (SELECT 1 FROM Customers r WHERE r.referrer_id = c.customer_id)) AS not_exists;NOT IN cho 0 (sai, vì referrer_id có NULL), NOT EXISTS cho số đúng. Dùng NOT EXISTS là an toàn hơn. (Ở Buổi 8 ta còn có cách thứ ba: LEFT JOIN ... WHERE bảng_cha.id IS NULL.)
Trong bài 7.2c, hệ thống cố ý tắt kiểm tra khóa ngoài rồi chèn dữ liệu vi phạm để bạn dò ra. Đó là lệnh mà DBA dùng khi nạp dữ liệu lớn:
| Tắt kiểm tra khóa ngoài | Bật lại | |
|---|---|---|
| SQL Server | ALTER TABLE Accounts NOCHECK CONSTRAINT ALL; |
ALTER TABLE Accounts WITH CHECK CHECK CONSTRAINT ALL; |
| MySQL | SET FOREIGN_KEY_CHECKS = 0; |
SET FOREIGN_KEY_CHECKS = 1; |
Mẹo: sau khi nạp xong, luôn chạy truy vấn dò mồ côi trước khi bật lại ràng buộc. Với SQL Server,
WITH CHECKbuộc kiểm tra cả dữ liệu cũ; thiếu nó, ràng buộc bị đánh dấu "không đáng tin" (not trusted) và trình tối ưu bỏ qua.
Vừa học xong cách đọc khóa ngoài và dò dòng mồ côi, hãy làm bài 7.2 để liệt kê khách hàng cùng khóa, tài khoản cùng khóa ngoài và kiểm tra toàn vẹn khóa ngoài bằng chính SQL.
3. Normalization (Chuẩn hóa)
Chuẩn hóa là quá trình tổ chức lại bảng để giảm dư thừa và tránh các dị thường (anomaly) khi thêm/sửa/xóa. Ba dạng chuẩn đầu tiên (1NF, 2NF, 3NF) là đủ cho 95% thiết kế thực tế. Mỗi dạng chuẩn "cao hơn" bao hàm dạng trước: đã 3NF thì đương nhiên đã 2NF và 1NF.
3.1. Bảng phi chuẩn hóa và ba loại dị thường
Giả sử phòng báo cáo lưu mọi thứ trong một bảng phẳng TradeReport (dữ liệu lấy từ bộ mẫu):
| trade_id | customer_code | customer_name | city | symbol | company_name | sector | quantity | price |
|---|---|---|---|---|---|---|---|---|
| 32 | KH100014 | Tran Thi Binh | Da Nang | CEO | Tap doan CEO | Bat dong san | 1000 | 22500 |
| 39 | KH100014 | Tran Thi Binh | Da Nang | SHS | Chung khoan Sai Gon - Ha Noi | Chung khoan | 1000 | 15500 |
| 80 | KH100014 | Tran Thi Binh | Da Nang | IDC | Tong Cong ty IDICO | Ha tang khu cong nghiep | 50 | 51000 |
| 40 | KH100168 | Le Thi Giang | Hai Phong | CEO | Tap doan CEO | Bat dong san | 2500 | 22000 |
Nhìn bảng ta đã thấy sự lặp: "Tran Thi Binh - Da Nang" lặp 3 lần, "Tap doan CEO - Bat dong san" lặp 2 lần. Sự lặp này sinh ra ba loại dị thường:
| Dị thường | Tình huống | Hậu quả |
|---|---|---|
| Cập nhật | Tran Thi Binh chuyển sang Ha Noi; phải sửa mọi dòng của bà | Sửa sót một dòng → dữ liệu mâu thuẫn ("Da Nang" và "Ha Noi" cùng tồn tại) |
| Thêm | Niêm yết mã mới nhưng chưa có giao dịch nào | Không có chỗ lưu (phải chèn dòng giao dịch "giả") |
| Xóa | Xóa giao dịch duy nhất của một khách hàng | Mất luôn thông tin khách hàng đó |
Chuẩn hóa từng bước sẽ loại bỏ các dị thường này. Với mỗi dạng chuẩn dưới đây, hãy đọc theo mẫu TRƯỚC → SAU.
3.2. 1NF: Loại bỏ nhóm lặp lại
Điều kiện 1NF: mỗi ô chứa một giá trị nguyên tử (không danh sách, không nhóm cột lặp lại như phone1, phone2, phone3), mỗi dòng xác định được bằng khóa.
Trường hợp A - danh sách trong một ô.
TRƯỚC (vi phạm 1NF):
| customer_id | full_name | phones |
|---|---|---|
| 1 | Nguyen Van An | 0910000137; 0988111222 |
SAU (1NF): tách thành bảng con 1-n CustomerPhone với khóa chính kép (customer_id, phone):
| customer_id | phone |
|---|---|
| 1 | 0910000137 |
| 1 | 0988111222 |
Trường hợp B - nhóm cột lặp lại. Bảng danh mục "ngang" cho mỗi tài khoản:
TRƯỚC (vi phạm 1NF):
| account_id | symbol1 | qty1 | symbol2 | qty2 | symbol3 | qty3 |
|---|---|---|---|---|---|---|
| 3 | CEO | 500 | NVB | 1000 | SHS | 3000 |
Tài khoản 3 thực tế giữ 4 mã (thêm VCB 100 cổ phiếu) - bảng ngang không còn chỗ, phải sửa cấu trúc bảng mỗi khi có tài khoản giữ nhiều mã hơn.
SAU (1NF): mỗi mã một dòng - chính là bảng Portfolio của bộ dữ liệu:
| account_id | symbol | quantity |
|---|---|---|
| 3 | CEO | 500 |
| 3 | NVB | 1000 |
| 3 | SHS | 3000 |
| 3 | VCB | 100 |
Ví dụ nghiệp vụ: cột
phones = '0910000137; 0988111222'khiến bộ phận CSKH không thể tìm khách theo số điện thoại bằngWHERE phone = '0988111222'; phải dùngLIKE '%0988111222%'- vừa chậm (không dùng được index) vừa dễ sai (khớp nhầm một phần số khác).
3.3. 2NF: Loại bỏ phụ thuộc hàm không đầy đủ
Điều kiện 2NF: đã 1NF và mọi cột không thuộc khóa phải phụ thuộc vào toàn bộ khóa chính (chỉ có ý nghĩa với bảng có khóa kép).
TRƯỚC (vi phạm 2NF): bảng PortfolioFlat với khóa (account_id, symbol):
| account_id | symbol | quantity | avg_cost | company_name | sector |
|---|---|---|---|---|---|
| 3 | CEO | 500 | 22700 | Tap doan CEO | Bat dong san |
| 3 | VCB | 100 | 90500 | Ngan hang Ngoai thuong | Ngan hang |
| 7 | CEO | 3000 | 22700 | Tap doan CEO | Bat dong san |
quantity,avg_costphụ thuộc vào cả cặp (account_id, symbol) → đúng.company_name,sectorchỉ phụ thuộcsymbol(một phần của khóa) → vi phạm 2NF: "Tap doan CEO" lặp ở mọi tài khoản giữ CEO.
SAU (2NF): tách phần chỉ phụ thuộc symbol ra bảng riêng.
Bảng Portfolio (chỉ dữ liệu phụ thuộc cả cặp):
| account_id | symbol | quantity | avg_cost |
|---|---|---|---|
| 3 | CEO | 500 | 22700 |
| 3 | VCB | 100 | 90500 |
| 7 | CEO | 3000 | 22700 |
Bảng Securities (mỗi mã đúng một dòng):
| symbol | company_name | sector |
|---|---|---|
| CEO | Tap doan CEO | Bat dong san |
| VCB | Ngan hang Ngoai thuong | Ngan hang |
Đây chính xác là thiết kế hiện tại của bộ dữ liệu mẫu. Muốn có lại bảng phẳng như cũ, chỉ cần JOIN hai bảng:
SELECT p.account_id, p.symbol, p.quantity, p.avg_cost, s.company_name, s.sector
FROM Portfolio p
JOIN Securities s ON s.symbol = p.symbol
WHERE p.account_id = 3
ORDER BY p.symbol;3.4. 3NF: Loại bỏ phụ thuộc bắc cầu
Điều kiện 3NF: đã 2NF và không có cột thường phụ thuộc vào cột thường khác (không có phụ thuộc bắc cầu): cột thường chỉ phụ thuộc khóa, toàn bộ khóa, và không gì ngoài khóa.
TRƯỚC (vi phạm 3NF): AccountFlat(account_id PK, customer_id, customer_name, customer_city, balance) - dữ liệu ba tài khoản của khách Tran Thi Binh:
| account_id | customer_id | customer_name | customer_city | balance |
|---|---|---|---|---|
| 2 | 2 | Tran Thi Binh | Da Nang | 8000000 |
| 3 | 2 | Tran Thi Binh | Da Nang | 32000000 |
| 4 | 2 | Tran Thi Binh | Da Nang | 8600000000 |
account_id → customer_id → customer_name, customer_city: tên và thành phố của khách phụ thuộc customer_id (cột thường), chỉ bắc cầu phụ thuộc account_id. Khách có 3 tài khoản thì tên/thành phố lặp 3 lần.
SAU (3NF): tách khách hàng ra bảng riêng, Accounts chỉ giữ customer_id làm khóa ngoài.
Customers:
| customer_id | full_name | city |
|---|---|---|
| 2 | Tran Thi Binh | Da Nang |
Accounts:
| account_id | customer_id | balance |
|---|---|---|
| 2 | 2 | 8000000 |
| 3 | 2 | 32000000 |
| 4 | 2 | 8600000000 |
Đúng thiết kế Customers - Accounts hiện có. Đổi thành phố của khách chỉ cần sửa một dòng duy nhất trong Customers.
Mẹo nhớ: cột không thuộc khóa phải phụ thuộc vào "the key (1NF), the whole key (2NF), and nothing but the key (3NF)" - tạm dịch: khóa, toàn bộ khóa, và không gì khác ngoài khóa.
| Dạng chuẩn | Loại bỏ | Dấu hiệu nhận biết | Cách sửa |
|---|---|---|---|
| 1NF | Nhóm lặp, ô không nguyên tử | Danh sách trong ô, col1, col2, col3 |
Tách ra bảng con 1-n |
| 2NF | Phụ thuộc một phần vào khóa kép | Cột chỉ phụ thuộc một nửa khóa | Tách theo phần khóa đó ra bảng riêng |
| 3NF | Phụ thuộc bắc cầu | Cột thường xác định cột thường khác | Tách cột xác định + cột bị xác định ra bảng riêng |
3.5. Áp dụng: từ TradeReport đến 3NF (bài 7.3c)
Bảng phẳng TradeReport ở mục 3.1 tách thành:
RptCustomer(customer_code PK, customer_name, city)- thông tin khách phụ thuộc mã khách;RptSecurity(symbol PK, company_name, sector)- thông tin mã phụ thuộc mã chứng khoán;RptTrade(trade_id PK, customer_code FK, symbol FK, quantity, price)- chỉ dữ liệu phụ thuộc trực tiếp giao dịch.
Nạp dữ liệu bằng INSERT ... SELECT DISTINCT từ bảng nguồn (bảng cha trước, bảng con sau). Khi cần báo cáo phẳng như cũ, ta JOIN ba bảng lại: chuẩn hóa không làm mất khả năng báo cáo, chỉ chuyển gánh nặng sang câu lệnh JOIN (Buổi 8). Ví dụ dưới đây dựng lại đúng bốn dòng của bảng phẳng ở mục 3.1 từ các bảng đã chuẩn hóa của bộ dữ liệu (Customers, Securities, Trades, nối qua Orders và Accounts):
SELECT t.trade_id, c.customer_code, c.full_name, c.city, t.symbol, s.company_name, s.sector, t.quantity, t.price
FROM Trades t
JOIN Orders o ON o.order_id = t.order_id
JOIN Accounts a ON a.account_id = o.account_id
JOIN Customers c ON c.customer_id = a.customer_id
JOIN Securities s ON s.symbol = t.symbol
WHERE t.trade_id IN (32, 39, 80, 40)
ORDER BY t.trade_id;3.6. Lợi ích và cái giá
| Lợi ích | Cái giá |
|---|---|
| Không dư thừa, dữ liệu một nguồn sự thật | Truy vấn cần JOIN nhiều bảng hơn |
| Sửa một chỗ, đúng mọi nơi (hết dị thường) | Thiết kế ban đầu phức tạp hơn |
| Ràng buộc PK/FK bảo vệ tính toàn vẹn | Báo cáo tổng hợp lớn có thể chậm hơn |
| Bảo trì dễ: thêm thuộc tính vào đúng bảng | Nhiều bảng, cần tài liệu (ER Diagram) |
Trong thực tế, đội hệ thống chấp nhận phi chuẩn hóa có chủ đích ở những chỗ đọc rất nhiều: CustomerStats.total_trade_value chính là dữ liệu suy ra được từ Trades nhưng được lưu sẵn để dashboard đọc nhanh. Đổi lại phải có cơ chế giữ đồng bộ (trigger ở Buổi 9). Quy tắc: chuẩn hóa trước, phi chuẩn hóa sau và có lý do, có cơ chế đồng bộ.
Cảnh báo: đừng nhầm "chuẩn hóa" với "chia thật nhiều bảng". Chuẩn hóa quá đà (vd tách riêng bảng "tên đệm", bảng "loại lệnh" chỉ có 2 giá trị) làm truy vấn rối mà không giảm dị thường nào. Dừng ở 3NF, rồi cân nhắc theo nhu cầu đọc/ghi.
Vừa học xong 1NF, 2NF, 3NF và cách thiết kế bảng 1-1, n-n, hãy làm bài 7.3 để tự viết CREATE TABLE cho quan hệ 1-1, bảng trung gian n-n và chuẩn hóa một bảng báo cáo phi chuẩn về 3NF (chấm theo trạng thái dữ liệu).
4. ER Diagram (Entity-Relationship)
ER Diagram là bản vẽ thiết kế ở mức "khái niệm": cho cả nhóm (lập trình viên, DBA, người nghiệp vụ) nhìn cùng một bức tranh về dữ liệu trước khi viết CREATE TABLE. Bản vẽ tốt trả lời được ba câu hỏi: có những đối tượng nào, mỗi đối tượng có thuộc tính gì, và chúng liên hệ với nhau ra sao (bao nhiêu - bao nhiêu).
4.1. Thành phần
| Thành phần | Ý nghĩa | Ví dụ |
|---|---|---|
| Entity (thực thể) | Đối tượng nghiệp vụ, thường thành một bảng | Customer, Account, Security, Order, Trade |
| Attribute (thuộc tính) | Đặc điểm của thực thể, thành cột; gạch chân = khóa | full_name, city, customer_id |
| Relationship (mối quan hệ) | Liên hệ giữa thực thể, thành khóa ngoài (hoặc bảng trung gian nếu n-n) | Customer sở hữu Account |
Mẹo gọi tên: thực thể là danh từ (Khách hàng, Tài khoản), mối quan hệ là động từ (sở hữu, đặt, khớp thành, nắm giữ).
4.2. Ký hiệu cardinality (Crow's Foot)
Ở đầu mỗi đường nối ghi bản số tối thiểu và tối đa. Trên hình vẽ chân chim, "một" là gạch đứng, "nhiều" là chân chim (ba nhánh), "không" là vòng tròn:
| Viết là | Ý nghĩa | Hình chân chim |
|---|---|---|
1..1 |
đúng một | hai gạch đứng |
0..1 |
không hoặc một | vòng tròn và một gạch |
1..n |
một hoặc nhiều | một gạch và chân chim |
0..n |
không hoặc nhiều | vòng tròn và chân chim |
Cách đọc: đứng ở thực thể A, nhìn đầu nối phía B để biết "một A có bao nhiêu B".
4.3. ER Diagram của hệ thống chứng khoán
Hình dưới là sơ đồ quan hệ của toàn bộ bộ dữ liệu mẫu mà chúng ta dùng suốt khóa học. Mỗi hộp là một thực thể (bảng), mỗi đường nối là một quan hệ khóa ngoài.
PK = khóa chính, FK = khóa ngoài. Đường nối: 1 ở phía bảng 'một', n ở phía bảng 'nhiều'. Đường nét đứt: liên hệ logic (không khai báo FK). Quan hệ n–n giữa Accounts và Securities đi qua bảng Portfolio.
Cách đọc hình: PK là khóa chính, FK là khóa ngoài, UQ là duy nhất. Đường nối có chân chim ở phía bảng con (bảng "n") và gạch đứng ở phía bảng cha. Đọc dọc theo chuỗi Customers → Accounts → Orders → Trades để thấy quan hệ 1-n nối tiếp; Portfolio nằm giữa Accounts và Securities là bảng trung gian của quan hệ n-n; CustomerStats bám vào Customers bằng quan hệ 1-1; Customers.referrer_id là đường nối vòng về chính bảng (quan hệ đệ quy).
Bảng tra cứu các quan hệ trong hình:
| Cha (phía 1) | Con (phía n) | Khóa ngoài (ở bảng con) | Bản số | Loại |
|---|---|---|---|---|
Customers |
Accounts |
Accounts.customer_id |
1..1 - 0..n |
1-n |
Accounts |
Orders |
Orders.account_id |
1..1 - 0..n |
1-n |
Orders |
Trades |
Trades.order_id |
1..1 - 0..n |
1-n |
Securities |
Orders, Trades, MarketPrices |
symbol ở mỗi bảng |
1..1 - 0..n |
1-n |
Accounts - Securities |
Portfolio |
Portfolio.account_id, Portfolio.symbol |
qua bảng trung gian | n-n |
Customers |
CustomerStats |
CustomerStats.customer_id (cũng là PK) |
1..1 - 0..1 |
1-1 |
Customers |
Customers |
Customers.referrer_id |
0..1 - 0..n |
đệ quy 1-n |
Cách đọc vài quan hệ:
- Customers 1..1 - 0..n Accounts: một khách hàng có không hoặc nhiều tài khoản; mỗi tài khoản thuộc đúng một khách hàng.
- Accounts - Securities (n-n) qua
Portfolio:Portfoliomang khóa chính kép(account_id, symbol)gồm hai khóa ngoài. - Customers 1..1 - 0..1 CustomerStats: quan hệ 1-1 tùy chọn về mặt lược đồ (một khách có tối đa một dòng thống kê; trong dữ liệu mẫu cả 40 khách đều có).
- Customers - Customers (referrer_id): đệ quy 0..1 người giới thiệu.
Lưu ý: trong hình, bản số "0" ở phía con (
0..n) cho biết một khách có thể chưa có tài khoản - đúng với khách 10 ở mục 1.2. Nếu nghiệp vụ quy định "mở khách hàng là phải mở tài khoản" thì bản số sẽ là1..n, và ta phải bảo đảm điều đó bằng ứng dụng hoặc transaction, vì khóa ngoài một mình không ép được "tối thiểu một".
4.4. Vẽ ER Diagram từng bước
Quy trình vẽ một ER cơ bản, áp dụng cho một yêu cầu mới - ví dụ "công ty muốn lưu phí giao dịch của mỗi lệnh; một lệnh có thể chịu nhiều loại phí (phí môi giới, phí sàn, thuế TNCN), mỗi loại tối đa một khoản":
- Tìm thực thể (danh từ):
Order(đã có),Fee/ khoản phí. - Tìm thuộc tính: loại phí, số tiền.
- Tìm quan hệ và bản số: một lệnh có
0..nkhoản phí; mỗi khoản phí thuộc1..1lệnh → 1-n. - Chọn khóa: khoản phí định danh bằng cặp
(order_id, fee_type)- khóa kép, vì mỗi loại phí xuất hiện tối đa một lần trong một lệnh. - Kiểm tra chuẩn hóa: không cột nào phụ thuộc một phần khóa hay bắc cầu → đạt 3NF.
+----------------------+ 1..1 0..n +----------------------+
| Orders |---------------------| OrderFee |
|----------------------| chịu phí |----------------------|
| PK order_id | | PK,FK order_id |
| symbol, side ... | | PK fee_type |
+----------------------+ | fee_amount |
+----------------------+Bước 5 chính là mẫu OrderFee ở mục 2.5, nên bạn có thể chuyển thẳng từ sơ đồ sang CREATE TABLE.
Vẽ bằng công cụ (không chấm tự động):
- SQL Server / SSMS: trong Object Explorer, mở CSDL → chuột phải Database Diagrams → New Database Diagram (lần đầu SSMS hỏi tạo đối tượng hỗ trợ - chọn Yes) → chọn các bảng → Add. Các đường nối khóa ngoài tự hiện; chuột phải vào đường nối xem Properties để biết bản số; mỗi bảng có biểu tượng chìa khóa ở cột PK.
- MySQL Workbench: menu Database → Reverse Engineer... chọn CSDL → Workbench sinh EER Diagram (ký hiệu chân chim). Menu Model → Forward Engineer làm ngược lại (từ sơ đồ ra script).
- Nếu chưa có công cụ: vẽ tay hoặc dùng bảng/ASCII như trên; điều quan trọng là đúng khóa và bản số. Các công cụ web miễn phí như dbdiagram.io hoặc draw.io cũng dùng được.
4.5. Kiểm tra một mô hình đã đúng chưa (checklist)
- Mỗi bảng có khóa chính không? (bài 7.1d: báo cáo
pk_columns = 0là lỗi); - Mọi quan hệ có khóa ngoài khai báo không? (so sánh với sơ đồ, bài 7.1c);
- Quan hệ 1-1 có UNIQUE trên cột FK không? (bài 7.1e);
- Quan hệ n-n có bảng trung gian với khóa chính kép không?;
- Có bảng nào còn nhóm lặp/phụ thuộc bắc cầu không (đạt 3NF)?;
- Dữ liệu có dòng mồ côi không? (mục 2.7).
Hoạt động cuối buổi (không chấm tự động, khoảng 15 phút): (1) dùng SSMS (Database Diagrams) hoặc MySQL Workbench (Reverse Engineer) để sinh ER Diagram của bộ dữ liệu
hnx_securities, so khớp với hình ở mục 4.3; (2) thêm bảngCustomerRepvàWatchlist(bài 7.3) vào sơ đồ; (3) tự chấm sơ đồ bằng checklist 4.5. Bài 7.1c, 7.1d, 7.1e là phiên bản chấm được của bước "xác định khóa và kiểm tra mô hình".
Sai lầm thường gặp
| Sai lầm | Hậu quả | Cách đúng |
|---|---|---|
Nhồi nhiều giá trị vào một ô ('VCB,FPT', nhiều số điện thoại) |
Vi phạm 1NF, không JOIN/ràng buộc/đánh index được | Tách bảng con hoặc bảng trung gian, mỗi giá trị một dòng |
| Quan hệ n-n mà không có bảng trung gian | Không giữ được nhiều đối tác, hoặc lặp dữ liệu | Bảng trung gian với hai FK và khóa chính kép |
| Đặt khóa chính là cột có thể đổi (số điện thoại, họ tên) | Đổi khóa kéo theo sửa hàng loạt bảng con | Dùng khóa đại diện (IDENTITY/AUTO_INCREMENT) và đặt UNIQUE cho mã nghiệp vụ |
| Quên khai báo khóa ngoài "vì ứng dụng đã kiểm tra rồi" | Import dữ liệu là xuất hiện dòng mồ côi | Luôn khai báo FK; dò mồ côi trước khi bật lại ràng buộc |
| Đặt cột tổng hợp (tổng tiền) vào bảng mà không có cơ chế cập nhật | Số liệu tổng lệch so với chi tiết | Tính khi truy vấn, hoặc phi chuẩn hóa có chủ đích kèm trigger/đồng bộ |
Dùng NOT IN (SELECT ...) khi tập con có NULL |
Kết quả rỗng/thiếu dòng mà không báo lỗi | NOT EXISTS hoặc LEFT JOIN ... IS NULL |
ON DELETE CASCADE lên lệnh/giao dịch |
Xóa một khách hàng làm mất luôn lịch sử giao dịch | Chặn xóa (NO ACTION), đóng khách bằng cột status |
In CONSTRAINT_NAME khi so sánh kết quả hai hệ |
Kết quả khác nhau vì tên tự sinh | Lọc/nối theo TABLE_NAME, COLUMN_NAME |
Tóm tắt
| Chủ đề | Điểm cần nhớ |
|---|---|
| 1-1 | Khóa ngoài + UNIQUE (hoặc FK là PK): CustomerStats, CustomerRep |
| 1-n | Khóa ngoài ở bảng "n": Customers-Accounts-Orders-Trades |
| n-n | Bảng trung gian với hai FK, khóa chính kép: Portfolio, Watchlist |
| PK / FK / UQ | PK: duy nhất + không NULL, 1 cái/bảng; FK: trỏ khóa bảng cha; UQ: duy nhất, nhiều cái, NULL tùy hệ |
| Composite key | Nhiều cột tạo một khóa: MarketPrices, Portfolio |
| DDL khóa | CONSTRAINT tên PRIMARY KEY/FOREIGN KEY/UNIQUE; xóa FK: DROP CONSTRAINT (T-SQL) / DROP FOREIGN KEY (MySQL) |
| 1NF / 2NF / 3NF | Ô nguyên tử / phụ thuộc toàn bộ khóa / không phụ thuộc bắc cầu |
| Lợi ích và cái giá | Ít dư thừa, dễ bảo trì ↔ nhiều JOIN; phi chuẩn hóa có chủ đích phải kèm cơ chế đồng bộ |
| ER Diagram | Entity - Attribute - Relationship; cardinality 1..1, 0..1, 1..n, 0..n |
INFORMATION_SCHEMA |
TABLE_CONSTRAINTS, KEY_COLUMN_USAGE, REFERENTIAL_CONSTRAINTS; MySQL nhớ lọc DATABASE() |
| Dòng mồ côi | NOT EXISTS; nhớ IS NOT NULL với FK cho phép NULL |
Bài tập thực hành của buổi
Nội dung syllabus không chấm tự động được: vẽ ER Diagram (thao tác đồ họa - làm theo hướng dẫn mục 4.4 và tự đối chiếu bằng checklist 4.5; bài 7.1c/7.1d/7.1e là phiên bản chấm được của bước "xác định khóa và kiểm tra mô hình").
Gợi ý mở rộng
- Thử thiết kế bảng
OrderFees(order_id, fee_type, fee_amount)với khóa kép và khóa ngoài trỏOrders; tự vẽ ER Diagram mở rộng (mẫu ở mục 2.5 và 4.4). - Xem
INFORMATION_SCHEMA.COLUMNSđể lập "từ điển dữ liệu": bảng, cột, kiểu, có NULL không. - Tìm hiểu BCNF và 4NF (những dạng chuẩn cao hơn 3NF) và
CHECKconstraint để ràng buộc miền giá trị (side IN ('BUY','SELL')). - Đọc trước Buổi 8: các quan hệ trên được "nối" lại bằng JOIN.