Subquery vô hướng
Bạn sẽ học được
- Subquery vô hướng là gì và khi nào nó trả về đúng một giá trị.
- Dùng subquery trong
WHEREđể so sánh với một con số tính từ dữ liệu. - Dùng subquery trong
SELECTđể thêm cột tham chiếu mà không cầnJOIN. - Viết subquery tương quan và hiểu vì sao nó chạy chậm hơn.
- Nhận ra lỗi "trả về nhiều dòng" và cách sửa.
Khái niệm cốt lõi
Subquery là một câu SELECT nằm bên trong câu SELECT khác. Subquery vô hướng (scalar subquery) là subquery trả về đúng một dòng, một cột — tức một giá trị duy nhất. Vì nó là một giá trị, bạn có thể đặt nó ở bất cứ đâu một con số hoặc chuỗi được phép xuất hiện.
Ví dụ quen thuộc nhất: tìm sản phẩm đắt hơn giá trung bình.
SELECT name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products);Đọc từ trong ra ngoài: (SELECT AVG(price) FROM products) được tính một lần, cho ra 634.444,44. Câu ngoài trở thành WHERE price > 634444.44. Kết quả ba dòng: Nồi chiên không dầu 1.690.000; Bàn phím cơ 1.250.000; Tai nghe Bluetooth 890.000.
Điều làm subquery mạnh hơn một hằng số viết tay: ngưỡng tự cập nhật. Thêm một sản phẩm mới, giá trung bình đổi, danh sách đổi theo — không phải sửa câu lệnh.
Cú pháp cần nhớ:
- Subquery luôn đặt trong ngoặc đơn.
- Trong
WHERE, subquery vô hướng đứng sau phép so sánh:=,>,<,>=,<=,<>. - Trong
SELECT, nó tạo một cột mới:SELECT name, (SELECT ...) AS cot_moi FROM .... - Không đặt
;bên trong subquery.
Subquery trong SELECT rất tiện để thêm "con số tham chiếu" mà không cần gom nhóm:
SELECT name, price,
(SELECT ROUND(AVG(price), 2) FROM products) AS gia_trung_binh,
price - (SELECT AVG(price) FROM products) AS chenh_lech
FROM products
ORDER BY chenh_lech DESC;Mỗi dòng vẫn giữ chi tiết sản phẩm, kèm mức trung bình lặp lại trên mọi dòng — dạng bảng hay dùng để hiển thị "so với mức chung".
Có hai loại subquery theo cách nó phụ thuộc vào câu ngoài:
| Loại | Đặc điểm | Ví dụ |
|---|---|---|
| Không tương quan | Chạy độc lập, tính một lần | WHERE price > (SELECT AVG(price) FROM products) |
| Tương quan | Tham chiếu cột của câu ngoài, chạy lại cho từng dòng | WHERE price > (SELECT AVG(price) FROM products p2 WHERE p2.category = p.category) |
Subquery tương quan mạnh nhưng cần cẩn thận về hiệu năng. Ví dụ tìm sản phẩm đắt hơn mức trung bình của danh mục nó thuộc về:
SELECT p.name, p.category, p.price
FROM products p
WHERE p.price > (
SELECT AVG(p2.price) FROM products p2 WHERE p2.category = p.category
);Ba dòng kết quả: Nồi chiên không dầu (1.690.000 so với trung bình Gia dụng 672.500), Bàn phím cơ (1.250.000 so với trung bình Điện tử 863.333,33), Tai nghe Bluetooth (890.000 so với cùng mức 863.333,33). Chuột không dây 450.000 thấp hơn trung bình Điện tử nên bị loại; ba sản phẩm còn lại của Gia dụng, cùng Sách và Thời trang, đều dưới trung bình danh mục.
Mỗi dòng của câu ngoài kích hoạt một lần chạy subquery. Với 9 sản phẩm thì không đáng kể; với hàng triệu dòng, optimizer thường biến đổi thành phép nối — nhưng đừng mặc định điều đó. Khi viết subquery tương quan trên bảng lớn, hãy chạy EXPLAIN ANALYZE để xem kế hoạch thật.
Subquery vô hướng còn dùng được với = khi kết quả chắc chắn một dòng, ví dụ tra cứu theo khóa:
SELECT name, price
FROM products
WHERE price = (SELECT MAX(price) FROM products);Kết quả: Nồi chiên không dầu. Cách viết này tương đương ORDER BY price DESC LIMIT 1 nhưng không phụ thuộc vào thứ tự — và nếu có hai sản phẩm cùng giá cao nhất, nó trả cả hai, trong khi LIMIT 1 chỉ trả một.
Ví dụ chạy được
So sánh doanh thu từng tháng với tổng doanh thu:
SELECT TO_CHAR(o.order_date, 'YYYY-MM') AS thang,
SUM(oi.quantity * oi.unit_price) AS doanh_thu,
ROUND(100.0 * SUM(oi.quantity * oi.unit_price)
/ (SELECT SUM(quantity * unit_price) FROM order_items), 1) AS phan_tram
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
GROUP BY 1
ORDER BY 1;Mỗi tháng kèm tỷ lệ trên tổng doanh thu 31.230.000. Tháng 4 cao nhất với 5.600.000 (17,9%); tháng 9 thấp nhất với 580.000 (1,9%). Subquery trong SELECT được tính một lần dù câu ngoài có 11 nhóm.
Tìm đơn hàng có giá trị trên mức trung bình của mọi đơn:
SELECT o.id, o.order_date,
SUM(oi.quantity * oi.unit_price) AS tong
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
GROUP BY o.id, o.order_date
HAVING SUM(oi.quantity * oi.unit_price) > (
SELECT AVG(tong) FROM (
SELECT SUM(quantity * unit_price) AS tong
FROM order_items GROUP BY order_id
) t
)
ORDER BY tong DESC;Câu này cho thấy một giới hạn: không thể viết HAVING SUM(...) > (SELECT AVG(SUM(...)) ...) — hàm tổng hợp không lồng nhau được. Phải tính trung bình ở một lớp con. Kết quả 7 đơn: 4.030.000; 3.960.000; 2.950.000; 2.580.000; 2.150.000; 2.100.000; 2.050.000. Trung bình mỗi đơn là 1.951.875.
Đếm khách có đơn hoàn tất bằng subquery nhiều dòng:
SELECT COUNT(*) AS so_khach
FROM customers
WHERE id IN (SELECT customer_id FROM orders WHERE status = 'completed');Kết quả 8. IN (subquery) là dạng subquery trả nhiều dòng một cột — khác với vô hướng, nhưng cùng họ và cùng cách đọc. Lưu ý cẩn thận với NOT IN khi cột có thể NULL (xem chương JOIN của khóa cơ bản).
Một lỗi kinh điển:
-- LỖI: subquery trả nhiều dòng
SELECT name, price
FROM products
WHERE price = (SELECT price FROM products WHERE category = 'Điện tử');PostgreSQL báo more than one row returned by a subquery used as an expression. Đây là subquery không vô hướng: ba sản phẩm Điện tử có ba giá khác nhau. Muốn so sánh với một tập, dùng IN; muốn so sánh với một giá trị đại diện, thêm hàm tổng hợp như MAX, MIN, AVG.
Thực hành
- Liệt kê sản phẩm có giá thấp hơn giá trung bình toàn cửa hàng. Kết quả 6 dòng, cao nhất là Chuột không dây 450.000.
- Với mỗi sản phẩm, hiện giá và chênh lệch so với giá trung bình của danh mục. Sản phẩm đắt nhất mỗi danh mục phải có chênh lệch dương.
- Tìm các đơn có tổng lớn hơn tổng trung bình mỗi đơn (1.951.875). Kiểm tra phải ra 7 dòng.
- Thử bỏ
AVGtrong ví dụ lỗi ở trên và quan sát thông báo. Ghi lại để nhận ra ngay khi gặp lại.
Lỗi thường gặp
| Triệu chứng | Nguyên nhân | Cách sửa |
|---|---|---|
more than one row returned by a subquery | Subquery trả nhiều dòng nhưng dùng như giá trị | Thêm MAX/MIN/AVG hoặc đổi sang IN |
| Subquery trả 0 dòng, cả câu trả rỗng | AVG trên bảng rỗng là NULL, mọi so sánh với NULL đều không đúng | Dùng COALESCE hoặc kiểm tra dữ liệu nguồn |
| Kết quả đúng nhưng rất chậm | Subquery tương quan chạy lại cho từng dòng | EXPLAIN ANALYZE, cân nhắc JOIN hoặc CTE |
| Không dùng được bí danh trong subquery | Bí danh SELECT chưa tồn tại khi subquery chạy | Bọc trong truy vấn con rồi lọc ở lớp ngoài |
| Quên ngoặc đơn | Cú pháp subquery bắt buộc có ngoặc | (SELECT ...) |
Dòng thứ hai là lỗi âm thầm: price > NULL cho ra NULL, và WHERE coi NULL là không đúng, nên cả câu trả về không dòng nào thay vì báo lỗi. Khi kết quả rỗng bất thường, hãy chạy riêng subquery để xem nó trả gì.
Tóm tắt
- Subquery vô hướng trả đúng một giá trị, dùng được trong
WHERE,SELECTvàHAVING. - Dùng nó khi ngưỡng so sánh phải tính từ chính dữ liệu, không phải hằng số.
- Subquery tương quan tham chiếu câu ngoài và chạy lại theo từng dòng — mạnh nhưng cần đo hiệu năng.
- Subquery nhiều dòng phải đi với
IN, không đi với=. NULLtừ subquery rỗng làm cả câu lệnh trả rỗng mà không báo lỗi.
Use ← → to move between lessons