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ặcBETWEEN
đượ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ưngLIKE '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 IN
và EXISTS
đề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:
Xem
EXPLAIN
để đảm bảo DBMS đang push điều kiện nào trước.Đặ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), hayMax 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
Dùng
EXPLAIN
để xem DBMS hiện làm gì (Full Table Scan, Index Scan, Filter, Projection?).Tạo index phù hợp với cột trong mệnh đề
WHERE
.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.
Test
EXISTS
vs.IN
nếu subquery trả về nhiều dòng.Kiểm tra DBMS có tự sắp xếp điều kiện (predicate pushdown) hay tự dùng “Index Or” scan.
Xem xét Covering Index nếu query trả về cột nào thì index hết cột đó.
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.
Materialized View cho truy vấn lặp lại, dữ liệu ít thay đổi.
Thử thay đổi cấu hình DB (bộ nhớ, parallel) nếu truy vấn vẫn chậm.
Đ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ộthire_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ụngLIKE '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ụngUNION 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ụngBETWEEN
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ằngJOIN
, 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ụngLEFT JOIN
vàIS 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ặcLOWER
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ặcCOALESCE
trongWHERE
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ụngGROUP BY
để thay thế nếu có thể.
Tổng Kết
Truy Vấn Tồi | Truy Vấn Chuẩn | Lý Do |
Sử dụng hàm trên cột được index | Sử 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 ở đầu | Sử dụng LIKE không có ký tự đại diện ở đầu | LIKE '%John%' không thể sử dụng index hiệu quả. |
Sử dụng OR không cần thiết | Sử dụng UNION ALL | OR yêu cầu quét toàn bộ bảng. |
Sử dụng IN với danh sách giá trị lớn | Sử dụng BETWEEN | IN 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ết | Sử dụng JOIN | Subquery trong WHERE có thể được thay thế bằng JOIN . |
Sử dụng NOT IN với subquery | Sử dụng LEFT JOIN và IS NULL | NOT 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án | Phép toán phức tạp làm chậm truy vấn. |
Sử dụng hàm UPPER /LOWER | Sử dụng collation | Hàm làm mất hiệu quả của index. |
Sử dụng ISNULL hoặc COALESCE | Tách điều kiện thành các phần riêng biệt | ISNULL hoặc COALESCE trong WHERE có thể làm chậm truy vấn. |
Sử dụng DISTINCT không cần thiết | Sử dụng GROUP BY | DISTINCT 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!