
Khóa chính khóa ngoại là gì? Khóa chính (primary key) là cột — hoặc nhóm cột — dùng để định danh duy nhất từng dòng trong một bảng, không được trùng và không được để rỗng. Khóa ngoại (foreign key) là cột ở bảng này chứa giá trị khóa chính của bảng kia, dùng để nối hai bảng lại với nhau và bắt cơ sở dữ liệu kiểm tra rằng giá trị đó có thật. Nói ngắn gọn: khóa chính trả lời câu hỏi “dòng này là ai?”, còn khóa ngoại trả lời “dòng này thuộc về ai?”.
Tôi làm phần mềm quản lý cho doanh nghiệp vừa và nhỏ nhiều năm, và đây là chỗ hầu hết mọi người vấp khi chuyển từ Google Sheet sang SQL. Trên Sheet, bạn gõ tên khách hàng vào ô là xong. Trong SQL, nếu bạn làm vậy thì ba tháng sau bạn sẽ có “Công ty TNHH Minh Phát”, “Cty Minh Phát” và “minh phat” — ba cái tên cho cùng một khách. Bài này giải thích hai khái niệm đó bằng ví dụ cửa hàng — kho — xưởng, kèm câu lệnh bạn chạy được ngay.
Khóa chính là gì? «Số định danh» của mỗi dòng dữ liệu
Khóa chính là cột định danh duy nhất cho mỗi dòng, giống số căn cước của một người. Mỗi bảng chỉ có một khóa chính, và giá trị của nó phải thỏa hai điều kiện: duy nhất (không dòng nào trùng dòng nào) và không rỗng (không được để trống).
Lấy bảng khách hàng của một cửa hàng vật tư xây dựng:
khach_hang(id, ho_ten, so_dien_thoai)- Dòng 1:
1 | Nguyễn Văn A | 0901xxxxxx - Dòng 2:
2 | Nguyễn Văn A | 0912xxxxxx
Hai người trùng tên vẫn phân biệt được, vì id khác nhau. Đó là toàn bộ công dụng của khóa chính.
Dùng id tự tăng hay UUID? Id tự tăng là dãy số 1, 2, 3… do cơ sở dữ liệu tự sinh — nhẹ, dễ đọc, dễ gọi qua điện thoại cho nhau (“đơn số 1045 nhé”). UUID là chuỗi ngẫu nhiên dài kiểu a3f1… — dài và khó đọc, nhưng không đụng nhau kể cả khi bạn gộp dữ liệu từ nhiều nguồn, nhiều chi nhánh, hoặc khi app ngoại tuyến tự sinh id trước rồi đồng bộ sau. Với phần lớn doanh nghiệp nhỏ chạy một hệ thống tập trung, id tự tăng là đủ và dễ sống hơn.
Khóa tự nhiên và khóa thay thế. Khóa tự nhiên là dữ liệu nghiệp vụ có sẵn — số điện thoại, mã số thuế, biển số xe. Khóa thay thế (surrogate key) là cột id vô nghĩa do hệ thống sinh ra. Tôi gần như luôn khuyên dùng khóa thay thế, vì dữ liệu nghiệp vụ hay thay đổi: khách đổi số điện thoại, hai vợ chồng dùng chung một số, nhân viên nhập nhầm rồi phải sửa. Nếu số điện thoại là khóa chính thì sửa một ô đó kéo theo phải sửa toàn bộ đơn hàng đang trỏ tới nó. Cách đúng là để id làm khóa chính, còn số điện thoại thì đặt ràng buộc unique để chống trùng.
Khóa ngoại là gì và nó bảo vệ dữ liệu của bạn thế nào?
Khóa ngoại là cột trỏ tới khóa chính của bảng khác, và cơ sở dữ liệu sẽ từ chối mọi giá trị không tồn tại ở bảng được trỏ tới. Ví dụ bảng don_hang có cột khach_hang_id tham chiếu tới khach_hang(id). Nếu ai đó cố lưu một đơn hàng với khach_hang_id = 999 trong khi không có khách số 999, câu lệnh sẽ báo lỗi và dữ liệu không vào được.
Đây là khác biệt lớn nhất so với Google Sheet. Trên Sheet, bạn có thể gõ bất cứ thứ gì vào ô “Khách hàng” — kể cả gõ nhầm. SQL thì không cho. Nó đứng ra làm người gác cổng, nên bạn không cần tin vào sự cẩn thận của từng nhân viên nhập liệu nữa.
Khóa ngoại còn quyết định chuyện gì xảy ra khi xóa dòng cha. Có ba hành vi hay dùng:
- RESTRICT (mặc định ở nhiều hệ quản trị): không cho xóa khách hàng nếu khách đó còn đơn hàng. An toàn nhất, và là lựa chọn tôi dùng cho dữ liệu tài chính.
- CASCADE: xóa khách thì xóa luôn mọi đơn hàng của khách đó. Rất tiện cho các bảng phụ thuộc hoàn toàn (ví dụ xóa đơn thì xóa dòng chi tiết đơn), nhưng cực kỳ nguy hiểm nếu áp lên dữ liệu lịch sử.
- SET NULL: xóa khách thì cột
khach_hang_idcủa các đơn cũ chuyển thành rỗng. Đơn vẫn còn nhưng mất thông tin khách — chỉ hợp khi quan hệ là tùy chọn.
Khóa chính và khóa ngoại trong SQL: câu lệnh tạo bảng mẫu
Phần khóa chính và khóa ngoại trong SQL được khai báo ngay lúc tạo bảng. Dưới đây là hai câu lệnh theo cú pháp PostgreSQL (cũng là cú pháp dùng trên Supabase):
CREATE TABLE khach_hang (id bigint generated always as identity primary key, ho_ten text not null, so_dien_thoai text unique);CREATE TABLE don_hang (id bigint generated always as identity primary key, ngay_dat date, khach_hang_id bigint not null references khach_hang(id));
Đọc từng mảnh cho dễ hiểu:
generated always as identity— để cơ sở dữ liệu tự đánh số, bạn không phải nghĩ.primary key— đây là khóa chính, tự động duy nhất và không rỗng.not null— bắt buộc phải có giá trị. Đặt ởkhach_hang_idnghĩa là: không có khách thì không được tạo đơn.unique— chống trùng nhưng vẫn cho phép sửa, hợp với số điện thoại.references khach_hang(id)— chính là khai báo khóa ngoại.
Bảng đã có sẵn dữ liệu thì sao? Dùng ALTER TABLE don_hang ADD CONSTRAINT don_hang_khach_hang_id_fkey FOREIGN KEY (khach_hang_id) REFERENCES khach_hang(id);. Lưu ý: nếu trong bảng đang tồn tại những dòng có khach_hang_id không khớp với bảng khách hàng, câu lệnh này sẽ báo lỗi và không chạy. Thực tế đó lại là chuyện tốt — nó chỉ cho bạn thấy dữ liệu rác đang nằm ở đâu để dọn trước.
Quan hệ 1 nhiều trong SQL: khóa ngoại luôn nằm ở phía «nhiều»
Quan hệ 1 nhiều trong SQL là trường hợp phổ biến nhất: một dòng ở bảng A tương ứng với nhiều dòng ở bảng B. Một khách hàng có nhiều đơn hàng. Một nhà cung cấp có nhiều phiếu nhập. Một phòng ban có nhiều nhân viên. Một đơn hàng có nhiều lần thanh toán.
Quy tắc duy nhất bạn cần nhớ: khóa ngoại luôn đặt ở bảng phía «nhiều». Bảng don_hang giữ cột khach_hang_id, chứ bảng khach_hang tuyệt đối không có cột kiểu “đơn_hàng_1, đơn_hàng_2, đơn_hàng_3”. Nhiều anh chị quen Excel hay thiết kế ngược lại, rồi kẹt ngay khi khách đặt tới đơn thứ tư.
Câu lệnh lấy danh sách đơn kèm tên khách:
SELECT d.id, d.ngay_dat, k.ho_ten FROM don_hang d JOIN khach_hang k ON k.id = d.khach_hang_id ORDER BY d.ngay_dat DESC;
Dòng ON k.id = d.khach_hang_id chính là chỗ khóa chính gặp khóa ngoại. Nếu bạn chưa quen với SELECT, JOIN và WHERE, tôi có viết riêng một bài về SQL là gì và 5 câu lệnh người mới nên biết để đọc trước.
Quan hệ nhiều nhiều trong SQL và bảng trung gian
Quan hệ nhiều nhiều trong SQL không thể dựng bằng một cột khóa ngoại — bắt buộc phải có bảng trung gian. Lý do: một đơn hàng chứa nhiều sản phẩm, đồng thời một sản phẩm xuất hiện trong nhiều đơn hàng. Không bên nào “chứa” bên nào, nên không biết đặt khóa ngoại ở đâu.
Cách giải: tạo bảng thứ ba, mỗi dòng là một cặp ghép, kèm các thông tin chỉ đúng với cặp đó:
CREATE TABLE chi_tiet_don (don_hang_id bigint not null references don_hang(id), san_pham_id bigint not null references san_pham(id), so_luong numeric not null, don_gia numeric not null, primary key (don_hang_id, san_pham_id));
Ở đây khóa chính là khóa ghép hai cột primary key (don_hang_id, san_pham_id): một sản phẩm chỉ được xuất hiện một lần trong một đơn. Hai cột so_luong và don_gia nằm ở bảng trung gian là đúng chỗ, vì cùng một sản phẩm bán cho đơn này 10 cái giá A, đơn kia 3 cái giá B.
Mô hình này lặp lại khắp nơi trong doanh nghiệp: nhân viên ↔ dự án (một người tham gia nhiều dự án, một dự án có nhiều người) qua bảng phan_cong(nhan_vien_id, du_an_id, vai_tro); hay định mức vật tư trong xưởng nội thất: sản phẩm ↔ vật tư qua bảng dinh_muc(san_pham_id, vat_tu_id, dinh_luong). Tôi từng dựng đúng kiểu bảng này cho một xưởng khi họ muốn biết đóng một bộ tủ bếp thì hết bao nhiêu mét ván — cấu trúc y hệt bảng chi tiết đơn, chỉ đổi tên.

Quan hệ 1-1 dùng khi nào?
Quan hệ 1-1 là khi một dòng ở bảng A ứng với đúng một dòng ở bảng B. Nó ít gặp hơn, và thường dùng để tách thông tin nhạy cảm hoặc ít dùng ra khỏi bảng chính. Ví dụ: bảng nhan_vien chứa họ tên, phòng ban, ngày vào làm — ai trong công ty cũng xem được; còn bảng ho_so_nhan_vien chứa mức lương, số CCCD, số tài khoản ngân hàng — chỉ nhân sự và kế toán xem. Khi đó ho_so_nhan_vien.nhan_vien_id vừa là khóa chính vừa là khóa ngoại trỏ về nhan_vien(id), và bạn phân quyền trên hai bảng khác nhau. Chủ đề phân quyền theo dòng tôi có bàn kỹ trong bài về Row Level Security trên Supabase và PostgreSQL.
Bảng tóm tắt các loại quan hệ
| Loại quan hệ | Ví dụ trong cửa hàng / xưởng | Khóa ngoại đặt ở đâu |
|---|---|---|
| 1 - nhiều | Một khách hàng có nhiều đơn hàng | Bảng phía «nhiều»: don_hang.khach_hang_id |
| 1 - nhiều | Một kho có nhiều phiếu nhập | phieu_nhap.kho_id |
| Nhiều - nhiều | Đơn hàng ↔ sản phẩm | Bảng trung gian chi_tiet_don, có cả hai cột id |
| Nhiều - nhiều | Nhân viên ↔ dự án | Bảng trung gian phan_cong |
| 1 - 1 | Nhân viên ↔ hồ sơ lương, giấy tờ | Bảng phụ, cột id vừa là khóa chính vừa là khóa ngoại |
Những lỗi thiết kế hay gặp khi mới chuyển từ Google Sheet sang SQL
Dưới đây là những lỗi tôi gặp đi gặp lại khi xem lại file dữ liệu của khách trước lúc dựng hệ thống:
- Lưu tên khách thay vì id trong bảng đơn. Khách đổi tên công ty một lần là lịch sử đơn hàng vỡ. Luôn lưu
khach_hang_id, còn tên thì lấy ra bằng JOIN khi cần hiển thị. - Nhét nhiều sản phẩm vào một ô. Kiểu “SP1, SP2, SP3” trong cột “Mặt hàng”. Ô đó nhìn thì gọn nhưng không cộng được số lượng, không tính được doanh thu theo mặt hàng, không tra được tồn kho. Mỗi sản phẩm phải là một dòng trong bảng trung gian.
- Quên tạo index cho cột khóa ngoại. PostgreSQL tự tạo index cho khóa chính nhưng không tự tạo cho khóa ngoại. Vài nghìn dòng thì chưa thấy gì, vài trăm nghìn dòng là truy vấn ì ạch. Khắc phục:
CREATE INDEX ON don_hang (khach_hang_id); - Dùng CASCADE cho dữ liệu lịch sử. Một thao tác xóa nhầm khách hàng có thể cuốn bay toàn bộ đơn hàng của họ, không hoàn tác được. Với bảng đơn hàng, phiếu nhập xuất, phiếu thu chi, hãy giữ RESTRICT và thêm cột trạng thái kiểu “ngưng hoạt động” thay vì xóa thật.
- Một bảng chứa nhiều loại đối tượng. Đây là tư duy Sheet khó bỏ nhất: một sheet “Dữ liệu” vừa có dòng khách, vừa có dòng đơn, vừa có dòng thu tiền. Nguyên tắc trong SQL là mỗi bảng chỉ chứa một loại đối tượng, và các bảng nối nhau bằng khóa ngoại.
Tóm lại và bước tiếp theo bạn tự làm được
Ba ý cần nhớ: khóa chính định danh duy nhất một dòng và nên là cột id vô nghĩa do hệ thống sinh ra; khóa ngoại nối hai bảng và bắt cơ sở dữ liệu kiểm tra giúp bạn; quan hệ 1-nhiều thì đặt khóa ngoại ở phía «nhiều», quan hệ nhiều-nhiều thì bắt buộc dựng bảng trung gian với khóa chính ghép hai cột.
Bước tiếp theo, bạn hãy lấy đúng file Excel hoặc Google Sheet đang dùng và làm ba việc: (1) liệt kê các loại đối tượng đang bị trộn trong đó — khách hàng, sản phẩm, đơn hàng, nhân viên — mỗi loại tách thành một bảng; (2) với từng cặp bảng, hỏi “một dòng bên này ứng với bao nhiêu dòng bên kia?” để xác định loại quan hệ; (3) viết thử câu CREATE TABLE cho hai bảng đầu tiên theo mẫu ở trên. Làm xong ba bước đó là bạn đã có sơ đồ dữ liệu dùng được cho một web app thật.
Nếu muốn xem một mô hình hoàn chỉnh có sẵn để đối chiếu, bạn đọc tiếp bài thiết kế database quản lý kho: bảng, cột, quan hệ và câu SQL mẫu. Còn nếu đang phân vân có nên rời Google Sheet hay chưa, bài ưu nhược điểm của Google Sheet khi dùng để quản lý công ty sẽ giúp bạn quyết định dựa trên quy mô dữ liệu thực tế.
Câu hỏi thường gặp
›Một bảng có thể có nhiều khóa chính không?
Không. Mỗi bảng chỉ được có đúng một khóa chính, nhưng khóa chính đó có thể gồm nhiều cột — gọi là khóa ghép, ví dụ primary key (don_hang_id, san_pham_id) trong bảng chi tiết đơn. Nếu bạn cần thêm cột khác cũng không được trùng (như số điện thoại, mã số thuế), hãy dùng ràng buộc unique thay vì cố tạo thêm khóa chính.
›Khóa ngoại có bắt buộc phải có không?
Về mặt kỹ thuật thì không — cơ sở dữ liệu vẫn chạy nếu bạn chỉ lưu số id mà không khai báo khóa ngoại. Nhưng khi đó không ai kiểm tra giúp bạn, nên dữ liệu rác sẽ lọt vào: đơn hàng trỏ tới khách không tồn tại, chi tiết đơn trỏ tới sản phẩm đã bị xóa. Với dữ liệu doanh nghiệp, tôi luôn khai báo khóa ngoại đầy đủ ngay từ đầu.
›Nên dùng id tự tăng hay UUID làm khóa chính?
Với doanh nghiệp nhỏ chạy một hệ thống tập trung, id tự tăng là đủ: ngắn, dễ đọc, dễ gọi tên khi trao đổi công việc. UUID hợp hơn khi bạn phải gộp dữ liệu từ nhiều chi nhánh hoặc nhiều nguồn, hoặc khi ứng dụng cần sinh id lúc đang ngoại tuyến rồi mới đồng bộ lên. Chọn một kiểu và dùng nhất quán cho cả hệ thống.
›Xóa khách hàng thì đơn hàng của họ sẽ ra sao?
Tùy vào hành vi bạn khai báo cho khóa ngoại. RESTRICT sẽ chặn, không cho xóa khi khách còn đơn hàng. CASCADE sẽ xóa luôn toàn bộ đơn hàng của khách đó. SET NULL giữ lại đơn nhưng xóa liên kết tới khách. Với dữ liệu bán hàng và tài chính, tôi khuyên dùng RESTRICT và thêm cột trạng thái 'ngưng hoạt động' thay vì xóa thật, để giữ nguyên lịch sử.
Tác giả
Chuyên gia chuyển đổi số · Đại diện 5F Edu
Hơn 8 năm triển khai và tư vấn chuyển đổi số cho doanh nghiệp vừa và nhỏ: chuẩn hoá quy trình, xây phần mềm quản lý bằng AppSheet và web app, tự động hoá bằng N8N. Người làm kênh YouTube Nghiện AppSheet.



