Bài 8: Truy Vấn Con – Subquery
Giai đoạn 2 – Truy Vấn Dữ Liệu · Mục tiêu: Hiểu và sử dụng truy vấn con (subquery) trong WHERE, FROM, SELECT và kết hợp với EXISTS, ANY, ALL.
1. Subquery là gì?
Subquery (truy vấn con) là một câu SELECT nằm bên trong câu SQL khác. Kết quả của subquery được dùng như dữ liệu đầu vào cho câu truy vấn bên ngoài.
-- Tìm nhân viên có lương cao nhất
SELECT HoTen, Luong FROM NhanVien
WHERE Luong = (SELECT MAX(Luong) FROM NhanVien);
-- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ← Subquery
Phân loại
| Loại | Kết quả trả về | Dùng với |
|---|---|---|
| Scalar (đơn giá trị) | 1 giá trị duy nhất | =, >, < |
| Row (1 dòng) | 1 dòng nhiều cột | =, IN |
| Table (bảng) | Nhiều dòng nhiều cột | IN, EXISTS, FROM |
2. Subquery trong WHERE
Với toán tử so sánh (Scalar Subquery)
-- Nhân viên có lương cao hơn lương trung bình
SELECT HoTen, Luong FROM NhanVien
WHERE Luong > (SELECT AVG(Luong) FROM NhanVien);
-- Sản phẩm có giá = giá cao nhất
SELECT TenSP, Gia FROM SanPham
WHERE Gia = (SELECT MAX(Gia) FROM SanPham);
-- Đơn hàng mới nhất
SELECT * FROM DonHang
WHERE NgayDat = (SELECT MAX(NgayDat) FROM DonHang);
Với IN (Table Subquery)
-- Khách hàng đã từng mua hàng
SELECT HoTen, DienThoai FROM KhachHang
WHERE MaKH IN (SELECT DISTINCT MaKH FROM DonHang);
-- Sản phẩm chưa từng được bán
SELECT TenSP FROM SanPham
WHERE MaSP NOT IN (SELECT DISTINCT MaSP FROM ChiTietDonHang);
-- Nhân viên thuộc phòng ban ở Tầng 3
SELECT HoTen FROM NhanVien
WHERE MaPB IN (SELECT MaPB FROM PhongBan WHERE DiaDiem = 'Tầng 3');
Subquery lồng nhiều cấp
-- Khách hàng mua sản phẩm do nhân viên phòng Kinh doanh bán
SELECT HoTen FROM KhachHang
WHERE MaKH IN (
SELECT MaKH FROM DonHang
WHERE MaNV IN (
SELECT MaNV FROM NhanVien
WHERE PhongBan = 'Kinh doanh'
)
);
3. Subquery trong FROM (Derived Table)
Subquery trong FROM tạo ra một bảng tạm để truy vấn tiếp.
-- Tìm phòng ban có tổng lương cao nhất
SELECT PhongBan, TongLuong
FROM (
SELECT PhongBan, SUM(Luong) AS TongLuong
FROM NhanVien
GROUP BY PhongBan
) AS ThongKe
WHERE TongLuong = (
SELECT MAX(TongLuong) FROM (
SELECT SUM(Luong) AS TongLuong
FROM NhanVien
GROUP BY PhongBan
) AS T
);
:::info Bắt buộc đặt Alias Subquery trong FROM phải có alias (tên tạm):
-- SAI: Thiếu alias
FROM (SELECT ...)
-- ĐÚNG: Có alias
FROM (SELECT ...) AS TenTam
:::
-- Top 3 khách hàng mua nhiều nhất
SELECT KhachHang, TongMua
FROM (
SELECT
kh.HoTen AS KhachHang,
SUM(dh.TongTien) AS TongMua
FROM DonHang AS dh
INNER JOIN KhachHang AS kh ON dh.MaKH = kh.MaKH
GROUP BY kh.MaKH
) AS ThongKe
ORDER BY TongMua DESC
LIMIT 3;
4. Subquery trong SELECT
Subquery trong SELECT trả về 1 giá trị cho mỗi dòng.
-- Mỗi nhân viên và tỷ lệ lương so với trung bình
SELECT
HoTen,
Luong,
(SELECT AVG(Luong) FROM NhanVien) AS LuongTB,
ROUND(Luong / (SELECT AVG(Luong) FROM NhanVien) * 100, 1) AS TyLePhanTram
FROM NhanVien;
-- Mỗi phòng ban và số nhân viên
SELECT
TenPB,
(SELECT COUNT(*) FROM NhanVien WHERE NhanVien.MaPB = PhongBan.MaPB) AS SoNV
FROM PhongBan;
5. EXISTS và NOT EXISTS
EXISTS kiểm tra xem subquery có trả về ít nhất 1 dòng hay không.
EXISTS
-- Khách hàng đã từng mua hàng (có ít nhất 1 đơn)
SELECT kh.HoTen FROM KhachHang AS kh
WHERE EXISTS (
SELECT 1 FROM DonHang AS dh WHERE dh.MaKH = kh.MaKH
);
NOT EXISTS
-- Khách hàng chưa từng mua hàng
SELECT kh.HoTen FROM KhachHang AS kh
WHERE NOT EXISTS (
SELECT 1 FROM DonHang AS dh WHERE dh.MaKH = kh.MaKH
);
-- Sản phẩm chưa từng được đặt
SELECT sp.TenSP FROM SanPham AS sp
WHERE NOT EXISTS (
SELECT 1 FROM ChiTietDonHang AS ct WHERE ct.MaSP = sp.MaSP
);
:::tip EXISTS vs IN
IN | EXISTS | |
|---|---|---|
| Cách hoạt động | So sánh với danh sách giá trị | Kiểm tra có dòng nào thoả không |
| Hiệu năng (bảng nhỏ) | Tương đương | Tương đương |
| Hiệu năng (bảng lớn) | Chậm hơn | Nhanh hơn |
| Xử lý NULL | Có thể sai kết quả | An toàn |
Khuyến nghị: Dùng EXISTS khi bảng con lớn, dùng IN khi danh sách giá trị nhỏ.
:::
6. ANY và ALL
ANY (SOME) – So sánh với bất kỳ giá trị nào
-- Nhân viên có lương > lương BẤT KỲ nhân viên nào ở phòng Nhân sự
SELECT HoTen, Luong FROM NhanVien
WHERE Luong > ANY (
SELECT Luong FROM NhanVien WHERE PhongBan = 'Nhân sự'
);
-- Tương đương: Luong > MIN(lương phòng Nhân sự)
ALL – So sánh với tất cả giá trị
-- Nhân viên có lương > lương TẤT CẢ nhân viên ở phòng Nhân sự
SELECT HoTen, Luong FROM NhanVien
WHERE Luong > ALL (
SELECT Luong FROM NhanVien WHERE PhongBan = 'Nhân sự'
);
-- Tương đương: Luong > MAX(lương phòng Nhân sự)
Bảng so sánh
| Toán tử | Ý nghĩa | Tương đương |
|---|---|---|
> ANY | Lớn hơn ít nhất 1 giá trị | > MIN(...) |
> ALL | Lớn hơn tất cả giá trị | > MAX(...) |
< ANY | Nhỏ hơn ít nhất 1 giá trị | < MAX(...) |
< ALL | Nhỏ hơn tất cả giá trị | < MIN(...) |
= ANY | Bằng ít nhất 1 giá trị | IN (...) |
7. Correlated Subquery (Subquery tương quan)
Subquery tương quan là subquery phụ thuộc vào dòng hiện tại của câu truy vấn ngoài.
-- Nhân viên có lương cao nhất trong phòng ban của họ
SELECT HoTen, PhongBan, Luong
FROM NhanVien AS nv1
WHERE Luong = (
SELECT MAX(Luong)
FROM NhanVien AS nv2
WHERE nv2.PhongBan = nv1.PhongBan -- Phụ thuộc vào dòng ngoài
);
-- Sản phẩm có giá cao hơn giá trung bình của danh mục
SELECT TenSP, DanhMuc, Gia
FROM SanPham AS sp1
WHERE Gia > (
SELECT AVG(Gia)
FROM SanPham AS sp2
WHERE sp2.DanhMuc = sp1.DanhMuc
);
:::warning Hiệu năng
Correlated subquery chạy lặp lại cho mỗi dòng → có thể chậm với bảng lớn. Cân nhắc chuyển sang JOIN nếu hiệu năng là vấn đề.
:::
8. So sánh Subquery vs JOIN
Nhiều trường hợp có thể viết bằng cả Subquery lẫn JOIN:
-- CÁCH 1: Subquery
SELECT HoTen FROM KhachHang
WHERE MaKH IN (SELECT MaKH FROM DonHang);
-- CÁCH 2: JOIN (thường nhanh hơn)
SELECT DISTINCT kh.HoTen
FROM KhachHang AS kh
INNER JOIN DonHang AS dh ON kh.MaKH = dh.MaKH;
| Tiêu chí | Subquery | JOIN |
|---|---|---|
| Dễ đọc | (trực quan) | Cần quen |
| Hiệu năng | Có thể chậm hơn | Thường nhanh hơn |
| Linh hoạt | Dùng được ở nhiều vị trí | Chỉ dùng ở FROM |
| Khi nào dùng | Kiểm tra tồn tại, so sánh 1 giá trị | Kết hợp nhiều cột từ nhiều bảng |
Bài tập thực hành
Sử dụng CSDL BaiTapJoin đã tạo ở Bài 7.
Bài tập cơ bản
- Tìm nhân viên có lương cao nhất (dùng subquery).
- Tìm nhân viên có lương cao hơn trung bình.
- Liệt kê phòng ban có nhân viên (dùng
IN+ subquery). - Liệt kê nhân viên chưa tham gia dự án nào (dùng
NOT IN).
Bài tập nâng cao
- Tìm nhân viên có lương cao nhất trong phòng ban của họ (correlated subquery).
- Liệt kê dự án có nhiều thành viên nhất (subquery trong FROM).
- Mỗi nhân viên, hiển thị số dự án họ tham gia (subquery trong SELECT).
- Tìm nhân viên có lương cao hơn tất cả nhân viên phòng Nhân sự (ALL).
- Tìm phòng ban mà tất cả nhân viên đều tham gia ít nhất 1 dự án (NOT EXISTS).
- Viết lại bài 5 bằng JOIN thay vì subquery.
Quay lại: Roadmap · Bài tiếp: Bài 9 – Thiết Kế CSDL & Chuẩn Hoá