EXPLAIN là gì? Khi bạn đặt EXPLAIN trước một câu SQL, MySQL không chạy query mà chỉ trả về execution plan — bản kế hoạch mô tả cách engine sẽ truy cập bảng, dùng index nào, ước tính bao nhiêu row.
EXPLAIN SELECT id, user_id, check_in
FROM bookings
WHERE user_id = 123 AND status = 'confirmed';
Tất cả các cột trong output EXPLAIN (MySQL)
| Cột | Ý nghĩa | Ghi chú quan trọng |
|---|---|---|
id |
ID của SELECT (1, 2, 3…). Subquery có id riêng. | Cùng id = cùng level. id lớn hơn = chạy trước (subquery trong → ngoài). |
select_type |
Loại SELECT | SIMPLE (không subquery/UNION), PRIMARY (query ngoài cùng), SUBQUERY, DERIVED (subquery trong FROM), UNION, DEPENDENT SUBQUERY (correlated — cảnh báo đỏ). |
table |
Bảng đang được truy cập | Có thể là alias, <derived2> (derived table từ id=2), hoặc <subqueryN>. |
partitions |
Partition nào được scan | NULL nếu bảng không partition. Nếu có, kiểm tra xem MySQL có prune đúng partition không. |
type |
Kiểu truy cập bảng — cột quan trọng nhất | Xem bảng phân tích chi tiết bên dưới. |
possible_keys |
Danh sách index có thể dùng được | MySQL liệt kê tất cả index liên quan. Nếu NULL → không có index nào phù hợp → cần tạo index. |
key |
Index thực sự được chọn | NULL = không dùng index → full table scan. Nếu possible_keys có nhưng key là NULL → optimizer quyết định scan nhanh hơn (thường do bảng nhỏ hoặc filter match quá nhiều row). |
key_len |
Số bytes của index được sử dụng | Giúp biết bao nhiêu cột trong composite index được dùng. VD: index (status, check_in) nếu key_len chỉ = 1 cột → chỉ dùng prefix status. |
ref |
Giá trị so sánh với index | const (giá trị cố định), func (kết quả function), hoặc tên cột từ bảng khác (trong JOIN). |
rows |
Số dòng ước tính phải scan | Chỉ là ước tính (dựa trên statistics). Số càng nhỏ càng tốt. Nếu hàng triệu → cần optimize. |
filtered |
% row còn lại sau khi áp dụng WHERE | rows × filtered/100 = số row thực sự trả về. filtered = 100% → filter không lọc gì thêm. filtered = 1% → 99% row bị loại sau scan → lãng phí, cần index tốt hơn. |
Extra |
Thông tin bổ sung — chứa nhiều tín hiệu quan trọng | Xem bảng phân tích chi tiết bên dưới. |
Cột type — Thứ tự hiệu suất (tốt → xấu)
| Type | Mô tả | Ví dụ điển hình | Đánh giá |
|---|---|---|---|
system |
Bảng chỉ có 1 row (system table) | Bảng metadata engine | 🟢 Tốt nhất |
const |
Match tối đa 1 row qua PRIMARY KEY hoặc UNIQUE index | WHERE id = 1 |
🟢 Rất nhanh |
eq_ref |
JOIN dùng PRIMARY KEY / UNIQUE — mỗi row bảng trước match đúng 1 row | JOIN users ON users.id = bookings.user_id |
🟢 Tốt cho JOIN |
ref |
Dùng non-unique index, có thể match nhiều row | WHERE status = 'confirmed' (index trên status) |
🟡 Tốt |
range |
Index range scan: BETWEEN, >, <, IN |
WHERE check_in BETWEEN '2024-12-01' AND '2024-12-31' |
🟡 Chấp nhận được |
index |
Full index scan — đọc toàn bộ index (nhỏ hơn full table scan) | SELECT status FROM bookings (index trên status) |
🟠 Cảnh báo |
ALL |
Full table scan — đọc toàn bộ bảng từ đầu đến cuối | Không có index phù hợp cho WHERE | 🔴 Cần optimize ngay |
Cột Extra — Các giá trị quan trọng
| Giá trị | Ý nghĩa | Tín hiệu |
|---|---|---|
Using index |
Covering index — chỉ đọc từ index, không cần truy cập bảng | 🟢 Rất tốt |
Using index condition |
Index Condition Pushdown (ICP) — filter thêm ở storage engine level | 🟢 Tốt (MySQL 5.6+) |
Using where |
Có thêm filter ở server layer sau khi đọc row | 🟡 Bình thường — kết hợp rows lớn thì cần xem lại |
Using temporary |
Tạo bảng tạm để xử lý (GROUP BY, DISTINCT, UNION) | 🔴 Chậm — xem xét thêm index hoặc rewrite query |
Using filesort |
Sort trên disk/memory — không dùng được index cho ORDER BY | 🔴 Chậm nếu tập lớn — thêm index cho ORDER BY |
Using join buffer |
Dùng buffer để JOIN (Block Nested Loop / Hash Join) | 🟠 Có thể thiếu index trên cột JOIN |
Select tables optimized away |
Query chỉ cần đọc metadata từ index (VD: MIN(id) với B-Tree) |
🟢 Tối ưu hoàn toàn |
1.2 EXPLAIN ANALYZE — Đo lường thực tế (MySQL 8.0.18+)
Khác biệt cốt lõi: EXPLAIN chỉ ước tính (không chạy query). EXPLAIN ANALYZE thực sự chạy query rồi trả về thời gian thực tế + số row thực tế ở mỗi bước.
EXPLAIN ANALYZE
SELECT b.id, b.check_in, u.name
FROM bookings b
JOIN users u ON u.id = b.user_id
WHERE b.status = 'confirmed' AND b.check_in >= '2024-12-01';
Output mẫu (tree format):
-> Nested loop inner join (cost=4.65 rows=5)
(actual time=0.089..0.234 rows=5 loops=1)
-> Index range scan on b using idx_status_checkin
(cost=2.50 rows=5)
(actual time=0.045..0.078 rows=5 loops=1)
-> Single-row index lookup on u using PRIMARY (id=b.user_id)
(cost=0.43 rows=1)
(actual time=0.028..0.029 rows=1 loops=5)
Cách đọc output:
| Thông tin | Ý nghĩa | Khi nào lo lắng |
|---|---|---|
cost=4.65 |
Chi phí ước tính của optimizer | So sánh cost giữa các phương án |
rows=5 (trong ngoặc đầu) |
Số row ước tính | So với actual rows — lệch nhiều → chạy ANALYZE TABLE |
actual time=0.045..0.078 |
Thời gian thực: first-row .. last-row (ms) | Thời gian lớn → bottleneck tại bước này |
rows=5 (actual) |
Số row thực tế trả về | Estimated vs Actual lệch xa → ANALYZE TABLE |
loops=5 |
Bước này lặp lại 5 lần | Loops lớn + time cao = N+1 ở DB level |
1.3 Các format output khác
FORMAT=JSON — chi tiết nhất, bao gồm cost breakdown:
EXPLAIN FORMAT=JSON SELECT * FROM bookings WHERE user_id = 123;
Hữu ích khi cần xem cost_info, used_key_parts (cột nào trong composite index được dùng), và attached_condition.
FORMAT=TREE — hiển thị dạng cây, dễ đọc hơn tabular:
EXPLAIN FORMAT=TREE SELECT * FROM bookings WHERE status = 'confirmed';
1.4 Workflow debug query chậm — Từng bước
- Chạy
EXPLAIN— xem execution plan - Kiểm tra
type— nếuALLhoặcindex→ cần thêm/sửa index - Kiểm tra
key— nếuNULL→ xempossible_keys, tại sao optimizer không chọn? - Kiểm tra
rows— ước tính quá lớn? So sánh với data thực tế - Kiểm tra
Extra—Using filesort/Using temporary→ thêm index cho ORDER BY / GROUP BY - Chạy
EXPLAIN ANALYZE(staging) — so sánh estimated vs actual. Lệch nhiều →ANALYZE TABLE - Thêm index / rewrite query → chạy
EXPLAINlại → so sánh
1.5 Các kỹ thuật tối ưu query phổ biến
- Tránh SELECT * — chỉ select cột cần thiết, giảm I/O, memory, và tăng cơ hội covering index
- Sử dụng index đúng cách — equality trước, range sau, covering index khi có thể (xem mục 2)
- Tránh function trên cột trong WHERE —
WHERE YEAR(created_at) = 2024→ không dùng index. Thay bằngWHERE created_at >= '2024-01-01' AND created_at < '2025-01-01' - Subquery vs JOIN — JOIN thường hiệu quả hơn correlated subquery
- EXISTS vs IN — EXISTS dừng ngay khi tìm thấy 1 kết quả, IN phải evaluate toàn bộ danh sách
- LIMIT sớm — áp dụng LIMIT trong subquery trước khi JOIN
- Batch processing — xử lý dữ liệu lớn theo chunk thay vì 1 query khổng lồ
1.6 Ví dụ thực tế (Booking System)
-- ❌ Chậm: full table scan + function on column
SELECT * FROM bookings
WHERE DATE(check_in) = '2024-12-25'
AND status = 'confirmed';
-- ✅ Nhanh: range scan + composite index
SELECT id, user_id, room_id, check_in, total_price
FROM bookings
WHERE check_in >= '2024-12-25' AND check_in < '2024-12-26'
AND status = 'confirmed';
-- Cần composite index: (status, check_in)