Tối Ưu Mệnh Đề Where: Phương Pháp Toàn Diện Và Lưu Ý Thực Tế

1. Kiểm Tra Kế Hoạch Thực Thi (Execution Plan) Trước

Trước khi tối ưu hóa, hãy dùng EXPLAIN (MySQL, SQL Server, Oracle) hoặc EXPLAIN ANALYZE (PostgreSQL) để xem:

  • Câu lệnh đang dùng Index Scan hay Full Table Scan?

  • Có dùng Index Filter, Index Condition hay Index Skip Scan?

  • Có bị “function call” trên cột khiến DBMS bỏ qua index?

Ví dụ (PostgreSQL):

EXPLAIN ANALYZE
SELECT * 
FROM employees
WHERE department_id = 10;
  • Kiểm tra cost, rows, actual time để biết tình trạng query.

2. Đảm Bảo Cột Trong WHERE Có Index Phù Hợp

Đây vẫn là nền tảng quan trọng nhất. Nếu mệnh đề WHERE lọc nhiều trên cùng một cột, hãy có index đơn giản hoặc index phức hợp (composite index) nếu truy vấn có nhiều cột trong điều kiện.

Ví dụ:

-- Truy vấn chậm do thiếu index:
SELECT * FROM employees WHERE department_id = 10;

-- Tạo index:
CREATE INDEX idx_employees_department_id ON employees(department_id);
  • Sau khi tạo index, chạy lại EXPLAIN để kiểm chứng DBMS đã dùng Index Scan hay chưa.

Lưu ý: Thận trọng khi thêm quá nhiều index nếu bảng cập nhật thường xuyên, vì chi phí ghi (INSERT/UPDATE/DELETE) sẽ tăng.


3. Tránh Dùng Hàm hoặc Biểu Thức Phức Tạp Trên Cột Được Index

Nếu bạn viết WHERE YEAR(hire_date) = 2023, DBMS khó sử dụng index trên hire_date (vì cột bị bao bọc bởi hàm). Hãy viết theo cách range (BETWEEN) hoặc so sánh trực tiếp.

Ví dụ:

-- Dùng hàm, làm mất cơ hội index:
SELECT * 
FROM employees
WHERE YEAR(hire_date) = 2023;

-- Tối ưu, không dùng hàm:
SELECT *
FROM employees
WHERE hire_date >= '2023-01-01'
  AND hire_date < '2024-01-01';
  • Đảm bảo index được sử dụng (kiểm tra bằng EXPLAIN).

4. Dùng Toán Tử So Sánh Hiệu Quả: =, >, <, BETWEEN, LIKE Có Ký Tự Đại Diện Ở Cuối

  • Các toán tử = hoặc BETWEEN được DBMS tối ưu rất tốt khi kết hợp với index.

  • LIKE '%ABC' cũng không sử dụng index (vì wildcard ở đầu), nhưng LIKE 'ABC%' có thể sử dụng index.

  • Để tìm kiếm từ khóa phức tạp, cân nhắc Full-Text Search hay Index Full-Text (SQL Server, PostgreSQL GIN/GiST, ElasticSearch đối với ngoại DB, v.v.).

Ví dụ:

-- Tình huống LIKE khó tối ưu index:
SELECT * FROM employees
WHERE name LIKE '%John%';

-- Nếu DB hỗ trợ Full-Text:
SELECT * FROM employees 
WHERE MATCH(name) AGAINST('John' IN NATURAL LANGUAGE MODE);
  • Nên dùng Full-Text Index để tăng tốc tìm kiếm “chứa chuỗi con”.

5. Sử Dụng EXISTS Thay Vì IN Khi Subquery Trả Về Nhiều Kết Quả

Tuy INEXISTS đều đúng về logic, nhưng EXISTS có thể dừng sớm khi tìm thấy một dòng phù hợp. Tuy nhiên, điều này cũng phụ thuộc vào cost-based optimizer. Luôn test với EXPLAIN.

Ví dụ:

-- Dùng IN:
SELECT * FROM employees 
WHERE department_id IN (
   SELECT department_id 
   FROM departments 
   WHERE location = 'New York'
);

-- Dùng EXISTS:
SELECT * 
FROM employees e
WHERE EXISTS (
   SELECT 1 
   FROM departments d
   WHERE d.department_id = e.department_id
     AND d.location = 'New York'
);
  • Nếu subquery trả về rất nhiều dòng, EXISTS có thể hiệu quả hơn.

6. Sắp Xếp Điều Kiện WHERE Theo Độ Chọn Lọc? (Tùy Thuộc Optimizer)

Nhiều người khuyên “đặt điều kiện lọc mạnh (loại bỏ nhiều dòng) lên trước”. Thực tế, các cost-based optimizer hiện đại (PostgreSQL, SQL Server, Oracle, MySQL 8+) có thể tự sắp xếp thứ tự lọc để tối ưu. Cách viết thứ tự trong WHERE đôi khi không còn nhiều ý nghĩa như trước.

Dù vậy, với nhiều điều kiện phức tạp, bạn nên:

  1. Xem EXPLAIN để đảm bảo DBMS đang push điều kiện nào trước.

  2. Đặc biệt hữu ích khi DBMS không tự rearrange (một số phiên bản cũ).


7. Tránh Lạm Dụng OR, Nhưng Hãy Kiểm Tra “Index Or” (Một Số DBMS)

Khi có biểu thức OR, DBMS có thể không sử dụng index tốt, nhất là khi OR kết hợp nhiều cột khác nhau. Tránh OR bằng cách dùng UNION ALL (hoặc UNION) để tách thành hai truy vấn nếu mỗi truy vấn có index riêng.

Ví dụ:

-- Có thể chậm: OR giữa hai cột
SELECT * FROM employees
WHERE department_id = 10
   OR salary > 50000;

-- Tách thành 2 truy vấn với UNION ALL:
SELECT * FROM employees WHERE department_id = 10
UNION ALL
SELECT * FROM employees WHERE salary > 50000;
  • Tuy nhiên, một số DBMS hiện đại có Index Or (SQL Server, Oracle) hoặc Bitmap Or (PostgreSQL) cho phép tận dụng nhiều index. Kiểm tra EXPLAIN để quyết định.

8. Tận Dụng Covering Index (Index Bao Phủ)

Nếu truy vấn chỉ lấy các cột xuất hiện trong index, DBMS không cần đọc bảng gốc.

Ví dụ:

-- Truy vấn chỉ cần employee_id, name, department_id
SELECT employee_id, name
FROM employees
WHERE department_id = 10;

-- Tạo index covering:
CREATE INDEX idx_employees_dept_covering
   ON employees(department_id, employee_id, name);

-- DB có thể chỉ đọc index mà không cần scan bảng.
  • Xem EXPLAIN: thay vì Index Scan + Heap Fetch, nó có thể chỉ là Index Only Scan.

9. Partitioning Để Giới Hạn Dữ Liệu Quét

Khi bảng rất lớn và được phân chia theo phạm vi (vd: theo năm, theo vùng địa lý), DBMS có thể prune các partition không liên quan, giúp đỡ tốn I/O.

Ví dụ (PostgreSQL):

CREATE TABLE employees (
    employee_id INT,
    name VARCHAR(100),
    department_id INT,
    hire_date DATE
) PARTITION BY RANGE (YEAR(hire_date));

CREATE TABLE employees_2023 PARTITION OF employees
FOR VALUES FROM (2023) TO (2024);

-- Truy vấn
SELECT * FROM employees
WHERE hire_date >= '2023-01-01'
  AND hire_date < '2024-01-01';
  • DBMS chỉ đọc partition employees_2023, tiết kiệm lớn khi dữ liệu khổng lồ.

10. Materialized View Cho Mệnh Đề WHERE Lặp Lại Trên Dữ Liệu Ít Thay Đổi

Nếu mệnh đề WHERE phức tạp hoặc tốn thời gian (join nhiều bảng, v.v.) mà dữ liệu cập nhật không nhiều, materialized view lưu sẵn kết quả, đọc nhanh hơn.

Ví dụ:

CREATE MATERIALIZED VIEW mv_high_salary_employees AS
SELECT employee_id, name, department_id, salary
FROM employees
WHERE salary > 50000;

SELECT * 
FROM mv_high_salary_employees
WHERE department_id = 10;
  • Phải lên lịch REFRESH MATERIALIZED VIEW khi dữ liệu thay đổi.

11. Tối Ưu Cấu Hình Database

  • work_mem (PostgreSQL), join_buffer_size (MySQL), hay Max Degree of Parallelism (SQL Server): hỗ trợ thao tác phân loại (sort), merge, hash…

  • Parallel Query: Tận dụng đa CPU, đẩy nhanh tốc độ lọc.


Quy Trình Chung Để Tối Ưu Mệnh Đề WHERE

  1. Dùng EXPLAIN để xem DBMS hiện làm gì (Full Table Scan, Index Scan, Filter, Projection?).

  2. Tạo index phù hợp với cột trong mệnh đề WHERE.

  3. Tránh hàm bao bọc cột được index; dùng range hoặc các biểu thức tương đương.

  4. Test EXISTS vs. IN nếu subquery trả về nhiều dòng.

  5. Kiểm tra DBMS có tự sắp xếp điều kiện (predicate pushdown) hay tự dùng “Index Or” scan.

  6. Xem xét Covering Index nếu query trả về cột nào thì index hết cột đó.

  7. Sử dụng Partitioning khi khối lượng dữ liệu cực lớn và có thể chia tách theo điều kiện thời gian/phạm vi.

  8. Materialized View cho truy vấn lặp lại, dữ liệu ít thay đổi.

  9. Thử thay đổi cấu hình DB (bộ nhớ, parallel) nếu truy vấn vẫn chậm.

  10. Đo lường hiệu suất trước/sau (thời gian, chi phí) để đánh giá hiệu quả thực sự.


Dưới đây là 10 ví dụ về các truy vấn (query) tồi liên quan đến mệnh đề WHERE 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 mệnh đề WHERE không hiệu quả.


1. Sử Dụng Hàm Trên Cột Được Index

Truy vấn tồi:

SELECT * FROM employees WHERE YEAR(hire_date) = 2023;

Truy vấn chuẩn:

SELECT * FROM employees 
WHERE hire_date BETWEEN '2023-01-01' AND '2023-12-31';

Lý do:

  • Sử dụng hàm YEAR trên cột hire_date làm mất hiệu quả của index. Thay vào đó, sử dụng phạm vi (BETWEEN) để tận dụng index.

2. Sử Dụng LIKE Với Ký Tự Đại Diện Ở Đầu

Truy vấn tồi:

SELECT * FROM employees WHERE name LIKE '%John%';

Truy vấn chuẩn:

SELECT * FROM employees 
WHERE name LIKE 'John%';

Lý do:

  • LIKE '%John%' không thể sử dụng index hiệu quả. Nếu có thể, hãy sử dụng LIKE 'John%' để tận dụng index.

3. Sử Dụng OR Không Cần Thiết

Truy vấn tồi:

SELECT * FROM employees 
WHERE department_id = 10 OR salary > 50000;

Truy vấn chuẩn:

SELECT * FROM employees WHERE department_id = 10
UNION ALL
SELECT * FROM employees WHERE salary > 50000;

Lý do:

  • OR có thể làm chậm truy vấn vì nó yêu cầu quét toàn bộ bảng. Sử dụng UNION ALL để tách các điều kiện thành các truy vấn riêng biệt.

4. Sử Dụng IN Với Danh Sách Giá Trị Lớn

Truy vấn tồi:

SELECT * FROM employees 
WHERE department_id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...);

Truy vấn chuẩn:

SELECT * FROM employees 
WHERE department_id BETWEEN 1 AND 10;

Lý do:

  • IN với danh sách giá trị lớn có thể làm chậm truy vấn. Sử dụng BETWEEN nếu các giá trị liên tục.

5. Sử Dụng Subquery Không Cần Thiết Trong WHERE

Truy vấn tồi:

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

Truy vấn chuẩn:

SELECT e.*
FROM employees e
JOIN departments d ON e.department_id = d.department_id
WHERE d.location = 'New York';

Lý do:

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

6. Sử Dụng NOT IN Với Subquery

Truy vấn tồi:

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

Truy vấn chuẩn:

SELECT e.*
FROM employees e
LEFT JOIN departments d ON e.department_id = d.department_id
AND d.location = 'New York'
WHERE d.department_id IS NULL;

Lý do:

  • NOT IN với subquery có thể chậm và gặp vấn đề với giá trị NULL. Sử dụng LEFT JOINIS NULL để thay thế.

7. Sử Dụng WHERE Với Phép Toán Phức Tạp

Truy vấn tồi:

SELECT * FROM employees 
WHERE salary * 1.1 > 50000;

Truy vấn chuẩn:

SELECT * FROM employees 
WHERE salary > 50000 / 1.1;

Lý do:

  • Phép toán phức tạp trong WHERE có thể làm chậm truy vấn. Hãy đơn giản hóa phép toán nếu có thể.

8. Sử Dụng WHERE Với Hàm UPPER/LOWER

Truy vấn tồi:

SELECT * FROM employees 
WHERE UPPER(name) = 'JOHN';

Truy vấn chuẩn:

SELECT * FROM employees 
WHERE name = 'John' COLLATE SQL_Latin1_General_CP1_CS_AS;

Lý do:

  • Sử dụng hàm UPPER hoặc LOWER trên cột làm mất hiệu quả của index. Sử dụng collation để so sánh không phân biệt chữ hoa chữ thường.

9. Sử Dụng WHERE Với ISNULL Hoặc COALESCE

Truy vấn tồi:

SELECT * FROM employees 
WHERE ISNULL(salary, 0) > 50000;

Truy vấn chuẩn:

SELECT * FROM employees 
WHERE salary > 50000 OR salary IS NULL;

Lý do:

  • Sử dụng ISNULL hoặc COALESCE trong WHERE có thể làm chậm truy vấn. Hãy tách điều kiện thành các phần riêng biệt.

10. Sử Dụng WHERE Với DISTINCT Không Cần Thiết

Truy vấn tồi:

SELECT DISTINCT department_id
FROM employees
WHERE salary > 50000;

Truy vấn chuẩn:

SELECT department_id
FROM employees
WHERE salary > 50000
GROUP BY department_id;

Lý do:

  • DISTINCT có thể làm chậm truy vấn. Sử dụng GROUP BY để thay thế nếu có thể.

Tổng Kết

Truy Vấn TồiTruy Vấn ChuẩnLý Do
Sử dụng hàm trên cột được indexSử dụng phạm vi (BETWEEN)Hàm làm mất hiệu quả của index.
Sử dụng LIKE với ký tự đại diện ở đầuSử dụng LIKE không có ký tự đại diện ở đầuLIKE '%John%' không thể sử dụng index hiệu quả.
Sử dụng OR không cần thiếtSử dụng UNION ALLOR yêu cầu quét toàn bộ bảng.
Sử dụng IN với danh sách giá trị lớnSử dụng BETWEENIN với danh sách lớn có thể làm chậm truy vấn.
Sử dụng subquery không cần thiếtSử dụng JOINSubquery trong WHERE có thể được thay thế bằng JOIN.
Sử dụng NOT IN với subquerySử dụng LEFT JOINIS NULLNOT IN với subquery có thể chậm và gặp vấn đề với NULL.
Sử dụng phép toán phức tạp trong WHEREĐơn giản hóa phép toánPhép toán phức tạp làm chậm truy vấn.
Sử dụng hàm UPPER/LOWERSử dụng collationHàm làm mất hiệu quả của index.
Sử dụng ISNULL hoặc COALESCETách điều kiện thành các phần riêng biệtISNULL hoặc COALESCE trong WHERE có thể làm chậm truy vấn.
Sử dụng DISTINCT không cần thiếtSử dụng GROUP BYDISTINCT có thể làm chậm truy vấn.

Kết Luận

Các truy vấn tồi thường xuất phát từ việc sử dụng mệnh đề WHERE không hiệu quả. Bằng cách áp dụng các phương pháp tối ưu hóa như sử dụng index, tránh hàm trên cột, và thay thế OR bằng UNION ALL, 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!