DHDataHọc
TheoryLesson 1/236 min read+10 XP

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ần JOIN.
  • 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.

code
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:

code
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ểmVí dụ
Không tương quanChạy độc lập, tính một lầnWHERE price > (SELECT AVG(price) FROM products)
Tương quanTham chiếu cột của câu ngoài, chạy lại cho từng dòngWHERE 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ề:

code
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:

code
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:

code
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:

code
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:

code
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:

code
-- 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

  1. 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.
  2. 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.
  3. 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.
  4. Thử bỏ AVG trong 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ứngNguyên nhânCách sửa
more than one row returned by a subquerySubquery 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ỗngAVG trên bảng rỗng là NULL, mọi so sánh với NULL đều không đúngDùng COALESCE hoặc kiểm tra dữ liệu nguồn
Kết quả đúng nhưng rất chậmSubquery tương quan chạy lại cho từng dòngEXPLAIN ANALYZE, cân nhắc JOIN hoặc CTE
Không dùng được bí danh trong subqueryBí danh SELECT chưa tồn tại khi subquery chạyBọc trong truy vấn con rồi lọc ở lớp ngoài
Quên ngoặc đơnCú 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, SELECT và 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 =.
  • NULL từ 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