Chúc mừng nha, tụi mình đã đến đoạn bắt đầu thú vị thật sự rồi! Hôm nay tụi mình sẽ xem cách kết hợp nhiều loại subquery khác nhau để giải quyết các bài toán khó nhằn. EXISTS, IN, HAVING — bộ ba này sẽ giúp bạn cảm thấy mình như phù thủy database luôn đó. Tụi mình sẽ lấy dữ liệu từ một bảng, lọc theo dữ liệu bảng khác, nhóm lại, rồi lại lọc tiếp trên nhóm. Và bonus — sẽ bàn luôn mấy mẹo giúp truy vấn chạy nhanh hơn.
Bắt đầu bằng việc đặt ra một bài toán chung, tụi mình sẽ giải từng bước xuyên suốt bài giảng này nha.
Đặt bài toán
Giả sử tụi mình có database trường đại học với ba bảng:
Bảng students
| id | name | group_id |
|---|---|---|
| 1 | Otto | 101 |
| 2 | Maria | 101 |
| 3 | Alex | 102 |
| 4 | Anna | 103 |
Bảng courses
| id | name |
|---|---|
| 1 | Toán học |
| 2 | Lập trình |
| 3 | Triết học |
Bảng enrollments
| student_id | course_id | grade |
|---|---|---|
| 1 | 1 | 90 |
| 1 | 2 | NULL |
| 2 | 1 | 85 |
| 3 | 3 | 70 |
Cần chọn tất cả sinh viên mà:
- Đã đăng ký ít nhất một môn học
EXISTS. - Chưa có điểm ở ít nhất một môn đã đăng ký
IN. - Thuộc nhóm mà điểm trung bình lớn hơn 80
HAVING.
Giải bằng EXISTS và IN
Bước 1: Kiểm tra sinh viên đã đăng ký (EXISTS). Bắt đầu với điều kiện đơn giản nhất. Tụi mình cần biết ai trong số sinh viên đã đăng ký ít nhất một môn. Dùng EXISTS là chuẩn bài.
SELECT name
FROM students s
WHERE EXISTS (
SELECT 1
FROM enrollments e
WHERE e.student_id = s.id
);
- Query ngoài chọn tên từ bảng
students. - Trong subquery, tụi mình kiểm tra có bản ghi nào trong bảng
enrollmentskhớp với sinh viên ngoài không (WHERE e.student_id = s.id). SELECT 1chỉ để báo là có bản ghi là được, không quan tâm nội dung.
Kết quả:
| name |
|---|
| Otto |
| Maria |
| Alex |
Giờ tụi mình đã biết ai đăng ký môn học rồi. Nhưng muốn nhiều hơn nữa. Muốn lọc theo việc chưa có điểm.
Bước 2: Kiểm tra chưa có điểm (IN + NULL). Giờ thêm điều kiện: chỉ lấy sinh viên mà có ít nhất một môn chưa có điểm. Ở đây IN và kiến thức về NULL sẽ giúp tụi mình.
SELECT name
FROM students s
WHERE id IN (
SELECT e.student_id
FROM enrollments e
WHERE e.grade IS NULL
);
- Query ngoài chọn tên sinh viên.
- Subquery tạo list
student_idtừ bảngenrollmentsnơigrade IS NULL.
Kết quả:
| name |
|---|
| Otto |
Vậy là Otto — sinh viên duy nhất có môn chưa có điểm. Hơi drama nhỉ! Nhưng chưa xong đâu: chỉ lấy nhóm mà điểm trung bình lớn hơn 80 thôi.
Giải bằng HAVING
Bước 3: Nhóm và lọc với HAVING.
Giờ là lúc kết hợp tất cả lại. Tụi mình cần:
- Tính điểm trung bình cho từng nhóm.
- Lọc nhóm có điểm trung bình lớn hơn 80.
- In ra sinh viên thuộc các nhóm này, kèm điều kiện trước đó.
SELECT name
FROM students s
WHERE s.group_id IN (
SELECT group_id
FROM students
JOIN enrollments ON students.id = enrollments.student_id
WHERE grade IS NOT NULL
GROUP BY group_id
HAVING AVG(grade) > 80
)
AND id IN (
SELECT e.student_id
FROM enrollments e
WHERE e.grade IS NULL
);
- Query ngoài chọn tên sinh viên thỏa mãn tất cả điều kiện.
- Subquery đầu trong
WHEREtrả về listgroup_idcho nhóm có điểm trung bình lớn hơn 80.- Tụi mình join
studentsvớienrollmentsđể lấy điểm. - Lọc bản ghi có
grade IS NOT NULL. - Group theo
group_id. - Dùng
HAVINGđể lọc nhóm.
- Tụi mình join
- Subquery thứ hai trong
WHEREkiểm tra sinh viên có ít nhất một môn chưa có điểm. - Hai điều kiện nối với nhau bằng
AND.
Kết quả:
| name |
|---|
| Otto |
Vậy là tụi mình phát hiện Otto — không chỉ là sinh viên duy nhất chưa có điểm, mà còn thuộc nhóm học bá luôn.
So sánh: EXISTS vs IN
EXISTS mạnh nhất khi bạn chỉ muốn kiểm tra có tồn tại bản ghi không. Nó hiệu quả vì dừng ngay khi tìm thấy bản ghi đầu tiên. Đặc biệt quan trọng với bảng lớn.
Trong khi đó IN hữu ích khi bạn cần tập trung vào nội dung dữ liệu. Ví dụ, khi lấy list id để lọc tiếp. Nhưng nhớ là IN có thể chậm nếu subquery trả về nhiều giá trị.
Khi nào dùng HAVING
Với dữ liệu đã tổng hợp, cần lọc theo kết quả tính toán thì HAVING là lựa chọn số 1. Nhưng nếu có thể chuyển điều kiện vào WHERE (ví dụ lọc theo cột), thì query sẽ đơn giản và chạy nhanh hơn.
Ví dụ đầy đủ
Để chắc kèo, tụi mình làm thêm ví dụ nữa: chọn nhóm mà có ít nhất một sinh viên điểm dưới 75, nhưng không đăng ký môn "Triết học".
Nhắc lại, các bảng của tụi mình:
Bảng students
| id | name | group_id |
|---|---|---|
| 1 | Otto | 101 |
| 2 | Maria | 101 |
| 3 | Alex | 102 |
| 4 | Anna | 103 |
Bảng courses
| id | name |
|---|---|
| 1 | Toán học |
| 2 | Lập trình |
| 3 | Triết học |
Bảng enrollments
| student_id | course_id | grade |
|---|---|---|
| 1 | 1 | 90 |
| 1 | 2 | NULL |
| 2 | 1 | 85 |
| 3 | 3 | 70 |
SELECT DISTINCT group_id
FROM students s
WHERE group_id IN (
SELECT s.group_id
FROM students s
JOIN enrollments e ON s.id = e.student_id
WHERE e.grade < 75
)
AND group_id NOT IN (
SELECT s.group_id -- subquery cấp 1
FROM students s
JOIN enrollments e ON s.id = e.student_id
WHERE e.course_id = (
SELECT id FROM courses WHERE name = 'Triết học' -- subquery cấp 2 :P
)
);
- Subquery đầu chọn nhóm có sinh viên điểm dưới 75.
- Subquery thứ hai loại nhóm liên quan đến môn "Triết học".
- Kết hợp điều kiện qua
INvàNOT INđể ra kết quả cuối.
Kết quả:
| group_id |
|---|
| 101 |
Cái này hữu ích cỡ nào?
Trong thực tế, mấy cách này cứu cánh khi phải phân tích mối quan hệ dữ liệu phức tạp. Ví dụ:
- Trong phân tích để tìm nhóm khách hàng "đặc biệt" (VIP, vấn đề, v.v.).
- Khi làm hệ thống gợi ý, cần lọc user theo nhiều tiêu chí.
- Khi phỏng vấn, bị hỏi tối ưu hóa truy vấn SQL phức tạp.
Luyện tập đi nhé! Đó là con đường lên trình của bạn đó.
GO TO FULL VERSION