Tối Ưu Subquery: Các Kỹ Thuật Chủ Đạo Và Lưu Ý Quan Trọng

1. Phân Tích Execution Plan Trước Khi Tối Ưu

Trước khi thay đổi truy vấn, cần dùng EXPLAIN (MySQL, SQL Server, Oracle) hoặc EXPLAIN ANALYZE (PostgreSQL) để xem:

  • Phương án DBMS đang dùng (ví dụ: Nested Loop hay Index Scan).

  • Subquery có bị thực thi lặp (correlated subquery) hay không.

  • Có quét toàn bộ bảng (Full Table Scan/Seq Scan) hay đã dùng index.

Ví dụ (PostgreSQL):

EXPLAIN ANALYZE
SELECT employee_id, name
FROM employees e
WHERE EXISTS (
    SELECT 1
    FROM orders o
    WHERE o.employee_id = e.employee_id
);
  • Kiểm tra EXPLAIN để xem DB có tự tối ưu thành JOIN nội bộ hay không.

2. Chuyển Subquery Thành JOIN (Đặc Biệt Với Correlated Subquery)

Khi một subquery phụ thuộc vào từng dòng từ bảng ngoài (correlated subquery), nó có thể được thực thi nhiều lần, gây lãng phí tài nguyên. Chuyển thành JOIN giúp DB chỉ cần quét dữ liệu và so khớp một lần.

Ví dụ:

-- Subquery ban đầu:
SELECT employee_id, name
FROM employees e
WHERE EXISTS (
    SELECT 1
    FROM orders o
    WHERE o.employee_id = e.employee_id
);

-- Tối ưu bằng JOIN + DISTINCT:
SELECT DISTINCT e.employee_id, e.name
FROM employees e
JOIN orders o ON e.employee_id = o.employee_id;
  • Kết quả tương đương.

  • Giảm số lần thực thi subquery.


3. So Sánh EXISTSIN

  • IN buộc DBMS so sánh với toàn bộ tập kết quả subquery.

  • EXISTS dừng ngay khi tìm thấy bản ghi phù hợp.

Tuy nhiên, hiệu năng thực tế phụ thuộc vào optimizer. Hãy kiểm tra với EXPLAIN để chắc chắn cách viết nào tối ưu hơn trong DBMS cụ thể.

Ví dụ:

-- Sử dụng IN:
SELECT employee_id, name
FROM employees
WHERE employee_id IN (
    SELECT employee_id
    FROM orders
    WHERE order_date > '2023-01-01'
);

-- Sử dụng EXISTS:
SELECT e.employee_id, e.name
FROM employees e
WHERE EXISTS (
    SELECT 1
    FROM orders o
    WHERE o.employee_id = e.employee_id
      AND o.order_date > '2023-01-01'
);
  • Trong nhiều DBMS, EXISTS thường có lợi khi subquery trả về rất nhiều dòng.

4. Sử Dụng CTE (Common Table Expressions)

CTE giúp tách nhỏ logic phức tạp, làm code dễ đọc. Tuy nhiên, trong một số DBMS (như PostgreSQL), CTE mặc định xử lý như subquery materialization (được thực thi một lần và lưu kết quả), nên cần chú ý hiệu năng nếu CTE được dùng nhiều lần. Kiểm tra EXPLAIN để xác minh.

Ví dụ:

-- Subquery lồng nhau:
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
WHERE department_id IN (
    SELECT department_id
    FROM departments
    WHERE location = 'New York'
)
GROUP BY department_id;

-- Tối ưu bằng CTE và JOIN:
WITH dept_ny AS (
    SELECT department_id
    FROM departments
    WHERE location = 'New York'
)
SELECT e.department_id, AVG(e.salary) AS avg_salary
FROM employees e
JOIN dept_ny d ON e.department_id = d.department_id
GROUP BY e.department_id;
  • Đo lường thời gian thực thi để chắc chắn lợi ích.

5. Tận Dụng Materialized View (MV)

Nếu subquery nặng, được gọi thường xuyên, và dữ liệu không thay đổi quá nhanh, MV giúp lưu sẵn kết quả để truy vấn nhanh hơn.

Ví dụ:

-- Tạo MV
CREATE MATERIALIZED VIEW high_budget_departments AS
SELECT department_id
FROM departments
WHERE budget > 1000000;

-- Sử dụng MV thay cho subquery
SELECT employee_id, name
FROM employees
WHERE department_id IN (
    SELECT department_id
    FROM high_budget_departments
);
  • Cần sắp xếp chu kỳ REFRESH MATERIALIZED VIEW để dữ liệu không lỗi thời.

6. Áp Dụng Window Functions Thay Cho Subquery Tính Hạng, Tổng, …

Khi subquery dùng để tính giá trị liên quan đến dòng hiện tại (ranking, running total), Window Function chỉ quét dữ liệu một lần, hiệu quả hơn subquery lồng nhau.

Ví dụ:

-- Subquery truyền thống:
SELECT employee_id, salary,
    (SELECT COUNT(*)
     FROM employees e2
     WHERE e2.salary > e1.salary) AS rank
FROM employees e1;

-- Dùng window function:
SELECT employee_id, salary,
    RANK() OVER (ORDER BY salary DESC) AS rank
FROM employees;
  • Rõ ràng, ngắn gọn, dễ bảo trì.

7. Đảm Bảo Index Phù Hợp

Các cột dùng trong subquery (hoặc mệnh đề JOIN, WHERE) cần index, đặc biệt cột thường bị lọc. Xem kế hoạch thực thi để biết DB có dùng index hay không.

Ví dụ:

CREATE INDEX idx_orders_order_date ON orders(order_date);
CREATE INDEX idx_employees_department_id ON employees(department_id);
  • Giúp tránh Full Table Scan khi JOIN hoặc so sánh.

  • Cân nhắc composite index nếu truy vấn dựa vào nhiều cột.


8. Tránh Correlated Subquery Nếu Có Thể

Correlated subquery (liên kết với bảng chính theo từng dòng) thường dẫn đến việc subquery chạy lặp. Nên chuyển thành JOIN + GROUP BY hoặc CTE.

Ví dụ:

-- Correlated Subquery:
SELECT employee_id, name,
    (SELECT AVG(salary)
     FROM employees e2
     WHERE e2.department_id = e1.department_id) AS avg_salary
FROM employees e1;

-- Tối ưu bằng JOIN + GROUP BY:
SELECT e1.employee_id, e1.name, AVG(e2.salary) AS avg_salary
FROM employees e1
JOIN employees e2 ON e1.department_id = e2.department_id
GROUP BY e1.employee_id, e1.name;
  • Một lần quét lớn vẫn thường nhanh hơn vô số subquery nhỏ.

9. Sử Dụng Temporary Table (Hoặc Table Tạm Thời Khác)

Khi subquery phức tạp, ta có thể chạy một lệnh tạo bảng tạm, sau đó JOIN/IN/EXISTS bảng tạm. Lợi ích đi kèm chi phí dung lượng và thời gian tạo bảng tạm, nên chỉ phù hợp trong một số tình huống (ETL, truy vấn báo cáo nặng…).

Ví dụ:

CREATE TEMP TABLE high_budget_departments AS
SELECT department_id
FROM departments
WHERE budget > 1000000;

SELECT e.employee_id, e.name
FROM employees e
WHERE e.department_id IN (
    SELECT department_id
    FROM high_budget_departments
);
  • Tách logic phức tạp ra từng bước.

  • Hữu ích khi tái sử dụng bảng tạm trong nhiều truy vấn tiếp theo (trong cùng session).


10. Tối Ưu Cấu Hình Database & Thử Nhiều Kịch Bản

  • Kiểm tra song song (Parallel Query): Ở PostgreSQL, cấu hình max_parallel_workers_per_gather, work_mem đủ lớn. Ở SQL Server, tinh chỉnh Max Degree of Parallelism

  • So sánh trước/sau thay đổi: Luôn đối chiếu Execution Plan và thời gian thực tế để xác định cải thiện hiệu suất.


Tổng Kết Quy Trình

  1. Dùng EXPLAIN để biết DBMS đang làm gì.

  2. Xem xét đổi subquery thành JOIN, đặc biệt với correlated subquery.

  3. Kiểm tra EXISTS vs. IN theo từng DBMS và khối lượng dữ liệu thực tế.

  4. Sử dụng CTE để tách logic — nhưng chú ý đến cách DBMS xử lý CTE nội bộ.

  5. Lợi dụng materialized view cho dữ liệu ít thay đổi hoặc truy vấn thường xuyên.

  6. Thay thế subquery tính toán bằng Window Function để chỉ quét một lần.

  7. Bảo đảm cột lọc/JOIN có index, tránh full scan không cần thiết.

  8. Tránh correlated subquery; dùng GROUP BY hoặc CTE/temporary table thay thế.

  9. Test hiệu suất & kiểm tra plan sau mỗi bước để tránh tối ưu sai hướng.



Dưới đây là 10 ví dụ về các truy vấn (query) tồi liên quan đến subquery và cách viết lại chúng thành các truy vấn chuẩn, tối ưu hơn. Các truy vấn tồi thường gặp vấn đề về hiệu suất, khó đọc, hoặc sử dụng subquery không cần thiết.

1. Sử Dụng Subquery Không Cần Thiết

Truy vấn tồi:

SELECT employee_id, name
FROM employees
WHERE employee_id IN (
    SELECT employee_id
    FROM employees
    WHERE department_id = 10
);

Truy vấn chuẩn:

SELECT employee_id, name
FROM employees
WHERE department_id = 10;

Lý do:

  • Subquery không cần thiết vì điều kiện có thể được đưa trực tiếp vào mệnh đề WHERE.

2. Sử Dụng IN Thay Vì EXISTS

Truy vấn tồi:

SELECT employee_id, name
FROM employees
WHERE department_id IN (
    SELECT department_id
    FROM departments
    WHERE location = 'New York'
);

Truy vấn chuẩn:

SELECT e.employee_id, e.name
FROM employees e
WHERE EXISTS (
    SELECT 1
    FROM departments d
    WHERE d.department_id = e.department_id
    AND d.location = 'New York'
);

Lý do:

  • EXISTS hiệu quả hơn IN vì nó dừng ngay khi tìm thấy kết quả đầu tiên.

3. Subquery Trong SELECT Không Cần Thiết

Truy vấn tồi:

SELECT employee_id, name,
    (SELECT department_name
     FROM departments d
     WHERE d.department_id = e.department_id) AS department_name
FROM employees e;

Truy vấn chuẩn:

SELECT e.employee_id, e.name, d.department_name
FROM employees e
JOIN departments d ON e.department_id = d.department_id;

Lý do:

  • Subquery trong SELECT có thể được thay thế bằng JOIN, giúp truy vấn dễ đọc và hiệu quả hơn.

4. Correlated Subquery Không Cần Thiết

Truy vấn tồi:

SELECT employee_id, name,
    (SELECT AVG(salary)
     FROM employees e2
     WHERE e2.department_id = e1.department_id) AS avg_salary
FROM employees e1;

Truy vấn chuẩn:

SELECT e1.employee_id, e1.name, AVG(e2.salary) AS avg_salary
FROM employees e1
JOIN employees e2 ON e1.department_id = e2.department_id
GROUP BY e1.employee_id, e1.name;

Lý do:

  • Correlated subquery thực thi nhiều lần, trong khi JOINGROUP BY chỉ cần quét bảng một lần.

5. Subquery Trong HAVING Không Cần Thiết

Truy vấn tồi:

SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
HAVING AVG(salary) > (
    SELECT AVG(salary)
    FROM employees
);

Truy vấn chuẩn:

WITH avg_salary AS (
    SELECT AVG(salary) AS avg_salary
    FROM employees
)
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
HAVING AVG(salary) > (SELECT avg_salary FROM avg_salary);

Lý do:

  • Subquery trong HAVING được tính toán nhiều lần. Sử dụng CTE giúp tính toán một lần và tái sử dụng.

6. Subquery Trong FROM Không Cần Thiết

Truy vấn tồi:

SELECT e.employee_id, e.name, d.department_name
FROM employees e,
    (SELECT department_id, department_name
     FROM departments) d
WHERE e.department_id = d.department_id;

Truy vấn chuẩn:

SELECT e.employee_id, e.name, d.department_name
FROM employees e
JOIN departments d ON e.department_id = d.department_id;

Lý do:

  • Subquery trong FROM không cần thiết và làm truy vấn khó đọc. Sử dụng JOIN trực tiếp là cách tốt hơn.

7. Subquery Lồng Nhau Không Cần Thiết

Truy vấn tồi:

SELECT employee_id, name
FROM employees
WHERE department_id IN (
    SELECT department_id
    FROM departments
    WHERE location_id IN (
        SELECT location_id
        FROM locations
        WHERE city = 'New York'
    )
);

Truy vấn chuẩn:

SELECT e.employee_id, e.name
FROM employees e
JOIN departments d ON e.department_id = d.department_id
JOIN locations l ON d.location_id = l.location_id
WHERE l.city = 'New York';

Lý do:

  • Subquery lồng nhau làm truy vấn phức tạp và chậm. Sử dụng JOIN giúp truy vấn dễ đọc và hiệu quả hơn.

8. Subquery Trong UPDATE Không Cần Thiết

Truy vấn tồi:

UPDATE employees
SET salary = salary * 1.1
WHERE department_id IN (
    SELECT department_id
    FROM departments
    WHERE location = 'New York'
);

Truy vấn chuẩn:

UPDATE employees e
SET salary = salary * 1.1
FROM departments d
WHERE e.department_id = d.department_id
AND d.location = 'New York';

Lý do:

  • Subquery trong UPDATE có thể được thay thế bằng JOIN, giúp truy vấn hiệu quả hơn.

9. Subquery Trong DELETE Không Cần Thiết

Truy vấn tồi:

DELETE FROM employees
WHERE department_id IN (
    SELECT department_id
    FROM departments
    WHERE location = 'New York'
);

Truy vấn chuẩn:

DELETE FROM employees e
USING departments d
WHERE e.department_id = d.department_id
AND d.location = 'New York';

Lý do:

  • Subquery trong DELETE có thể được thay thế bằng USING, giúp truy vấn hiệu quả hơn.

10. Subquery Trong INSERT Không Cần Thiết

Truy vấn tồi:

INSERT INTO high_salary_employees
SELECT employee_id, name, salary
FROM employees
WHERE salary > (
    SELECT AVG(salary)
    FROM employees
);

Truy vấn chuẩn:

WITH avg_salary AS (
    SELECT AVG(salary) AS avg_salary
    FROM employees
)
INSERT INTO high_salary_employees
SELECT e.employee_id, e.name, e.salary
FROM employees e, avg_salary
WHERE e.salary > avg_salary.avg_salary;

Lý do:

  • Subquery trong INSERT được tính toán nhiều lần. Sử dụng CTE giúp tính toán một lần và tái sử dụng.

Tổng Kết

Truy Vấn TồiTruy Vấn ChuẩnLý Do
Subquery không cần thiếtĐưa điều kiện trực tiếp vào WHERESubquery làm truy vấn phức tạp và chậm.
Sử dụng IN thay vì EXISTSSử dụng EXISTSEXISTS hiệu quả hơn khi subquery trả về nhiều dòng.
Subquery trong SELECT không cần thiếtSử dụng JOINJOIN giúp truy vấn dễ đọc và hiệu quả hơn.
Correlated subquery không cần thiếtSử dụng JOINGROUP BYCorrelated subquery thực thi nhiều lần, trong khi JOIN chỉ quét một lần.
Subquery trong HAVING không cần thiếtSử dụng CTESubquery trong HAVING được tính toán nhiều lần.
Subquery trong FROM không cần thiếtSử dụng JOINSubquery trong FROM làm truy vấn khó đọc và chậm.
Subquery lồng nhau không cần thiếtSử dụng JOINSubquery lồng nhau làm truy vấn phức tạp và chậm.
Subquery trong UPDATE không cần thiếtSử dụng JOINSubquery trong UPDATE có thể được thay thế bằng JOIN.
Subquery trong DELETE không cần thiếtSử dụng USINGSubquery trong DELETE có thể được thay thế bằng USING.
Subquery trong INSERT không cần thiếtSử dụng CTESubquery trong INSERT được tính toán nhiều lần.

Kết Luận

Các truy vấn tồi thường xuất phát từ việc sử dụng subquery không cần thiết hoặc không tối ưu. Bằng cách chuyển đổi chúng thành các truy vấn chuẩn sử dụng JOIN, EXISTS, hoặc CTE, bạn có thể cải thiện đáng kể hiệu suất và khả năng đọc của truy vấn. Hãy luôn kiểm tra Execution Plan để xác định điểm nghẽn và áp dụng giải pháp phù hợp!