Primary Key vs Foreign Key (Khóa chính vs Khóa ngoại)
Bạn sẽ Học được gì
Kết thúc trang này, bạn sẽ có thể:
- Giải thích Khóa chính (Primary Key) là gì và tại sao mọi bảng đều cần một
- Giải thích Khóa ngoại (Foreign Key) là gì và cách nó thiết lập mối quan hệ giữa các bảng
- So sánh Khóa chính và Khóa ngoại — mục đích, ràng buộc (Constraint), và hành vi của chúng
- Hiểu Tính toàn vẹn tham chiếu (Referential Integrity) và cách Khóa ngoại thực thi nó
- Chọn giữa Khóa tự nhiên (Natural Key) và Khóa thay thế (Surrogate Key) với các đánh đổi
- Áp dụng các Best Practices khi định nghĩa khóa trong schema thực tế
Vấn đề: Nhận diện và Liên kết Dữ liệu
Hãy tưởng tượng một cơ sở dữ liệu cho nền tảng thương mại điện tử. Bạn có hàng triệu đơn hàng, hàng nghìn sản phẩm, và hàng nghìn người dùng. Hai câu hỏi cơ bản nảy sinh:
- Làm sao để nhận diện duy nhất mỗi dòng dữ liệu? — Không có một định danh đáng tin cậy, bạn không thể phân biệt đơn hàng này với đơn hàng khác, người dùng này với người dùng khác.
- Làm sao để liên kết dữ liệu liên quan giữa các bảng? — Một đơn hàng thuộc về một người dùng và chứa các sản phẩm. Không có mối quan hệ rõ ràng, dữ liệu bị đứt gãy và không đáng tin cậy.
Khóa chính (Primary Key) giải quyết vấn đề đầu tiên. Khóa ngoại (Foreign Key) giải quyết vấn đề thứ hai. Cùng nhau, chúng là xương sống của thiết kế cơ sở dữ liệu quan hệ.
Khóa chính (Primary Key)
Khóa chính (Primary Key) là một cột (hoặc tập hợp các cột) nhận diện duy nhất mỗi dòng trong một bảng. Mọi bảng trong cơ sở dữ liệu quan hệ nên có chính xác một khóa chính.
Thuộc tính
| Thuộc tính | Mô tả |
|---|---|
| Duy nhất (Unique) | Không có hai dòng nào có cùng giá trị khóa chính |
| Không NULL (Not NULL) | Các cột khóa chính không thể chứa giá trị NULL |
| Bất biến (Immutable) | Giá trị khóa chính không nên thay đổi theo thời gian |
| Chỉ một mỗi bảng | Một bảng chỉ có thể có một khóa chính (dù có thể là khóa tổng hợp - Composite Key) |
Tạo Khóa chính
-- Khóa chính một cột
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(255)
);
-- Ràng buộc có tên (khuyến nghị cho production)
CREATE TABLE users (
id INT,
name VARCHAR(100),
email VARCHAR(255),
CONSTRAINT pk_users PRIMARY KEY (id)
);
-- Khóa chính tổng hợp (Composite Primary Key - nhiều cột)
CREATE TABLE order_items (
order_id INT,
product_id INT,
quantity INT,
PRIMARY KEY (order_id, product_id)
);
Điều gì tạo nên một Khóa chính tốt?
| Tiêu chí | Tại sao quan trọng |
|---|---|
| Duy nhất (Unique) | Không bao giờ trùng lặp — đảm bảo mọi dòng đều có thể nhận diện |
| Không bao giờ NULL | NULL không thể so sánh, nên không thể dùng làm định danh đáng tin cậy |
| Bất biến (Immutable) | Thay đổi khóa chính yêu cầu cập nhật tất cả khóa ngoại tham chiếu đến nó |
| Đơn giản (Simple) | Ưu tiên một cột số nguyên thay vì khóa tổng hợp nhiều cột |
| Hiệu quả (Efficient) | Kiểu dữ liệu nhỏ (INT, BIGINT) đánh chỉ mục và nối (Join) nhanh hơn chuỗi hoặc UUID |
Khóa ngoại (Foreign Key)
Khóa ngoại (Foreign Key) là một cột (hoặc tập hợp các cột) trong một bảng tham chiếu đến khóa chính của bảng khác. Nó thiết lập mối quan hệ giữa hai bảng và thực thi Tính toàn vẹn tham chiếu (Referential Integrity).
Thuộc tính
| Thuộc tính | Mô tả |
|---|---|
| Tham chiếu khóa chính | Khóa ngoại trỏ đến khóa chính (hoặc khóa duy nhất) trong bảng khác |
| Có thể NULL | Khóa ngoại có thể NULL, nghĩa là mối quan hệ là tùy chọn |
| Có thể trùng lặp | Nhiều dòng có thể tham chiếu đến cùng một dòng cha (Một-nhiều - One-to-Many) |
| Nhiều mỗi bảng | Một bảng có thể có nhiều khóa ngoại tham chiếu đến các bảng khác nhau |
Tạo Khóa ngoại
CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT,
order_date DATE,
status VARCHAR(20),
CONSTRAINT fk_orders_users
FOREIGN KEY (user_id)
REFERENCES users(id)
);
Tính toàn vẹn Tham chiếu (Referential Integrity)
Tính toàn vẹn tham chiếu đảm bảo rằng mọi giá trị khóa ngoại khớp với một khóa chính tồn tại trong bảng được tham chiếu — không có tham chiếu mồ côi (Orphaned Reference).
- Đơn hàng 101, 102, và 103 hợp lệ —
user_idtham chiếu đến người dùng thực. - Đơn hàng 104 không hợp lệ —
user_id = 999không tồn tại trongusers. Ràng buộc khóa ngoại sẽ từ chối lệnh insert này.
Hành động ON DELETE / ON UPDATE
Khi một dòng được tham chiếu bị xóa hoặc cập nhật, cơ sở dữ liệu phải quyết định điều gì xảy ra với các dòng tham chiếu đến nó. Khóa ngoại hỗ trợ hành động tham chiếu (Referential Actions):
CONSTRAINT fk_orders_users
FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE CASCADE -- xóa đơn hàng khi người dùng bị xóa
ON UPDATE CASCADE; -- cập nhật user_id khi users.id thay đổi
| Hành động | ON DELETE | ON UPDATE |
|---|---|---|
| CASCADE | Tự động xóa các dòng tham chiếu | Tự động cập nhật các dòng tham chiếu |
| SET NULL | Đặt khóa ngoại thành NULL | Đặt khóa ngoại thành NULL |
| SET DEFAULT | Đặt khóa ngoại về giá trị mặc định | Đặt khóa ngoại về giá trị mặc định |
| RESTRICT | Ngăn chặn xóa (lỗi) | Ngăn chặn cập nhật (lỗi) |
| NO ACTION | Giống RESTRICT (trong một số DB có thể trì hoãn) | Giống RESTRICT (trong một số DB có thể trì hoãn) |
RESTRICT (hoặc NO ACTION) là mặc định an toàn nhất — nó buộc ứng dụng xử lý mối quan hệ một cách rõ ràng. Chỉ sử dụng CASCADE khi bạn chắc chắn rằng xóa cha luôn nên xóa con (ví dụ: chi tiết đơn hàng khi đơn hàng bị xóa).
Khóa chính vs Khóa ngoại: So sánh
| Khóa chính (Primary Key) | Khóa ngoại (Foreign Key) | |
|---|---|---|
| Mục đích | Nhận diện duy nhất mỗi dòng trong bảng | Tham chiếu đến khóa chính trong bảng khác |
| Tính duy nhất | Phải duy nhất | Có thể có giá trị trùng lặp |
| NULL | Không thể NULL | Có thể NULL (mối quan hệ tùy chọn) |
| Mỗi bảng | Chính xác một | Không hoặc nhiều |
| Hướng | Được tham chiếu bởi khóa ngoại | Tham chiếu đến khóa chính |
| Tự tạo | Thường tự tăng (Auto-increment) hoặc UUID | Giá trị đến từ bảng được tham chiếu |
| Chỉ mục (Index) | Tự động được đánh chỉ mục (Clustered trong hầu hết DB) | Không tự động đánh chỉ mục trong một số DB (ví dụ: PostgreSQL, MySQL InnoDB) |
| Loại ràng buộc | Tính toàn vẹn thực thể (Entity Integrity) | Tính toàn vẹn tham chiếu (Referential Integrity) |
Bản số của Mối quan hệ (Relationship Cardinality)
Khóa ngoại định nghĩa bản số (Cardinality) của mối quan hệ giữa các bảng:
| Mối quan hệ | Ví dụ | Cách hoạt động |
|---|---|---|
| Một-Nhiều (One-to-Many) | Một người dùng → nhiều đơn hàng | Khóa ngoại ở phía "nhiều" (orders.user_id) |
| Một-Một (One-to-One) | Một người dùng → một hồ sơ | Khóa ngoại ở một trong hai phía với ràng buộc UNIQUE |
| Nhiều-Nhiều (Many-to-Many) | Sinh viên ↔ khóa học | Bảng nối (Junction Table) với hai khóa ngoại |
-- Một-Một: hồ sơ người dùng
CREATE TABLE profiles (
id INT PRIMARY KEY,
user_id INT UNIQUE, -- UNIQUE biến nó thành 1:1
bio TEXT,
CONSTRAINT fk_profiles_users FOREIGN KEY (user_id) REFERENCES users(id)
);
-- Nhiều-Nhiều: sinh viên ↔ khóa học (bảng nối - Junction Table)
CREATE TABLE enrollments (
student_id INT,
course_id INT,
enrolled_at TIMESTAMP,
PRIMARY KEY (student_id, course_id),
FOREIGN KEY (student_id) REFERENCES students(id),
FOREIGN KEY (course_id) REFERENCES courses(id)
);
Khóa tự nhiên (Natural Key) vs Khóa thay thế (Surrogate Key)
Khi chọn khóa chính, bạn đối mặt với một quyết định thiết kế cơ bản:
| Khóa tự nhiên (Natural Key) | Khóa thay thế (Surrogate Key) | |
|---|---|---|
| Định nghĩa | Cột có ý nghĩa nghiệp vụ (ví dụ: email, ISBN, SSN) | Cột được tạo nhân tạo không có ý nghĩa nghiệp vụ (ví dụ: INT tự tăng, UUID) |
| Ví dụ | email, isbn, tax_id | id INT AUTO_INCREMENT, id UUID |
| Ưu điểm | Không cần cột phụ; có ý nghĩa trực tiếp | Không bao giờ thay đổi; đơn giản; đồng nhất giữa các bảng |
| Nhược điểm | Có thể thay đổi (email, SSN); có thể là tổng hợp; có thể lớn | Cột phụ; không có ý nghĩa nghiệp vụ |
| Hiệu năng | Có thể chậm nếu khóa là chuỗi dài | Nối (Join) nhanh trên kiểu số nguyên nhỏ |
Khóa thay thế (khuyến nghị)
-- Số nguyên tự tăng (phổ biến nhất)
CREATE TABLE users (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- PostgreSQL 10+
name VARCHAR(100),
email VARCHAR(255) UNIQUE
);
-- SQL Server
CREATE TABLE users (
id INT IDENTITY(1,1) PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(255) UNIQUE
);
-- UUID (cho hệ thống phân tán - Distributed Systems)
CREATE TABLE users (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY, -- PostgreSQL 13+
name VARCHAR(100),
email VARCHAR(255) UNIQUE
);
Khi nào nên sử dụng Khóa tự nhiên
Khóa tự nhiên phù hợp khi định danh nghiệp vụ thực sự bất biến và đảm bảo duy nhất:
- Mã quốc gia ISO (
US,VN,DE) - Mã tiền tệ (
USD,EUR) - Mã ngôn ngữ (
en,vi)
Tránh sử dụng email, số điện thoại, hoặc SSN làm khóa chính. Chúng có thể thay đổi, được sử dụng lại, hoặc có vấn đề về quyền riêng tư. Sử dụng khóa thay thế và đặt ràng buộc UNIQUE trên định danh tự nhiên.
Đánh chỉ mục Khóa ngoại (Foreign Key Indexing)
Một yếu tố quan trọng về hiệu năng: các cột khóa ngoại nên được đánh chỉ mục (Indexed).
Nếu không có index trên orders.user_id, cơ sở dữ liệu phải thực hiện quét toàn bảng (Full Table Scan) trên orders mỗi khi một dòng trong users bị xóa hoặc cập nhật (để thực thi ràng buộc khóa ngoại). Điều này có thể ảnh hưởng nghiêm trọng đến hiệu năng.
-- PostgreSQL, MySQL InnoDB: khóa ngoại KHÔNG được tự động đánh chỉ mục ở phía tham chiếu
-- Bạn phải tạo index thủ công:
CREATE INDEX idx_orders_user_id ON orders(user_id);
-- SQL Server: khóa ngoại CŨNG KHÔNG được tự động đánh chỉ mục
-- Cùng quy tắc áp dụng — luôn đánh chỉ mục các cột khóa ngoại của bạn.
Luôn đánh chỉ mục các cột khóa ngoại. Đây là một trong những lỗi hiệu năng phổ biến nhất trong thiết kế cơ sở dữ liệu. Một số ORM và framework không làm điều này tự động.
Các Lỗi Phổ biến
- Sử dụng khóa tự nhiên có thể thay đổi — nếu khóa chính thay đổi, mọi khóa ngoại tham chiếu đến nó cũng phải thay đổi. Sử dụng khóa thay thế để tránh điều này.
- Thiếu index cho khóa ngoại — không có index trên cột khóa ngoại, các lệnh xóa và cập nhật trên bảng cha gây ra quét toàn bảng (Full Table Scan).
- Lạm dụng CASCADE —
ON DELETE CASCADEcó thể âm thầm xóa lượng lớn dữ liệu. Sử dụng có chủ đích, không phải theo mặc định. - Không xử lý tham chiếu vòng (Circular References) — hai bảng tham chiếu lẫn nhau có thể làm cho insert và delete phức tạp. Phá vỡ chu trình bằng khóa ngoại nullable hoặc ràng buộc trì hoãn (Deferred Constraints).
- Sử dụng GUID/UUID làm Clustered Index — UUID ngẫu nhiên gây phân mảnh chỉ mục (Index Fragmentation). Sử dụng UUID tuần tự hoặc số nguyên thay thế cho khóa chính Clustered.
- Khóa chính tổng hợp (Composite Primary Key) trong bảng lớn — khóa chính nhiều cột làm tất cả các tham chiếu khóa ngoại rộng hơn và chậm hơn.Ưu tiên khóa thay thế một cột.
Best Practices
- Luôn định nghĩa khóa chính — mọi bảng nên có một. Không có nó, bạn không thể nhận diện hoặc cập nhật dòng một cách đáng tin cậy.
- Ưu tiên khóa thay thế (Surrogate Key) — sử dụng số nguyên tự tăng hoặc UUID. Giữ định danh tự nhiên dưới dạng ràng buộc UNIQUE.
- Luôn định nghĩa khóa ngoại — đừng dựa vào logic ứng dụng để duy trì mối quan hệ. Cơ sở dữ liệu thực thi tính toàn vẹn ngay cả khi ứng dụng có lỗi.
- Đánh chỉ mục mọi cột khóa ngoại — điều này quan trọng cho hiệu năng nối (Join Performance) và thực thi ràng buộc.
- Sử dụng RESTRICT theo mặc định — chỉ chọn CASCADE khi bạn có yêu cầu nghiệp vụ rõ ràng cho hành vi cascade.
- Giữ khóa chính nhỏ — INT hoặc BIGINT là lý tưởng. Mọi khóa ngoại nhân bản giá trị khóa chính, nên khóa nhỏ hơn nghĩa là chỉ mục nhỏ hơn và nối nhanh hơn.
- Không bao giờ thay đổi giá trị khóa chính — nếu bạn cần thay đổi định danh, thêm khóa thay thế mới và giữ giá trị cũ dưới dạng cột unique.
Câu hỏi Phỏng vấn
1. Sự khác biệt giữa Khóa chính (Primary Key) và Khóa ngoại (Foreign Key) là gì?
| Khóa chính (Primary Key) | Khóa ngoại (Foreign Key) | |
|---|---|---|
| Mục đích | Nhận diện duy nhất một dòng | Tham chiếu đến một dòng trong bảng khác |
| Tính duy nhất | Phải duy nhất | Có thể trùng lặp |
| NULL | Không thể NULL | Có thể NULL |
| Mỗi bảng | Chính xác một | Không hoặc nhiều |
| Thực thi | Tính toàn vẹn thực thể (Entity Integrity) | Tính toàn vẹn tham chiếu (Referential Integrity) |
| Tự đánh Index | Có (luôn luôn) | Phụ thuộc vào cơ sở dữ liệu |
2. Tính toàn vẹn tham chiếu (Referential Integrity) là gì và được thực thi như thế nào?
Tính toàn vẹn tham chiếu đảm bảo rằng mọi giá trị khóa ngoại khớp với một khóa chính tồn tại trong bảng được tham chiếu. Nó ngăn chặn:
- Insert một dòng con với cha không tồn tại
- Delete một dòng cha vẫn còn được tham chiếu bởi dòng con (trừ khi dùng CASCADE)
- Update khóa chính của cha trong khi dòng con vẫn tham chiếu đến giá trị cũ
Nó được thực thi bởi cơ sở d ữ liệu thông qua ràng buộc khóa ngoại (Foreign Key Constraint) — cơ sở dữ liệu kiểm tra mọi INSERT, UPDATE, và DELETE dựa trên ràng buộc.
3. Sự khác biệt giữa Khóa tự nhiên (Natural Key) và Khóa thay thế (Surrogate Key) là gì?
| Khóa tự nhiên (Natural Key) | Khóa thay thế (Surrogate Key) | |
|---|---|---|
| Nguồn | Dữ liệu nghiệp vụ (email, ISBN, SSN) | Được tạo ra (tự tăng, UUID) |
| Có ý nghĩa | Có — có ý nghĩa nghiệp vụ | Không — định danh nhân tạo |
| Ổn định | Có thể thay đổi theo thời gian | Không bao giờ thay đổi |
| Kích thước | Có thể lớn (chuỗi) | Nhỏ và đồng nhất (INT, BIGINT) |
| Khuyến nghị | Sử dụng làm ràng buộc UNIQUE | Sử dụng làm khóa chính |
Khóa thay thế thường được ưu tiên vì ổn định, đơn giản và hiệu năng tốt.
4. Điều gì xảy ra khi bạn xóa một dòng được tham chiếu bởi khóa ngoại?
Nó phụ thuộc vào hành động ON DELETE được định nghĩa trên ràng buộc khóa ngoại:
| Hành động | Kết quả |
|---|---|
| RESTRICT / NO ACTION | Lệnh xóa bị từ chối — lỗi được trả về |
| CASCADE | Tất cả các dòng tham chiếu được tự động xóa |
| SET NULL | Các cột khóa ngoại trong dòng tham chiếu được đặt thành NULL |
| SET DEFAULT | Các cột khóa ngoại được đặt về giá trị mặc định |
Mặc định là RESTRICT, là lựa chọn an toàn nhất.
5. Tại sao các cột khóa ngoại nên được đánh chỉ mục (Indexed)?
Nếu không có index trên cột khóa ngoại:
- Xóa dòng cha yêu cầu quét toàn bảng (Full Table Scan) trên bảng con để xác nhận không có tham chiếu (hoặc tìm dòng để cascade)
- Nối (Join) bảng cha và con chậm — cơ sở dữ liệu không thể tra cứu dòng khớp hiệu quả
- Khóa (Locking) có thể leo thang từ mức dòng lên mức bảng trong một số cơ sở dữ liệu (ví dụ: MySQL InnoDB)
Đánh chỉ mục cột khóa ngoại đảm bảo tra cứu O(log n) thay vì quét O(n).
6. Một bảng có thể có nhiều Khóa ngoại không?
Có. Một bảng có thể có bao nhiêu khóa ngoại tùy cần, mỗi khóa tham chiếu đến một bảng khác (hoặc thậm chí cùng bảng cho mối quan hệ tự tham chiếu - Self-referencing).
CREATE TABLE order_items (
id INT PRIMARY KEY,
order_id INT REFERENCES orders(id), -- FK đến orders
product_id INT REFERENCES products(id), -- FK đến products
quantity INT
);
Ví dụ phổ biến:
order_itemstham chiếu cảordersvàproductsemployeestham chiếudepartments(phòng ban) vàemployees(quản lý) — tự tham chiếupoststham chiếuusers(tác giả) vàcategories(danh mục)
Tìm hiểu thêm
- Các Thuộc tính ACID — cách giao dịch đảm bảo tính nhất quán
- SQL Joins — truy vấn dữ liệu qua các bảng có mối quan hệ
- Indexing trong Cơ sở dữ liệu — cách Index tăng tốc truy vấn trên khóa
- Database Locking — kiểm soát đồng thời khi sửa đổi dữ liệu liên quan