[SQL] Subquery trong SQL

Nói một cách đơn giản, Subquery là một câu truy vấn SQL được đặt bên trong một câu truy vấn SQL khác (thường gọi là truy vấn cha hoặc truy vấn ngoài). Kết quả của Subquery sẽ được sử dụng bởi truy vấn cha để hoàn thành nhiệm vụ của nó như: lọc dữ liệu, so sánh giá trị...).
Tại sao lại dùng Subquery?
Chia nhỏ vấn đề: Giúp giải quyết các bài toán phức tạp bằng cách chia thành các bước nhỏ hơn, dễ quản lý hơn.
Lấy dữ liệu động: Cung cấp các giá trị hoặc tập hợp giá trị mà truy vấn cha cần, những giá trị này có thể không biết trước hoặc thay đổi theo thời gian.
Để dễ hình dung và có ví dụ trực quan, chúng ta sẽ sử dụng 2 bảng dữ liệu giả định sau đây trong suốt bài viết:
NhanVien
(Thông tin nhân viên)
MaNV | TenNV | Luong | MaPB |
NV01 | Nguyễn Văn A | 60000 | PB01 |
NV02 | Trần Thị B | 75000 | PB01 |
NV03 | Lê Văn C | 50000 | PB02 |
NV04 | Phạm Thị D | 80000 | PB02 |
NV05 | Hoàng Văn E | 90000 | PB03 |
PhongBan
(Thông tin phòng ban)
MaPB | TenPB |
PB01 | Kinh Doanh |
PB02 | Kỹ Thuật |
PB03 | Nhân Sự |
Bây giờ, chúng ta cùng đi vào chi tiết từng loại Subquery nhé!
1. Các Loại Subquery
Subquery có thể được phân loại dựa vào cái gì nó trả về.
a. Scalar Subquery (Truy vấn con vô hướng)
Đặc điểm: Chỉ trả về một giá trị duy nhất (một hàng, một cột).
Sử dụng: Thường được dùng trong mệnh đề
WHERE
để so sánh, hoặc trong danh sáchSELECT
để lấy một giá trị cụ thể.Ví dụ: Tìm những nhân viên có mức lương cao nhất công ty.
Bước 1 (Subquery): Tìm mức lương cao nhất là bao nhiêu?
SELECT MAX(Luong) FROM NhanVien
→ Kết quả:
90000
Bước 2 (Truy vấn cha): Tìm nhân viên có lương bằng
90000
.SELECT MaNV, TenNV, Luong FROM NhanVien WHERE Luong = (SELECT MAX(Luong) FROM NhanVien);
→ Kết quả:
MaNV
TenNV
Luong
NV05
Hoàng Văn E
90000
b. Column Subquery (Truy vấn con cột)
Đặc điểm: Trả về một cột chứa nhiều hàng giá trị.
Sử dụng: Thường đi kèm với các toán tử như
IN
,NOT IN
,ANY
,ALL
trong mệnh đềWHERE
.Ví dụ: Tìm thông tin các phòng ban có nhân viên đang làm việc.
Bước 1 (Subquery): Lấy danh sách mã phòng ban (
MaPB
) từ bảngNhanVien
(những phòng ban có người làm).SELECT DISTINCT MaPB FROM NhanVien WHERE MaPB IS NOT NULL
→ Kết quả trả về là
PB01
,PB02
,PB03
Bước 2 (Truy vấn cha): Tìm phòng ban có
MaPB
nằm trong danh sách vừa lấy được.SELECT MaPB, TenPB FROM PhongBan WHERE MaPB IN (SELECT DISTINCT MaPB FROM NhanVien WHERE MaPB IS NOT NULL);
→ Kết quả:
MaPB
TenPB
PB01
Kinh Doanh
PB02
Kỹ Thuật
PB03
Nhân Sự
c. Row Subquery (Truy vấn con hàng)
Đặc điểm: Trả về một hàng duy nhất chứa nhiều cột giá trị.
Sử dụng: Thường dùng để so sánh nhiều cột cùng lúc trong mệnh đề
WHERE
.Ví dụ: Tìm nhân viên có cùng
MaPB
vàLuong
với nhân viênNV01
.Bước 1 (Subquery): Lấy
MaPB
vàLuong
củaNV01
.SELECT MaPB, Luong FROM NhanVien WHERE MaNV = 'NV01'
→ Kết quả trả về là một hàng:
('PB01', 60000)
Bước 2 (Truy vấn cha): Tìm các nhân viên khác
NV01
mà cóMaPB
vàLuong
khớp với hàng kết quả từ Subquery.SELECT MaNV, TenNV FROM NhanVien WHERE (MaPB, Luong) = (SELECT MaPB, Luong FROM NhanVien WHERE MaNV = 'NV01') AND MaNV <> 'NV01'; -- Loại trừ chính NV01
Kết quả: Nếu có người khác cùng phòng, cùng lương, họ sẽ hiện ra.
d. Table Subquery (Truy vấn con bảng)
Đặc điểm: Trả về nhiều hàng và nhiều cột, giống như một bảng ảo.
Sử dụng: Thường được đặt trong mệnh đề
FROM
hoặcJOIN
. Khi đặt trongFROM
, nó còn được gọi là Derived Table (Bảng dẫn xuất) hoặc Inline View. Bạn phải đặt tên (alias) cho Table Subquery này.Ví dụ: Tìm tên nhân viên và tên phòng ban của họ, nhưng chỉ lấy những phòng ban có lương trung bình trên 65000.
Bước 1 (Subquery - Table Subquery): Tính lương trung bình cho mỗi phòng ban và chỉ giữ lại những phòng có lương trung bình > 65000.
SELECT MaPB, AVG(Luong) AS LuongTB FROM NhanVien GROUP BY MaPB HAVING AVG(Luong) > 65000
→ Kết quả trả về là:
MaPB
LuongTB
PB01
67500
PB02
65000
PB03
90000
Bước 2 (Truy vấn cha): Kết nối (JOIN) bảng
NhanVien
với kết quả của Subquery (đặt tên làPhongBanLuongCao
) dựa trênMaPB
.SELECT NV.TenNV, PB.TenPB FROM NhanVien AS NV INNER JOIN PhongBan AS PB ON NV.MaPB = PB.MaPB INNER JOIN ( -- Đây là Table Subquery SELECT MaPB FROM NhanVien GROUP BY MaPB HAVING AVG(Luong) > 65000 ) AS PhongBanLuongCao ON NV.MaPB = PhongBanLuongCao.MaPB;
(Cách khác, JOIN với bảng PhongBan trước rồi mới JOIN với subquery)
SELECT NV.TenNV, PB.TenPB FROM NhanVien AS NV INNER JOIN ( -- Table Subquery tính lương TB và lọc SELECT MaPB FROM NhanVien GROUP BY MaPB HAVING AVG(Luong) > 65000 ) AS PB_Filtered ON NV.MaPB = PB_Filtered.MaPB INNER JOIN PhongBan AS PB ON NV.MaPB = PB.MaPB;
→ Kết quả:
TenNV
TenPB
Nguyễn Văn A
Kinh Doanh
Trần Thị B
Kinh Doanh
Hoàng Văn E
Nhân Sự
2. Nested Subqueries (Truy vấn con lồng nhau)
Định nghĩa: Đơn giản là một Subquery nằm bên trong một Subquery khác. Bạn có thể lồng nhiều cấp.
Cách hoạt động: Truy vấn con ở cấp sâu nhất sẽ được thực thi trước, kết quả của nó được dùng cho truy vấn con ở cấp ngoài liền kề, cứ thế cho đến truy vấn cha ngoài cùng.
Ví dụ: Tìm nhân viên có lương cao hơn lương trung bình của tất cả nhân viên trong phòng ban "Kinh Doanh".
Subquery cấp 2 (trong cùng): Tìm
MaPB
của phòng 'Kinh Doanh'.SELECT MaPB FROM PhongBan WHERE TenPB = 'Kinh Doanh'
→ Kết quả:
PB01
Subquery cấp 1: Tính lương trung bình của các nhân viên có
MaPB
=PB01
.SELECT AVG(Luong) FROM NhanVien WHERE MaPB = (SELECT MaPB FROM PhongBan WHERE TenPB = 'Kinh Doanh')
→ Kết quả:
(60000 + 75000) / 2 = 6750)
Truy vấn cha: Tìm nhân viên có lương >
67500
.SELECT TenNV, Luong FROM NhanVien WHERE Luong > ( SELECT AVG(Luong) FROM NhanVien WHERE MaPB = (SELECT MaPB FROM PhongBan WHERE TenPB = 'Kinh Doanh') );
→ Kết quả:
TenNV
Luong
Trần Thị B
75000
Phạm Thị D
80000
Hoàng Văn E
90000
3. Correlated Subqueries (Truy vấn con tương quan)
Định nghĩa: Đây là loại Subquery "đặc biệt". Không giống như các loại trên (thường chỉ chạy 1 lần), Correlated Subquery sẽ chạy lặp đi lặp lại, một lần cho mỗi hàng được xử lý bởi truy vấn cha. Lý do là vì Subquery này tham chiếu đến một (hoặc nhiều) cột từ truy vấn cha.
Cách hoạt động:
Truy vấn cha lấy một hàng để xử lý (ví dụ: hàng của NV01).
Giá trị từ hàng đó (ví dụ:
MaPB = 'PB01'
) được "truyền vào" cho Subquery tương quan.Subquery tương quan thực thi với giá trị đó (tính lương trung bình của PB01).
Kết quả của Subquery được trả về cho truy vấn cha để hoàn thành điều kiện
WHERE
cho hàng NV01.Lặp lại quy trình cho hàng tiếp theo của truy vấn cha (NV02, NV03...).
Ví dụ: Tìm những nhân viên có mức lương cao hơn lương trung bình của chính phòng ban mà họ đang làm việc. Để làm điều này, với mỗi nhân viên (mỗi hàng của truy vấn cha), bạn phải chạy lại phép tính lương trung bình chỉ riêng cho phòng ban của nhân viên đó.
SELECT NV1.MaNV, NV1.TenNV, NV1.Luong, NV1.MaPB FROM NhanVien AS NV1 -- Đặt alias NV1 cho bảng ở truy vấn cha WHERE NV1.Luong > ( -- Bắt đầu Correlated Subquery SELECT AVG(NV2.Luong) FROM NhanVien AS NV2 -- Đặt alias NV2 cho bảng ở subquery WHERE NV2.MaPB = NV1.MaPB -- Đây là điểm TƯƠNG QUAN: Subquery dùng MaPB từ truy vấn cha (NV1) -- Kết thúc Correlated Subquery );
→ Kết quả cuối cùng:
MaNV
TenNV
Luong
MaPB
NV02
Trần Thị B
75000
PB01
NV04
Phạm Thị D
80000
PB02
Giải thích ví dụ:
Khi truy vấn cha xử lý
NV01
(MaPB = 'PB01'
,Luong = 60000
): Subquery sẽ chạySELECT AVG(Luong) FROM NhanVien WHERE MaPB = 'PB01'
, kết quả là67500
. Điều kiện60000 > 67500
là SAI -> NV01 không được chọn.Khi truy vấn cha xử lý
NV02
(MaPB = 'PB01'
,Luong = 75000
): Subquery chạy lạiSELECT AVG(Luong) FROM NhanVien WHERE MaPB = 'PB01'
, kết quả là67500
. Điều kiện75000 > 67500
là ĐÚNG -> NV02 được chọn.Khi truy vấn cha xử lý
NV03
(MaPB = 'PB02'
,Luong = 50000
): Subquery chạySELECT AVG(Luong) FROM NhanVien WHERE MaPB = 'PB02'
, kết quả là(50000 + 80000) / 2 = 65000
. Điều kiện50000 > 65000
là SAI -> NV03 không được chọn.Khi truy vấn cha xử lý
NV04
(MaPB = 'PB02'
,Luong = 80000
): Subquery chạy lạiSELECT AVG(Luong) FROM NhanVien WHERE MaPB = 'PB02'
, kết quả là65000
. Điều kiện80000 > 65000
là ĐÚNG -> NV04 được chọn.Khi truy vấn cha xử lý
NV05
(MaPB = 'PB03'
,Luong = 90000
): Subquery chạySELECT AVG(Luong) FROM NhanVien WHERE MaPB = 'PB03'
, kết quả là90000
. Điều kiện90000 > 90000
là SAI -> NV05 không được chọn.
Toán tử
EXISTS
vàNOT EXISTS
: Rất thường được dùng với Correlated Subqueries để kiểm tra xem có tồn tại hàng nào trong Subquery thỏa mãn điều kiện liên quan đến hàng hiện tại của truy vấn cha hay không.Ví dụ: Tìm các phòng ban có ít nhất một nhân viên.
SELECT PB.MaPB, PB.TenPB FROM PhongBan AS PB WHERE EXISTS ( -- Kiểm tra xem có tồn tại... SELECT 1 -- Chỉ cần chọn giá trị bất kỳ để kiểm tra tồn tại FROM NhanVien AS NV WHERE NV.MaPB = PB.MaPB -- ...nhân viên nào thuộc phòng ban này không );
Tổng kết
Subquery là truy vấn con nằm trong truy vấn cha, giúp giải quyết bài toán phức tạp.
Phân loại theo kết quả trả về: Scalar (1 giá trị), Column (1 cột, nhiều hàng), Row (1 hàng, nhiều cột), Table (nhiều hàng, nhiều cột).
Nested Subquery: Subquery lồng trong subquery khác, thực thi từ trong ra ngoài.
Correlated Subquery: Subquery phụ thuộc và chạy lặp lại cho mỗi hàng của truy vấn cha, thường dùng
EXISTS
hoặc so sánh với giá trị từ truy vấn cha.Luôn cân nhắc:
Tính dễ đọc: Subquery có làm câu lệnh dễ hiểu hơn không?
Hiệu năng: Liệu có cách khác (như
JOIN
) hiệu quả hơn không? Đặc biệt cẩn thận với Correlated Subquery trên bảng lớn.Đơn giản hóa: Chia nhỏ truy vấn phức tạp thành các bước hoặc sử dụng CTE (Common Table Expressions) khi bạn học nâng cao hơn.
Subscribe to my newsletter
Read articles from Ha Ngoc Hieu directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
