Khi index truyền thống “đầu hàng” trước bài toán thực tế
Nhớ hồi mình còn cầm trịch hệ thống CMS cho một trang tin tức lớn, table articles cán mốc 25 triệu dòng. Cứ mỗi lần team Content chạy report lọc bài viết theo tháng là CPU server nhảy vọt lên 90%, dù cột created_at đã được đánh index cẩn thận.
SELECT * FROM articles WHERE YEAR(created_at) = 2024;
Vấn đề nằm ở chỗ: MySQL sẽ “mù” hoàn toàn với index khi bạn bọc cột vào một hàm như YEAR(). Thay vì tìm nhanh, nó phải tính toán lại giá trị cho từng dòng một trong số 25 triệu record đó. Kết quả là một cú Full Table Scan tốn tới 30-40 giây, khiến người dùng ức chế vì trang load mãi không xong.
Trước đây, mình thường phải tạo thêm một cột thật (Stored Column) chỉ để lưu giá trị năm rồi đánh index lên đó. Cách này vừa tốn storage vừa làm code rườm rà. Nhưng từ bản MySQL 8.0, Functional Indexes và Invisible Indexes đã mang đến một hướng tiếp cận gọn gàng hơn hẳn. Mình đã dùng bộ đôi này để dọn dẹp database mà không cần thay đổi cấu trúc table quá nhiều.
Functional Indexes: Đánh index cho những thứ “vô hình”
Nói đơn giản, Functional Index cho phép bạn đánh index trực tiếp lên một biểu thức hoặc hàm. Đây là cứu cánh khi bạn thường xuyên query dữ liệu qua các hàm xử lý chuỗi, thời gian hoặc dữ liệu JSON phức tạp.
Tại sao phương pháp này lại nhanh?
Thực tế, MySQL sẽ tạo ra một cột ảo ẩn (Hidden Virtual Column) và build cây index trên đó. Khi bạn query đúng biểu thức đó, Optimizer sẽ tự động bắt lấy index này thay vì quét toàn bộ bảng.
Để giải quyết bài toán lọc theo năm phía trên, mình chỉ cần chạy duy nhất một lệnh:
ALTER TABLE articles ADD INDEX idx_created_year ((YEAR(created_at)));
Mẹo nhỏ: Bạn bắt buộc phải dùng hai lớp ngoặc đơn ((...)). Nếu thiếu, MySQL sẽ báo lỗi cú pháp ngay lập tức vì không hiểu đây là một biểu thức.
Xử lý dữ liệu JSON trong chớp mắt
Giả sử bạn có 500.000 sản phẩm điện tử, mỗi sản phẩm có cấu hình RAM, CPU lưu trong cột JSON specs. Việc tìm kiếm sản phẩm có RAM ’16GB’ thường rất chậm. Với MySQL 8, hãy thử đánh index trực tiếp vào key trong JSON:
ALTER TABLE products ADD INDEX idx_ram_size ((CAST(specs->>"$.ram" AS CHAR(10))));
Sau khi áp dụng, tốc độ truy vấn từ 2 giây giảm xuống chỉ còn vài miligiây. Một sự khác biệt cực kỳ đáng kể trên môi trường thực tế.
Invisible Indexes: Chế độ “tàng hình” giúp bảo trì an toàn
Đã bao giờ bạn muốn xóa một index vì nghi ngờ nó dư thừa nhưng lại sợ xóa nhầm gây sập web chưa? Xóa index thì dễ, nhưng nếu sai lầm, việc build lại index trên table hàng chục GB có thể mất cả tiếng đồng hồ, khiến hệ thống tê liệt.
Invisible Index cho phép bạn ẩn index đó đi đối với bộ tối ưu hóa (Optimizer), nhưng MySQL vẫn âm thầm cập nhật dữ liệu cho nó khi có lệnh INSERT hay UPDATE.
Quy trình dọn dẹp database chuẩn “chuyên gia”
Thay vì xóa thẳng tay, mình luôn thực hiện theo lộ trình 3 bước an toàn:
- Ẩn index: Chuyển sang trạng thái Invisible.
ALTER TABLE orders ALTER INDEX idx_old_status INVISIBLE; - Quan sát: Theo dõi log và dashboard trong 48 giờ. Nếu không có query nào bị chậm đột ngột, index này thực sự là “rác”.
- Xóa vĩnh viễn: Lúc này mới thực hiện lệnh
DROP INDEXmột cách tự tin.
Nếu chẳng may có biến, bạn chỉ mất 0.1 giây để hiện nó lại bằng lệnh VISIBLE, nhanh hơn nhiều so với việc ngồi chờ re-build index từ đầu.
Bảng so sánh nhanh hai tính năng
| Tính năng | Điểm mạnh | Lưu ý quan trọng |
|---|---|---|
| Functional Index | Tăng tốc query chứa hàm, biểu thức hoặc JSON cực mạnh. | Làm chậm nhẹ thao tác ghi. Cần MySQL 8.0.13 trở lên. |
| Invisible Index | Thử nghiệm xóa index không rủi ro, rollback trong tích tắc. | Vẫn tốn dung lượng lưu trữ dù đang ở trạng thái ẩn. |
Kinh nghiệm thực tế từ những lần “đau thương”
Khi triển khai các loại index mới này, mình rút ra được vài quy tắc vàng:
- Đừng lạm dụng Functional Index: Bản chất nó vẫn là một cột ẩn. Nếu tạo quá nhiều, dung lượng database sẽ phình to và làm chậm các thao tác
INSERThàng loạt. - Luôn dùng EXPLAIN: Sau khi tạo index, hãy chạy
EXPLAINđể kiểm tra. Nếu kiểu dữ liệu trong query (ví dụ: chuỗi) không khớp với kiểu dữ liệu của Functional Index (ví dụ: số), MySQL sẽ phớt lờ index của bạn. - Test index mới bằng Invisible: Khi cần thêm index mới, hãy tạo nó ở trạng thái
INVISIBLE. Sau đó, bạn có thể dùng session flaguse_invisible_indexes=onđể test riêng cho mình trước khi công khai cho toàn bộ server.
Lời kết
Tối ưu database không phải là nhồi nhét thật nhiều index. Một kỹ sư giỏi là người biết dùng index đúng lúc và đúng chỗ. Functional Index giúp trị những câu query oái oăm, còn Invisible Index mang lại sự an tâm tuyệt đối khi bảo trì.
Nếu bạn đang dùng MySQL 8 mà vẫn còn loay hoay với Generated Columns thủ công hay lo sợ mỗi khi dọn dẹp database, hãy thử ngay hai tính năng này. Chắc chắn công việc quản trị của bạn sẽ nhàn hơn rất nhiều!

