[SQL] Subquery trong SQL

Ha Ngoc HieuHa Ngoc Hieu
10 min read

Nói một cách đơn giản, Subquery là một câu truy vấn SQL được đặt bên trong một câu truy vấn SQL khác (thường gọi là truy vấn cha hoặc truy vấn ngoài). Kết quả của Subquery sẽ được sử dụng bởi truy vấn cha để hoàn thành nhiệm vụ của nó như: lọc dữ liệu, so sánh giá trị...).

Tại sao lại dùng Subquery?

  • Chia nhỏ vấn đề: Giúp giải quyết các bài toán phức tạp bằng cách chia thành các bước nhỏ hơn, dễ quản lý hơn.

  • Lấy dữ liệu động: Cung cấp các giá trị hoặc tập hợp giá trị mà truy vấn cha cần, những giá trị này có thể không biết trước hoặc thay đổi theo thời gian.

Để dễ hình dung và có ví dụ trực quan, chúng ta sẽ sử dụng 2 bảng dữ liệu giả định sau đây trong suốt bài viết:

  1. NhanVien (Thông tin nhân viên)
MaNVTenNVLuongMaPB
NV01Nguyễn Văn A60000PB01
NV02Trần Thị B75000PB01
NV03Lê Văn C50000PB02
NV04Phạm Thị D80000PB02
NV05Hoàng Văn E90000PB03
  1. PhongBan (Thông tin phòng ban)
MaPBTenPB
PB01Kinh Doanh
PB02Kỹ Thuật
PB03Nhân Sự

Bây giờ, chúng ta cùng đi vào chi tiết từng loại Subquery nhé!


1. Các Loại Subquery

Subquery có thể được phân loại dựa vào cái gì nó trả về.

a. Scalar Subquery (Truy vấn con vô hướng)

  • Đặc điểm: Chỉ trả về một giá trị duy nhất (một hàng, một cột).

  • Sử dụng: Thường được dùng trong mệnh đề WHERE để so sánh, hoặc trong danh sách SELECT để lấy một giá trị cụ thể.

  • Ví dụ: Tìm những nhân viên có mức lương cao nhất công ty.

    • Bước 1 (Subquery): Tìm mức lương cao nhất là bao nhiêu?

        SELECT MAX(Luong) FROM NhanVien
      

      → Kết quả: 90000

    • Bước 2 (Truy vấn cha): Tìm nhân viên có lương bằng 90000.

        SELECT MaNV, TenNV, Luong
        FROM NhanVien
        WHERE Luong = (SELECT MAX(Luong) FROM NhanVien);
      

      → Kết quả:

      MaNV

      TenNV

      Luong

      NV05

      Hoàng Văn E

      90000

b. Column Subquery (Truy vấn con cột)

  • Đặc điểm: Trả về một cột chứa nhiều hàng giá trị.

  • Sử dụng: Thường đi kèm với các toán tử như IN, NOT IN, ANY, ALL trong mệnh đề WHERE.

  • Ví dụ: Tìm thông tin các phòng ban nhân viên đang làm việc.

    • Bước 1 (Subquery): Lấy danh sách mã phòng ban (MaPB) từ bảng NhanVien (những phòng ban có người làm).

        SELECT DISTINCT MaPB FROM NhanVien WHERE MaPB IS NOT NULL
      

      → Kết quả trả về là PB01, PB02, PB03

    • Bước 2 (Truy vấn cha): Tìm phòng ban có MaPB nằm trong danh sách vừa lấy được.

        SELECT MaPB, TenPB
        FROM PhongBan
        WHERE MaPB IN (SELECT DISTINCT MaPB FROM NhanVien WHERE MaPB IS NOT NULL);
      

      → Kết quả:

      MaPB

      TenPB

      PB01

      Kinh Doanh

      PB02

      Kỹ Thuật

      PB03

      Nhân Sự

c. Row Subquery (Truy vấn con hàng)

  • Đặc điểm: Trả về một hàng duy nhất chứa nhiều cột giá trị.

  • Sử dụng: Thường dùng để so sánh nhiều cột cùng lúc trong mệnh đề WHERE.

  • Ví dụ: Tìm nhân viên có cùng MaPBLuong với nhân viên NV01.

    • Bước 1 (Subquery): Lấy MaPBLuong của NV01.

        SELECT MaPB, Luong FROM NhanVien WHERE MaNV = 'NV01'
      

      → Kết quả trả về là một hàng: ('PB01', 60000)

    • Bước 2 (Truy vấn cha): Tìm các nhân viên khác NV01 mà có MaPBLuong khớp với hàng kết quả từ Subquery.

        SELECT MaNV, TenNV
        FROM NhanVien
        WHERE (MaPB, Luong) = (SELECT MaPB, Luong FROM NhanVien WHERE MaNV = 'NV01')
          AND MaNV <> 'NV01'; -- Loại trừ chính NV01
      
    • Kết quả: Nếu có người khác cùng phòng, cùng lương, họ sẽ hiện ra.

d. Table Subquery (Truy vấn con bảng)

  • Đặc điểm: Trả về nhiều hàngnhiều cột, giống như một bảng ảo.

  • Sử dụng: Thường được đặt trong mệnh đề FROM hoặc JOIN. Khi đặt trong FROM, nó còn được gọi là Derived Table (Bảng dẫn xuất) hoặc Inline View. Bạn phải đặt tên (alias) cho Table Subquery này.

  • Ví dụ: Tìm tên nhân viên và tên phòng ban của họ, nhưng chỉ lấy những phòng ban có lương trung bình trên 65000.

    • Bước 1 (Subquery - Table Subquery): Tính lương trung bình cho mỗi phòng ban và chỉ giữ lại những phòng có lương trung bình > 65000.

        SELECT MaPB, AVG(Luong) AS LuongTB
        FROM NhanVien
        GROUP BY MaPB
        HAVING AVG(Luong) > 65000
      

      → Kết quả trả về là:

      MaPB

      LuongTB

      PB01

      67500

      PB02

      65000

      PB03

      90000

  • Bước 2 (Truy vấn cha): Kết nối (JOIN) bảng NhanVien với kết quả của Subquery (đặt tên là PhongBanLuongCao) dựa trên MaPB.

      SELECT NV.TenNV, PB.TenPB
      FROM NhanVien AS NV
      INNER JOIN PhongBan AS PB ON NV.MaPB = PB.MaPB
      INNER JOIN ( -- Đây là Table Subquery
          SELECT MaPB
          FROM NhanVien
          GROUP BY MaPB
          HAVING AVG(Luong) > 65000
      ) AS PhongBanLuongCao ON NV.MaPB = PhongBanLuongCao.MaPB;
    

    (Cách khác, JOIN với bảng PhongBan trước rồi mới JOIN với subquery)

      SELECT NV.TenNV, PB.TenPB
      FROM NhanVien AS NV
      INNER JOIN ( -- Table Subquery tính lương TB và lọc
           SELECT MaPB
           FROM NhanVien
           GROUP BY MaPB
           HAVING AVG(Luong) > 65000
         ) AS PB_Filtered ON NV.MaPB = PB_Filtered.MaPB
      INNER JOIN PhongBan AS PB ON NV.MaPB = PB.MaPB;
    

    → Kết quả:

    TenNV

    TenPB

    Nguyễn Văn A

    Kinh Doanh

    Trần Thị B

    Kinh Doanh

    Hoàng Văn E

    Nhân Sự


2. Nested Subqueries (Truy vấn con lồng nhau)

  • Định nghĩa: Đơn giản là một Subquery nằm bên trong một Subquery khác. Bạn có thể lồng nhiều cấp.

  • Cách hoạt động: Truy vấn con ở cấp sâu nhất sẽ được thực thi trước, kết quả của nó được dùng cho truy vấn con ở cấp ngoài liền kề, cứ thế cho đến truy vấn cha ngoài cùng.

Ví dụ: Tìm nhân viên có lương cao hơn lương trung bình của tất cả nhân viên trong phòng ban "Kinh Doanh".

  • Subquery cấp 2 (trong cùng): Tìm MaPB của phòng 'Kinh Doanh'.

      SELECT MaPB FROM PhongBan WHERE TenPB = 'Kinh Doanh'
    

    → Kết quả: PB01

  • Subquery cấp 1: Tính lương trung bình của các nhân viên có MaPB = PB01.

      SELECT AVG(Luong) FROM NhanVien WHERE MaPB = (SELECT MaPB FROM PhongBan WHERE TenPB = 'Kinh Doanh')
    

    → Kết quả: (60000 + 75000) / 2 = 6750)

  • Truy vấn cha: Tìm nhân viên có lương > 67500.

      SELECT TenNV, Luong
      FROM NhanVien
      WHERE Luong > (
          SELECT AVG(Luong)
          FROM NhanVien
          WHERE MaPB = (SELECT MaPB FROM PhongBan WHERE TenPB = 'Kinh Doanh')
      );
    

    → Kết quả:

    TenNV

    Luong

    Trần Thị B

    75000

    Phạm Thị D

    80000

    Hoàng Văn E

    90000

💡
Lồng quá nhiều cấp Subquery có thể làm câu lệnh khó đọc và ảnh hưởng đến hiệu năng. Cân nhắc dùng JOIN hoặc Common Table Expressions (CTE) để thay thế nếu có thể.

3. Correlated Subqueries (Truy vấn con tương quan)

  • Định nghĩa: Đây là loại Subquery "đặc biệt". Không giống như các loại trên (thường chỉ chạy 1 lần), Correlated Subquery sẽ chạy lặp đi lặp lại, một lần cho mỗi hàng được xử lý bởi truy vấn cha. Lý do là vì Subquery này tham chiếu đến một (hoặc nhiều) cột từ truy vấn cha.

  • Cách hoạt động:

    1. Truy vấn cha lấy một hàng để xử lý (ví dụ: hàng của NV01).

    2. Giá trị từ hàng đó (ví dụ: MaPB = 'PB01') được "truyền vào" cho Subquery tương quan.

    3. Subquery tương quan thực thi với giá trị đó (tính lương trung bình của PB01).

    4. Kết quả của Subquery được trả về cho truy vấn cha để hoàn thành điều kiện WHERE cho hàng NV01.

    5. Lặp lại quy trình cho hàng tiếp theo của truy vấn cha (NV02, NV03...).

  • Ví dụ: Tìm những nhân viên có mức lương cao hơn lương trung bình của chính phòng ban mà họ đang làm việc. Để làm điều này, với mỗi nhân viên (mỗi hàng của truy vấn cha), bạn phải chạy lại phép tính lương trung bình chỉ riêng cho phòng ban của nhân viên đó.

      SELECT NV1.MaNV, NV1.TenNV, NV1.Luong, NV1.MaPB
      FROM NhanVien AS NV1 -- Đặt alias NV1 cho bảng ở truy vấn cha
      WHERE NV1.Luong > (
          -- Bắt đầu Correlated Subquery
          SELECT AVG(NV2.Luong)
          FROM NhanVien AS NV2 -- Đặt alias NV2 cho bảng ở subquery
          WHERE NV2.MaPB = NV1.MaPB -- Đây là điểm TƯƠNG QUAN: Subquery dùng MaPB từ truy vấn cha (NV1)
          -- Kết thúc Correlated Subquery
      );
    

    Kết quả cuối cùng:

    MaNV

    TenNV

    Luong

    MaPB

    NV02

    Trần Thị B

    75000

    PB01

    NV04

    Phạm Thị D

    80000

    PB02

    Giải thích ví dụ:

    • Khi truy vấn cha xử lý NV01 (MaPB = 'PB01', Luong = 60000): Subquery sẽ chạy SELECT AVG(Luong) FROM NhanVien WHERE MaPB = 'PB01', kết quả là 67500. Điều kiện 60000 > 67500 là SAI -> NV01 không được chọn.

    • Khi truy vấn cha xử lý NV02 (MaPB = 'PB01', Luong = 75000): Subquery chạy lại SELECT AVG(Luong) FROM NhanVien WHERE MaPB = 'PB01', kết quả là 67500. Điều kiện 75000 > 67500 là ĐÚNG -> NV02 được chọn.

    • Khi truy vấn cha xử lý NV03 (MaPB = 'PB02', Luong = 50000): Subquery chạy SELECT AVG(Luong) FROM NhanVien WHERE MaPB = 'PB02', kết quả là (50000 + 80000) / 2 = 65000. Điều kiện 50000 > 65000 là SAI -> NV03 không được chọn.

    • Khi truy vấn cha xử lý NV04 (MaPB = 'PB02', Luong = 80000): Subquery chạy lại SELECT AVG(Luong) FROM NhanVien WHERE MaPB = 'PB02', kết quả là 65000. Điều kiện 80000 > 65000 là ĐÚNG -> NV04 được chọn.

    • Khi truy vấn cha xử lý NV05 (MaPB = 'PB03', Luong = 90000): Subquery chạy SELECT AVG(Luong) FROM NhanVien WHERE MaPB = 'PB03', kết quả là 90000. Điều kiện 90000 > 90000 là SAI -> NV05 không được chọn.

  • Toán tử EXISTSNOT EXISTS: Rất thường được dùng với Correlated Subqueries để kiểm tra xem có tồn tại hàng nào trong Subquery thỏa mãn điều kiện liên quan đến hàng hiện tại của truy vấn cha hay không.

    • Ví dụ: Tìm các phòng ban ít nhất một nhân viên.

        SELECT PB.MaPB, PB.TenPB
        FROM PhongBan AS PB
        WHERE EXISTS ( -- Kiểm tra xem có tồn tại...
            SELECT 1 -- Chỉ cần chọn giá trị bất kỳ để kiểm tra tồn tại
            FROM NhanVien AS NV
            WHERE NV.MaPB = PB.MaPB -- ...nhân viên nào thuộc phòng ban này không
        );
      
💡
Vì Correlated Subquery chạy lại nhiều lần, nó thường chậm hơn các loại Subquery khác hoặc các phương pháp thay thế như JOIN, đặc biệt với các bảng lớn. Tuy nhiên, trong một số trường hợp, nó lại là cách diễn đạt logic tự nhiên và dễ hiểu nhất.

Tổng kết

  • Subquery là truy vấn con nằm trong truy vấn cha, giúp giải quyết bài toán phức tạp.

  • Phân loại theo kết quả trả về: Scalar (1 giá trị), Column (1 cột, nhiều hàng), Row (1 hàng, nhiều cột), Table (nhiều hàng, nhiều cột).

  • Nested Subquery: Subquery lồng trong subquery khác, thực thi từ trong ra ngoài.

  • Correlated Subquery: Subquery phụ thuộc và chạy lặp lại cho mỗi hàng của truy vấn cha, thường dùng EXISTS hoặc so sánh với giá trị từ truy vấn cha.

  • Luôn cân nhắc:

    • Tính dễ đọc: Subquery có làm câu lệnh dễ hiểu hơn không?

    • Hiệu năng: Liệu có cách khác (như JOIN) hiệu quả hơn không? Đặc biệt cẩn thận với Correlated Subquery trên bảng lớn.

    • Đơn giản hóa: Chia nhỏ truy vấn phức tạp thành các bước hoặc sử dụng CTE (Common Table Expressions) khi bạn học nâng cao hơn.

0
Subscribe to my newsletter

Read articles from Ha Ngoc Hieu directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ha Ngoc Hieu
Ha Ngoc Hieu