Trong bài này
Cô Mai cần mời những học viên đã đăng ký ít nhất một lớp. Cô chỉ cần danh sách người, không cần lặp tên theo từng lớp. Thầy Nam lại muốn tìm người có số xu cao hơn mức trung bình hiện tại. Để lọc đúng người, ta cần kiểm tra đăng ký của từng học viên hoặc tính mức xu trung bình trước.
Truy vấn con cho phép đặt bước kiểm tra hay tính toán ấy ngay trong câu truy vấn chính.
Một truy vấn con có thể cung cấp giá trị so sánh
Truy vấn con (subquery) là câu SELECT nằm bên trong một câu lệnh khác. Bài 16 dùng nó như một bảng nguồn. Khi cần một giá trị, ta dùng truy vấn con chỉ trả một cột và một dòng. Cách dùng này gọi là truy vấn con vô hướng (scalar subquery).
Trước khi lọc người, hãy chạy riêng câu tính trung bình:
Dữ liệu đầu vào
students6 dòng| id | name | coins |
|---|---|---|
| 1 | An | 120 |
| 2 | Bình | 90 |
| 3 | Chi | 180 |
| 4 | Dung | 60 |
| 5 | Huy | 180 |
| 6 | Lan | 150 |
Câu lệnh
SELECT AVG(coins) AS average_coins
FROM students;Kết quả
| average_coins |
|---|
| 130 |
Kết quả là 130 xu. Hàm AVG gộp số dư của toàn bộ sáu học viên thành một giá trị. Vì không có GROUP BY, nó không tạo một mức trung bình riêng cho từng khối. Ta có thể dùng mức trung bình này làm ngưỡng để so với số xu của từng học viên.
Ta đặt câu vừa kiểm vào ngoặc sau dấu >:
Dữ liệu đầu vào
studentsgiữ 3/6 dòng| id | name | coins |
|---|---|---|
| 1 | An | 120 |
| 2 | Bình | 90 |
| 3 | Chi | 180 |
| 4 | Dung | 60 |
| 5 | Huy | 180 |
| 6 | Lan | 150 |
Câu lệnh
SELECT id, name, coins
FROM students
WHERE coins > (
SELECT AVG(coins)
FROM students
)
ORDER BY id;Kết quả
| id | name | coins |
|---|---|---|
| 3 | Chi | 180 |
| 5 | Huy | 180 |
| 6 | Lan | 150 |
Kết quả giữ Chi, Huy và Lan với số xu 180, 180, 150. An có 120 nên không đạt điều kiện. Truy vấn ngoài đọc từng học viên rồi so số xu của người đó với cùng một mức trung bình. Bạn không phải gõ cố định số 130 vào điều kiện.
| Phần truy vấn | Câu hỏi nó trả lời |
|---|---|
SELECT AVG(coins) bên trong | Mức trung bình chung hiện tại là bao nhiêu? |
WHERE coins > (...) bên ngoài | Người đang xét có vượt mức đó không? |
SELECT id, name, coins bên ngoài | Cần hiển thị thông tin nào của người đạt? |
Khi dữ liệu thay đổi, mức trung bình cũng thay đổi theo. Viết WHERE coins > 130 chỉ đúng với bộ mẫu hiện tại, chưa diễn tả đầy đủ yêu cầu. Đặt câu tính trung bình trong truy vấn con giúp báo cáo tự tính lại ngưỡng từ dữ liệu hiện có. Đây là khác biệt giữa một ngưỡng cố định và một ngưỡng được tính.
EXISTS hỏi có ít nhất một dòng liên quan hay không
EXISTS là phép kiểm tồn tại: nó cho kết quả đúng khi truy vấn con trả ít nhất một dòng. Phép kiểm chỉ quan tâm có dòng hay không, không quan tâm giá trị của cột được chọn. Vì vậy, cách viết quen thuộc là SELECT 1 trong truy vấn con. Số 1 không phải mã lớp cần tìm.
Ta dùng EXISTS để lấy những người có đăng ký:
Dữ liệu đầu vào
studentsgiữ 5/6 dòng| id | name |
|---|---|
| 1 | An |
| 2 | Bình |
| 3 | Chi |
| 4 | Dung |
| 5 | Huy |
| 6 | Lan |
enrollments7 dòng| student_id | class_id | status |
|---|---|---|
| 1 | 1 | active |
| 1 | 3 | active |
| 2 | 1 | active |
| 3 | 2 | active |
| 3 | 3 | active |
| 4 | 2 | paused |
| 5 | 1 | active |
Câu lệnh
SELECT s.id, s.name
FROM students AS s
WHERE EXISTS (
SELECT 1
FROM enrollments AS e
WHERE e.student_id = s.id
)
ORDER BY s.id;Kết quả
| id | name |
|---|---|
| 1 | An |
| 2 | Bình |
| 3 | Chi |
| 4 | Dung |
| 5 | Huy |
Kết quả có An, Bình, Chi, Dung và Huy. An có hai đăng ký nhưng chỉ có một dòng trong danh sách. Truy vấn ngoài vẫn đọc một dòng cho mỗi học viên.
EXISTS chỉ quyết định giữ hay loại dòng học viên. Nó không ghép thêm các đăng ký vào kết quả như phép nối.
Với câu hỏi “ai có đăng ký?”, bạn có thể đối chiếu ba cách viết sau với hiện tượng nhân dòng ở bài 16:
| Cách viết | Số dòng trong mẫu | An xuất hiện thế nào? |
|---|---|---|
JOIN theo mã học viên | 7 | Hai dòng ứng với hai đăng ký |
JOIN rồi SELECT DISTINCT s.id, s.name | 5 | Một dòng vì hai cột kết quả trùng nhau |
EXISTS kiểm đăng ký của từng người | 5 | Một dòng vì chỉ giữ hoặc loại học viên |
DISTINCT ở đây giữ cả mã và tên để hai người trùng tên vẫn là hai người khác nhau. Với EXISTS, số đăng ký không làm lặp dòng học viên.
Truy vấn con tương quan (correlated subquery) dùng giá trị từ truy vấn ngoài. Điều kiện e.student_id = s.id hỏi về đăng ký của đúng người đang xét. Với An, nó kiểm mã 1; với Lan, nó kiểm mã 6. Lan không có dòng phù hợp nên bị loại.
Đọc học viênLấy mã học viên đang xétTìm đăng kýĐối chiếu mã học viênCó ít nhất một dòngGiữ học viên một lầnKhông có dòngLoại học viên khỏi kết quả
Bạn có thể thêm điều kiện e.status = 'active' vào truy vấn con nếu chỉ cần người đang học. Khi đó Dung không được giữ vì đăng ký của Dung là paused. Bạn cần kiểm tra đúng loại đăng ký mà đề yêu cầu. Có đăng ký bất kỳ khác với có đăng ký đang học, tức trạng thái active.
Hãy quan sát danh sách sau khi bỏ điều kiện tương quan:
Dữ liệu đầu vào
studentsgiữ 6/6 dòng| id | name |
|---|---|
| 1 | An |
| 2 | Bình |
| 3 | Chi |
| 4 | Dung |
| 5 | Huy |
| 6 | Lan |
enrollments7 dòng| student_id | class_id |
|---|---|
| 1 | 1 |
| 1 | 3 |
| 2 | 1 |
| 3 | 2 |
| 3 | 3 |
| 4 | 2 |
| 5 | 1 |
Câu lệnh
SELECT s.id, s.name
FROM students AS s
WHERE EXISTS (
SELECT 1
FROM enrollments AS e
)
ORDER BY s.id;Kết quả
| id | name |
|---|---|
| 1 | An |
| 2 | Bình |
| 3 | Chi |
| 4 | Dung |
| 5 | Huy |
| 6 | Lan |
Kết quả có cả sáu người, gồm Lan chưa đăng ký. Chỉ cần bảng có một đăng ký của bất kỳ ai, truy vấn con đã trả về dòng. Điều kiện khi đó đúng với mọi học viên, kể cả người chưa đăng ký.
SQL không tự thêm liên kết dù các bảng đều có mã học viên. Hãy kiểm tra xem truy vấn con đã dùng mã học viên từ truy vấn ngoài để tìm đúng đăng ký hay chưa.
NOT EXISTS phân biệt không có điểm với không có điểm cao
NOT EXISTS đảo phép kiểm tồn tại: nó đúng khi truy vấn con không trả dòng nào. Muốn tìm học viên chưa làm bài, ta kiểm không có bất kỳ dòng điểm nào của người ấy. Điều kiện liên kết bằng mã vẫn cần giữ nguyên.
Câu dưới tìm người hoàn toàn chưa có lần chấm:
Dữ liệu đầu vào
students6 dòng| id | name |
|---|---|
| 1 | An |
| 2 | Bình |
| 3 | Chi |
| 4 | Dung |
| 5 | Huy |
| 6 | Lan |
scores7 dòng| id | student_id | score |
|---|---|---|
| 1 | 1 | 80 |
| 2 | 1 | 90 |
| 3 | 2 | 70 |
| 4 | 3 | 90 |
| 5 | 3 | 100 |
| 6 | 4 | 50 |
| 7 | 5 | 90 |
Câu lệnh
SELECT s.id, s.name
FROM students AS s
WHERE NOT EXISTS (
SELECT 1
FROM scores AS t
WHERE t.student_id = s.id
)
ORDER BY s.id;Kết quả
| id | name |
|---|---|
| 6 | Lan |
Kết quả chỉ có Lan. Dung có điểm 50 nên không được chọn, dù điểm ấy thấp. Câu hỏi đang nói chưa làm bài chứ không nói chưa đạt điểm cao. Nếu coi điểm thấp như chưa có điểm, bạn sẽ chọn cả Dung dù bạn ấy đã làm bài.
Bây giờ cô Mai cần tìm những người chưa từng đạt từ 90 điểm trở lên. Điều kiện điểm phải nằm cùng điều kiện người trong truy vấn con:
Dữ liệu đầu vào
students6 dòng| id | name |
|---|---|
| 1 | An |
| 2 | Bình |
| 3 | Chi |
| 4 | Dung |
| 5 | Huy |
| 6 | Lan |
scores7 dòng| id | student_id | score |
|---|---|---|
| 1 | 1 | 80 |
| 2 | 1 | 90 |
| 3 | 2 | 70 |
| 4 | 3 | 90 |
| 5 | 3 | 100 |
| 6 | 4 | 50 |
| 7 | 5 | 90 |
Câu lệnh
SELECT s.id, s.name
FROM students AS s
WHERE NOT EXISTS (
SELECT 1
FROM scores AS t
WHERE t.student_id = s.id AND t.score >= 90
)
ORDER BY s.id;Kết quả
| id | name |
|---|---|
| 2 | Bình |
| 4 | Dung |
| 6 | Lan |
Lần này Bình, Dung và Lan đều xuất hiện. Bình và Dung có điểm nhưng không có điểm đạt ngưỡng. Lan không có điểm nào nên cũng không có điểm đạt ngưỡng.
NOT EXISTS chỉ kiểm tra có lần chấm nào thỏa điều kiện bên trong hay không. Nó không yêu cầu học viên phải từng làm bài trước đó.
| Câu hỏi | Tập dòng cần kiểm không tồn tại |
|---|---|
| Ai chưa làm bài? | Mọi điểm của người đang xét |
Ai chưa đạt 90? | Những điểm từ 90 của người đang xét |
Ai có ít nhất một điểm từ 90? | Dùng EXISTS trên những điểm từ 90 |
Khi viết điều kiện, bạn hãy thử đọc yêu cầu thành một câu đầy đủ. “Không có lần chấm của người này đạt từ 90” bao gồm cả người chưa được chấm. “Có điểm dưới 90” lại có thể chọn An dù An đã có một điểm 90. Vì vậy, tìm người không có điểm cao và tìm người có điểm thấp có thể cho hai danh sách khác nhau.
NOT IN cần xét cả giá trị NULL trong tập
IN kiểm một giá trị có thuộc một tập giá trị hay không. NOT IN kiểm điều ngược lại, nhưng NULL có thể khiến kết luận trở thành chưa biết. Muốn thấy điều đó, ta tạo danh sách loại trừ minh họa. Bảng riêng excluded chứa mã 1 và một mã chưa xác định.
Bảng excluded chỉ là dữ liệu riêng của ví dụ. Trong bảng điểm chuẩn scores, cột student_id bắt buộc có mã học viên, không chứa NULL. Dữ liệu riêng chỉ dùng để quan sát một tập có giá trị thiếu. Câu sau thử loại người có mã thuộc danh sách đó:
Dữ liệu đầu vào
students6 dòng| id | name |
|---|---|
| 1 | An |
| 2 | Bình |
| 3 | Chi |
| 4 | Dung |
| 5 | Huy |
| 6 | Lan |
excluded2 dòng| student_id |
|---|
| 1 |
| NULL |
Câu lệnh
SELECT id, name
FROM students
WHERE id NOT IN (
SELECT student_id
FROM excluded
)
ORDER BY id;Kết quả
| id | name |
|---|---|
| Không có dòng nào | |
Kết quả rỗng. Mã 1 bị loại vì đã có trong danh sách. Với các mã khác, phép so sánh với NULL cho kết quả chưa biết, nên điều kiện không thể là đúng.
WHERE chỉ giữ điều kiện đúng, nên những dòng ấy cũng không qua. Bạn không thể coi NULL như một mã chắc chắn khác mọi người.
Ta viết lại điều kiện: chỉ giữ học viên khi không có dòng nào trong danh sách loại trừ khớp mã của người đó:
Dữ liệu đầu vào
students6 dòng| id | name |
|---|---|
| 1 | An |
| 2 | Bình |
| 3 | Chi |
| 4 | Dung |
| 5 | Huy |
| 6 | Lan |
excluded2 dòng| student_id |
|---|
| 1 |
| NULL |
Câu lệnh
SELECT s.id, s.name
FROM students AS s
WHERE NOT EXISTS (
SELECT 1
FROM excluded AS x
WHERE x.student_id = s.id
)
ORDER BY s.id;Kết quả
| id | name |
|---|---|
| 2 | Bình |
| 3 | Chi |
| 4 | Dung |
| 5 | Huy |
| 6 | Lan |
Kết quả giữ Bình, Chi, Dung, Huy và Lan. Dòng NULL trong excluded không khớp một mã người cụ thể. Trong cách viết này, bạn kiểm tra trực tiếp mã nào khớp với học viên đang xét. Nó giúp yêu cầu dễ đọc hơn, nhưng không có nghĩa lúc nào cũng chạy nhanh hơn cách viết khác.
Lỗi thường gặp
| Lỗi | Vì sao sai | Cách sửa |
|---|---|---|
| Gõ ngưỡng trung bình mẫu cố định | Không theo dữ liệu mới | Tính AVG bằng truy vấn con |
Bỏ điều kiện t.student_id = s.id | Kiểm toàn bảng thay vì từng người | Giữ điều kiện tương quan |
Hiểu SELECT 1 là chỉ chọn mã 1 | EXISTS kiểm dòng, không kiểm giá trị chọn | Đặt mã cần tìm trong WHERE |
Dùng NOT IN mà bỏ qua NULL | Điều kiện có thể chưa biết | Xét NULL hoặc dùng NOT EXISTS theo quan hệ |
Tóm tắt
- Truy vấn con vô hướng cung cấp một giá trị cho phép so sánh.
EXISTSgiữ học viên khi có ít nhất một dòng phù hợp. Mỗi học viên vẫn chỉ xuất hiện một lần, dù có nhiều đăng ký.- Điều kiện tương quan nối câu hỏi bên trong với dòng đang xét bên ngoài.
NOT EXISTSkiểm không có dòng thỏa toàn bộ điều kiện bên trong.- Nếu danh sách dùng cho
NOT INcóNULL, điều kiện có thể trả về kết quả chưa biết và loại cả những người bạn muốn giữ.