Chuyển tới nội dung chính

Bài 7: JOIN – Kết Hợp Nhiều Bảng

Giai đoạn 2 – Truy Vấn Dữ Liệu · Mục tiêu: Hiểu và sử dụng thành thạo các loại JOIN để kết hợp dữ liệu từ nhiều bảng.


1. Tại sao cần JOIN?

Trong CSDL thực tế, dữ liệu được tách ra nhiều bảng để tránh trùng lặp. Khi cần xem thông tin đầy đủ, ta phải kết hợp (JOIN) các bảng lại.

Ví dụ minh hoạ

Bảng KhachHang:

MaKHHoTenDienThoai
1Nguyễn Văn An0901234567
2Trần Thị Bình0912345678
3Lê Minh Châu0923456789

Bảng DonHang:

MaDHMaKHSanPhamTongTien
1011iPhone 1528990000
1021AirPods5990000
1032MacBook27490000

→ Câu hỏi: "Khách hàng nào đã mua gì?" → Cần JOIN 2 bảng.


2. INNER JOIN – Kết hợp các dòng khớp nhau

INNER JOIN chỉ trả về những dòng có giá trị khớp ở cả hai bảng.

Cú pháp

SELECT cot1, cot2, ...
FROM bang_A
INNER JOIN bang_B ON bang_A.cot_chung = bang_B.cot_chung;

Ví dụ

-- Liệt kê đơn hàng kèm tên khách hàng
SELECT
DonHang.MaDH,
KhachHang.HoTen,
KhachHang.DienThoai,
DonHang.SanPham,
DonHang.TongTien
FROM DonHang
INNER JOIN KhachHang ON DonHang.MaKH = KhachHang.MaKH;

Kết quả:

MaDHHoTenDienThoaiSanPhamTongTien
101Nguyễn Văn An0901234567iPhone 1528990000
102Nguyễn Văn An0901234567AirPods5990000
103Trần Thị Bình0912345678MacBook27490000

Lê Minh Châu (MaKH = 3) không xuất hiện vì chưa có đơn hàng.

Dùng Alias để viết ngắn gọn

SELECT
dh.MaDH,
kh.HoTen,
dh.SanPham,
dh.TongTien
FROM DonHang AS dh
INNER JOIN KhachHang AS kh ON dh.MaKH = kh.MaKH;

3. LEFT JOIN – Lấy tất cả từ bảng bên trái

LEFT JOIN trả về tất cả dòng từ bảng bên trái, dù bảng bên phải không có dòng khớp (sẽ hiển thị NULL).

-- Liệt kê TẤT CẢ khách hàng, kể cả chưa mua hàng
SELECT
kh.HoTen,
dh.MaDH,
dh.SanPham,
dh.TongTien
FROM KhachHang AS kh
LEFT JOIN DonHang AS dh ON kh.MaKH = dh.MaKH;

Kết quả:

HoTenMaDHSanPhamTongTien
Nguyễn Văn An101iPhone 1528990000
Nguyễn Văn An102AirPods5990000
Trần Thị Bình103MacBook27490000
Lê Minh ChâuNULLNULLNULL

→ Lê Minh Châu xuất hiện với các cột đơn hàng = NULL.

Tìm khách hàng chưa mua hàng

SELECT kh.HoTen, kh.DienThoai
FROM KhachHang AS kh
LEFT JOIN DonHang AS dh ON kh.MaKH = dh.MaKH
WHERE dh.MaDH IS NULL;

4. RIGHT JOIN – Lấy tất cả từ bảng bên phải

RIGHT JOIN ngược lại với LEFT JOIN: trả về tất cả dòng từ bảng bên phải.

-- Tất cả đơn hàng, kể cả đơn hàng không có thông tin khách (hiếm gặp)
SELECT
kh.HoTen,
dh.MaDH,
dh.SanPham
FROM KhachHang AS kh
RIGHT JOIN DonHang AS dh ON kh.MaKH = dh.MaKH;

:::tip LEFT JOIN vs RIGHT JOIN Trong thực tế, hầu hết mọi người sử dụng LEFT JOINđổi thứ tự bảng thay vì dùng RIGHT JOIN. Cả hai cho kết quả giống nhau:

-- Hai câu này cho kết quả giống nhau:
SELECT * FROM A LEFT JOIN B ON A.id = B.id;
SELECT * FROM B RIGHT JOIN A ON A.id = B.id;

:::


5. FULL JOIN – Lấy tất cả từ cả hai bảng

FULL JOIN trả về tất cả dòng từ cả hai bảng. Dòng không khớp sẽ có NULL.

:::warning MySQL không hỗ trợ FULL JOIN trực tiếp MySQL không có FULL OUTER JOIN. Cách giải quyết:

-- Mô phỏng FULL JOIN bằng UNION
SELECT kh.HoTen, dh.MaDH, dh.SanPham
FROM KhachHang AS kh
LEFT JOIN DonHang AS dh ON kh.MaKH = dh.MaKH

UNION

SELECT kh.HoTen, dh.MaDH, dh.SanPham
FROM KhachHang AS kh
RIGHT JOIN DonHang AS dh ON kh.MaKH = dh.MaKH;

:::


6. CROSS JOIN – Tích Descartes

CROSS JOIN kết hợp mỗi dòng của bảng A với mỗi dòng của bảng B.

-- Mỗi nhân viên × mỗi sản phẩm
SELECT nv.HoTen, sp.TenSP
FROM NhanVien AS nv
CROSS JOIN SanPham AS sp;
-- Nếu NhanVien có 5 dòng, SanPham có 3 dòng → kết quả: 5 × 3 = 15 dòng

:::caution Cẩn thận với CROSS JOIN CROSS JOIN tạo ra rất nhiều dòng (N × M). Chỉ dùng khi thực sự cần tạo tất cả tổ hợp. :::


7. Self Join – Tự kết hợp với chính mình

Một bảng JOIN với chính nó, hữu ích khi bảng có quan hệ cha-con.

-- Bảng NhanVien có cột MaQuanLy tham chiếu đến MaNV
SELECT
nv.HoTen AS NhanVien,
ql.HoTen AS QuanLy
FROM NhanVien AS nv
LEFT JOIN NhanVien AS ql ON nv.MaQuanLy = ql.MaNV;

Kết quả:

NhanVienQuanLy
Nguyễn Văn AnLê Minh Châu
Trần Thị BìnhLý Kim Khoa
Lê Minh ChâuNULL
......

8. JOIN nhiều bảng

-- Đơn hàng + Khách hàng + Nhân viên bán hàng
SELECT
dh.MaDH,
kh.HoTen AS KhachHang,
nv.HoTen AS NhanVienBan,
dh.SanPham,
dh.TongTien,
dh.NgayDat
FROM DonHang AS dh
INNER JOIN KhachHang AS kh ON dh.MaKH = kh.MaKH
INNER JOIN NhanVien AS nv ON dh.MaNV = nv.MaNV
ORDER BY dh.NgayDat DESC;

JOIN 4 bảng (Chi tiết đơn hàng)

SELECT
dh.MaDH,
kh.HoTen AS KhachHang,
sp.TenSP,
ct.SoLuong,
ct.DonGia,
ct.SoLuong * ct.DonGia AS ThanhTien
FROM ChiTietDonHang AS ct
INNER JOIN DonHang AS dh ON ct.MaDH = dh.MaDH
INNER JOIN KhachHang AS kh ON dh.MaKH = kh.MaKH
INNER JOIN SanPham AS sp ON ct.MaSP = sp.MaSP;

9. Bảng tóm tắt các loại JOIN

Loại JOINMô tảKết quả
INNER JOINChỉ lấy dòng khớp ở cả hai bảngA ∩ B
LEFT JOINTất cả bảng trái + khớp bảng phảiA + (A ∩ B)
RIGHT JOINTất cả bảng phải + khớp bảng tráiB + (A ∩ B)
FULL JOINTất cả cả hai bảngA ∪ B
CROSS JOINTích Descartes (mọi tổ hợp)A × B
Self JOINBảng JOIN với chính nóQuan hệ cha-con

Bài tập thực hành

Dữ liệu mẫu

CREATE DATABASE IF NOT EXISTS BaiTapJoin;
USE BaiTapJoin;

CREATE TABLE PhongBan (
MaPB INT PRIMARY KEY,
TenPB VARCHAR(100),
DiaDiem VARCHAR(100)
);

CREATE TABLE NhanVien (
MaNV INT AUTO_INCREMENT PRIMARY KEY,
HoTen VARCHAR(100) NOT NULL,
MaPB INT,
Luong DECIMAL(12,2),
MaQuanLy INT,
FOREIGN KEY (MaPB) REFERENCES PhongBan(MaPB),
FOREIGN KEY (MaQuanLy) REFERENCES NhanVien(MaNV)
);

CREATE TABLE DuAn (
MaDA INT AUTO_INCREMENT PRIMARY KEY,
TenDA VARCHAR(200),
NgayBatDau DATE,
NgayKetThuc DATE
);

CREATE TABLE PhanCong (
MaNV INT,
MaDA INT,
VaiTro VARCHAR(50),
PRIMARY KEY (MaNV, MaDA),
FOREIGN KEY (MaNV) REFERENCES NhanVien(MaNV),
FOREIGN KEY (MaDA) REFERENCES DuAn(MaDA)
);

INSERT INTO PhongBan VALUES
(1, 'Kỹ thuật', 'Tầng 3'),
(2, 'Kinh doanh', 'Tầng 2'),
(3, 'Nhân sự', 'Tầng 1'),
(4, 'Marketing', 'Tầng 4');

INSERT INTO NhanVien (HoTen, MaPB, Luong, MaQuanLy) VALUES
('Nguyễn Văn An', 1, 30000000, NULL),
('Trần Thị Bình', 1, 20000000, 1),
('Lê Minh Châu', 2, 25000000, NULL),
('Phạm Đức Dũng', 2, 15000000, 3),
('Hoàng Thị Em', 3, 18000000, NULL),
('Võ Văn Phong', NULL, 12000000, 1);

INSERT INTO DuAn VALUES
(1, 'Website Bán Hàng', '2024-01-01', '2024-06-30'),
(2, 'App Di Động', '2024-03-01', '2024-12-31'),
(3, 'Hệ Thống CRM', '2024-06-01', NULL);

INSERT INTO PhanCong VALUES
(1, 1, 'Trưởng nhóm'),
(2, 1, 'Lập trình viên'),
(1, 2, 'Kiến trúc sư'),
(4, 2, 'Kinh doanh'),
(2, 3, 'Lập trình viên'),
(5, 3, 'Nhân sự');

Bài tập

  1. Liệt kê nhân viên kèm tên phòng ban (INNER JOIN).
  2. Liệt kê tất cả nhân viên kèm phòng ban, kể cả nhân viên chưa thuộc phòng ban nào (LEFT JOIN).
  3. Liệt kê tất cả phòng ban, kể cả phòng ban chưa có nhân viên (LEFT JOIN đổi thứ tự).
  4. Liệt kê nhân viên và tên quản lý của họ (Self Join).
  5. Liệt kê nhân viên, dự án họ tham gia và vai trò (JOIN 3 bảng).
  6. Tìm nhân viên chưa tham gia dự án nào.
  7. Tìm dự án chưa có ai tham gia.
  8. Thống kê số nhân viênlương trung bình theo phòng ban (JOIN + GROUP BY).
  9. Tìm dự án có nhiều hơn 2 thành viên.
  10. Liệt kê nhân viên tham gia nhiều hơn 1 dự án, kèm tên phòng ban.

Quay lại: Roadmap · Bài tiếp: Bài 8 – Truy Vấn Con (Subquery)