Làm việc với subquery giống như chơi cờ vua kiểu "lột đồ": nhìn thì tưởng dễ, nhưng chỉ cần đi sai một nước là toang liền. Vậy lỗi từ đâu ra? Chủ yếu là do không hiểu cú pháp, bỏ qua mấy đặc thù logic của SQL hoặc đơn giản là không để ý. Trong bài này tụi mình sẽ nói về những lỗi hay gặp nhất và cách để né chúng.
Lỗi cú pháp
Subquery đòi hỏi phải cực kỳ cẩn thận với cú pháp. Quên dấu phẩy, ngoặc hoặc alias là query của bạn có thể sập luôn. Cùng xem mấy lỗi điển hình nhé.
Thiếu ngoặc
Ngoặc là thứ cực kỳ quan trọng khi dùng subquery. Subquery phải được đặt trong ngoặc tròn, thiếu một cặp thôi là SQL báo lỗi cú pháp liền.
Ví dụ lỗi:
SELECT student_name
FROM students
WHERE student_id IN SELECT student_id FROM enrollments);
Lỗi:
ERROR: syntax error at or near "SELECT"
Sửa đúng:
SELECT student_name
FROM students
WHERE student_id IN (SELECT student_id FROM enrollments);
Chú thích: Query trong IN luôn phải nằm trong ngoặc để SQL hiểu là bạn muốn chạy subquery.
Thiếu alias
Khi bạn dùng subquery trong phần FROM, nhớ đặt alias cho nó nhé. Không có alias là PostgreSQL sẽ bị lú luôn.
Ví dụ lỗi:
SELECT student_name, avg_score
FROM (SELECT student_id, AVG(score) AS avg_score FROM grades GROUP BY student_id)
WHERE avg_score > 80;
Lỗi:
ERROR: subquery in FROM must have an alias
Sửa đúng:
SELECT student_name, avg_score
FROM (SELECT student_id, AVG(score) AS avg_score FROM grades GROUP BY student_id) AS subquery
WHERE avg_score > 80;
Chú thích: PostgreSQL bắt buộc mỗi bảng tạm (kết quả subquery trong FROM) phải có tên.
Vấn đề hiệu năng
Subquery mà tối ưu kém thì database của bạn sẽ chạy chậm như rùa luôn. Hiệu năng thường tệ do tính toán dư thừa hoặc thiếu index.
Tính toán dư thừa
Subquery trong phần SELECT có thể bị tính lại cho từng dòng kết quả, dẫn đến tốn thời gian kinh khủng.
Ví dụ:
SELECT student_name,
(SELECT COUNT(*) FROM enrollments WHERE enrollments.student_id = students.student_id) AS course_count
FROM students;
Nếu bảng students có hàng chục ngàn dòng, subquery này sẽ chạy lại cho từng dòng luôn đó.
Tối ưu:
WITH course_counts AS (
SELECT student_id, COUNT(*) AS course_count
FROM enrollments
GROUP BY student_id
)
SELECT s.student_name, c.course_count
FROM students s
LEFT JOIN course_counts c ON s.student_id = c.student_id;
CTE (Common Table Expression) hoặc join giúp bạn tránh phải tính lại cho từng dòng. Cách này tụi mình sẽ học kỹ hơn ở mấy level sau :P
Thiếu index
Nếu bạn dùng subquery phức tạp trong phần WHERE, nhớ kiểm tra xem cột cần thiết đã có index chưa nhé.
Ví dụ:
SELECT student_name
FROM students
WHERE student_id IN (SELECT student_id FROM enrollments WHERE course_id = 10);
Nếu cột student_id trong bảng enrollments chưa có index, subquery sẽ phải quét toàn bộ bảng luôn.
Tối ưu: Tạo index:
CREATE INDEX idx_enrollments_course_id ON enrollments (course_id);
Bạn nghe nói về index hoài chắc cũng tò mò nó là gì rồi nhỉ. Nhưng đợi mấy level nữa sẽ rõ. Index là thứ quan trọng, giúp tăng tốc query mà không cần sửa code. Nhưng index không biến query dở thành hay đâu, chỉ giúp bạn tăng tốc trên production thôi, nhất là khi bảng có cả triệu dòng.
Lỗi logic
Lỗi logic trong subquery cũng hay gặp không kém gì lỗi cú pháp. Vấn đề thường do không hiểu rõ về NULL, filter hoặc hàm tổng hợp.
Dùng NULL sai cách
NULL là cái bẫy cho newbie luôn. Khi dùng IN hoặc NOT IN trong subquery, nếu có NULL là kết quả sẽ bị ảnh hưởng.
Ví dụ lỗi:
SELECT student_name
FROM students
WHERE student_id NOT IN (SELECT student_id FROM enrollments);
Nếu trong bảng enrollments có dòng nào student_id = NULL, query này sẽ trả về rỗng luôn. Vì điều kiện NOT IN sẽ thành NULL IS NOT IN.
Sửa đúng:
SELECT student_name
FROM students
WHERE student_id NOT IN (SELECT student_id FROM enrollments WHERE student_id IS NOT NULL);
Luôn filter NULL nếu dùng NOT IN nhé.
Lỗi ở điều kiện filter
Subquery thường dùng để filter phức tạp, nhưng nếu sai điều kiện thì kết quả sẽ khác hẳn mong đợi luôn.
Ví dụ lỗi:
SELECT student_name
FROM students
WHERE (SELECT AVG(score) FROM grades WHERE grades.student_id = students.student_id) > 80;
Nếu có bạn nào không có điểm, subquery sẽ trả về NULL và bạn đó sẽ không được trả về kết quả.
Sửa đúng:
SELECT student_name
FROM students
WHERE COALESCE((SELECT AVG(score) FROM grades WHERE grades.student_id = students.student_id), 0) > 80;
Dùng COALESCE để thay NULL bằng giá trị mặc định nhé.
Tips để tránh lỗi
Để né mấy lỗi thường gặp khi làm việc với subquery, nhớ mấy điều sau:
Dùng ngoặc và alias cho chuẩn. Nếu có gì không chạy, kiểm tra lại xem đã đóng đủ ngoặc và đặt alias cho subquery chưa.
Tối ưu query. Cố gắng hạn chế dùng subquery nếu có thể dùng JOIN, WITH hoặc index.
Chú ý NULL. Luôn nghĩ tới khả năng có NULL trong subquery và dùng IS NOT NULL, COALESCE hoặc mấy thứ tương tự.
Test kỹ. Test từng subquery riêng để chắc chắn nó trả về đúng kết quả.
Dễ đọc. Dùng indent và alias cho code dễ đọc. Nhớ là một tháng sau quay lại bạn cũng chẳng nhớ mình viết gì đâu :D
GO TO FULL VERSION