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

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ạiKế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ộtIN, 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

INEXISTS
Cách hoạt độngSo 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 đươngTương đương
Hiệu năng (bảng lớn)Chậm hơnNhanh hơn
Xử lý NULLCó 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ĩaTương đương
> ANYLớn hơn ít nhất 1 giá trị> MIN(...)
> ALLLớn hơn tất cả giá trị> MAX(...)
< ANYNhỏ hơn ít nhất 1 giá trị< MAX(...)
< ALLNhỏ hơn tất cả giá trị< MIN(...)
= ANYBằ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íSubqueryJOIN
Dễ đọc(trực quan)Cần quen
Hiệu năngCó thể chậm hơnThường nhanh hơn
Linh hoạtDùng được ở nhiều vị tríChỉ dùng ở FROM
Khi nào dùngKiể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

  1. Tìm nhân viên có lương cao nhất (dùng subquery).
  2. Tìm nhân viên có lương cao hơn trung bình.
  3. Liệt kê phòng ban có nhân viên (dùng IN + subquery).
  4. 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

  1. Tìm nhân viên có lương cao nhất trong phòng ban của họ (correlated subquery).
  2. Liệt kê dự án có nhiều thành viên nhất (subquery trong FROM).
  3. Mỗi nhân viên, hiển thị số dự án họ tham gia (subquery trong SELECT).
  4. 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).
  5. Tìm phòng ban mà tất cả nhân viên đều tham gia ít nhất 1 dự án (NOT EXISTS).
  6. 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á