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 EXISTS
Và IN
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ỉnhMax 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
Dùng
EXPLAIN
để biết DBMS đang làm gì.Xem xét đổi subquery thành JOIN, đặc biệt với correlated subquery.
Kiểm tra
EXISTS
vs.IN
theo từng DBMS và khối lượng dữ liệu thực tế.Sử dụng CTE để tách logic — nhưng chú ý đến cách DBMS xử lý CTE nội bộ.
Lợi dụng materialized view cho dữ liệu ít thay đổi hoặc truy vấn thường xuyên.
Thay thế subquery tính toán bằng Window Function để chỉ quét một lần.
Bảo đảm cột lọc/JOIN có index, tránh full scan không cần thiết.
Tránh correlated subquery; dùng GROUP BY hoặc CTE/temporary table thay thế.
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ơnIN
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ằngJOIN
, 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
JOIN
vàGROUP 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ụngJOIN
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ằngJOIN
, 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ằngUSING
, 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ồi | Truy Vấn Chuẩn | Lý Do |
Subquery không cần thiết | Đưa điều kiện trực tiếp vào WHERE | Subquery làm truy vấn phức tạp và chậm. |
Sử dụng IN thay vì EXISTS | Sử dụng EXISTS | EXISTS hiệu quả hơn khi subquery trả về nhiều dòng. |
Subquery trong SELECT không cần thiết | Sử dụng JOIN | JOIN giúp truy vấn dễ đọc và hiệu quả hơn. |
Correlated subquery không cần thiết | Sử dụng JOIN và GROUP BY | Correlated 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ết | Sử dụng CTE | Subquery trong HAVING được tính toán nhiều lần. |
Subquery trong FROM không cần thiết | Sử dụng JOIN | Subquery trong FROM làm truy vấn khó đọc và chậm. |
Subquery lồng nhau không cần thiết | Sử dụng JOIN | Subquery lồng nhau làm truy vấn phức tạp và chậm. |
Subquery trong UPDATE không cần thiết | Sử dụng JOIN | Subquery trong UPDATE có thể được thay thế bằng JOIN . |
Subquery trong DELETE không cần thiết | Sử dụng USING | Subquery trong DELETE có thể được thay thế bằng USING . |
Subquery trong INSERT không cần thiết | Sử dụng CTE | Subquery 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!