EXPLAIN — Đọc hiểu Execution Plan

7 phút đọc

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

  1. Chạy EXPLAIN — xem execution plan
  2. Kiểm tra type — nếu ALL hoặc index → cần thêm/sửa index
  3. Kiểm tra key — nếu NULL → xem possible_keys, tại sao optimizer không chọn?
  4. Kiểm tra rows — ước tính quá lớn? So sánh với data thực tế
  5. Kiểm tra ExtraUsing filesort / Using temporary → thêm index cho ORDER BY / GROUP BY
  6. Chạy EXPLAIN ANALYZE (staging) — so sánh estimated vs actual. Lệch nhiều → ANALYZE TABLE
  7. Thêm index / rewrite query → chạy EXPLAIN lạ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 WHEREWHERE YEAR(created_at) = 2024 → không dùng index. Thay bằng WHERE 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)

Tags

Bài viết liên quan

Đang cập nhật...