Tình huống mà ai làm DBA cũng từng gặp
Tưởng tượng lúc 2 giờ sáng, bạn nhận alert: production database chậm, CPU 100%, hàng trăm request đang timeout. Mở slow query log ra, nguyên nhân hiện ra ngay — một câu SELECT chạy full table scan trên bảng 50 triệu dòng, thiếu index, được gọi 500 lần/giây từ ứng dụng.
Team dev đang ngủ. Code đang freeze để chuẩn bị release sáng hôm sau. Pipeline CI/CD tốn 30 phút để deploy. Bạn không có 30 phút — bạn cần fix ngay bây giờ.
MySQL Query Rewrite Plugin sinh ra cho những khoảnh khắc như thế này. Mình đã dùng nó để xử lý nhiều tình huống tương tự mà không cần đụng vào một dòng code nào của ứng dụng.
Query Rewrite Plugin là gì và hoạt động như thế nào
Query Rewrite Plugin được tích hợp trong MySQL 5.7+ (cần cài thủ công) và MySQL 8.0+. Nó can thiệp ở tầng parser — trước khi query được thực thi, plugin kiểm tra xem query đó có khớp với rule nào trong bảng query_rewrite.rewrite_rules không. Nếu khớp, query bị thay thế bằng phiên bản đã viết lại.
Có hai loại rewrite:
- Pre-parse rewrite (plugin
Rewriter): Hoạt động sau khi parse, dùng pattern matching với?làm wildcard cho literal values. - Post-parse rewrite: Ít phổ biến hơn, hoạt động trên AST sau khi parse.
Trên thực tế, plugin Rewriter (pre-parse) là thứ bạn sẽ dùng trong hầu hết mọi trường hợp.
Giới hạn cần biết trước
- Chỉ rewrite
SELECT,INSERT,UPDATE,DELETE— không rewrite DDL. - Pattern matching dựa trên cấu trúc query, không phải string. Hai query cùng ý nghĩa nhưng viết khác nhau sẽ cần hai rule riêng.
- Không áp dụng cho prepared statements qua binary protocol — chỉ hoạt động với text protocol.
Cài đặt và kích hoạt plugin
Trên MySQL 8.0, script cài đặt đã có sẵn:
# Chạy script cài đặt đi kèm MySQL
mysql -u root -p < /usr/share/mysql/install_rewriter.sql
# Kiểm tra plugin đã active chưa
mysql -u root -p -e "SHOW PLUGINS\G" | grep -i rewriter
Nếu thành công, output trông như sau:
Name: Rewriter
Status: ACTIVE
Type: AUDIT
Library: rewriter.so
Plugin tạo ra database query_rewrite với bảng rewrite_rules — nơi bạn định nghĩa tất cả các rule:
DESCRIBE query_rewrite.rewrite_rules;
+------------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------------+--------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| pattern | longtext | NO | | NULL | |
| pattern_database | varchar(64) | YES | | NULL | |
| replacement | longtext | NO | | NULL | |
| enabled | enum('YES', | NO | | YES | |
| message | varchar(128) | YES | | NULL | |
| pattern_digest | varchar(64) | YES | | NULL | |
| normalized_patte | varchar(100) | YES | | NULL | |
+------------------+--------------+------+-----+---------+----------------+
Thực hành: Các use case thực tế
Use case 1: Thêm LIMIT vào query không có LIMIT
Classic mistake. Developer quên LIMIT, production kéo nguyên cả bảng về — mình từng thấy một query như vậy trả 2.3 triệu dòng về frontend trong một request duy nhất:
INSERT INTO query_rewrite.rewrite_rules (pattern, replacement) VALUES (
'SELECT * FROM orders WHERE status = ?',
'SELECT * FROM orders WHERE status = ? LIMIT 1000'
);
-- Sau mỗi lần INSERT/UPDATE rule, phải flush để apply:
CALL query_rewrite.flush_rewrite_rules();
Từ đây, mọi query dạng SELECT * FROM orders WHERE status = 'pending' sẽ tự động được thêm LIMIT 1000. Ứng dụng không hay biết gì cả.
Use case 2: Force sử dụng index
MySQL optimizer đôi khi chọn sai execution plan — đặc biệt với bảng có dữ liệu lệch (skewed data). Thay vì hint trong code, rewrite thẳng tại database layer:
INSERT INTO query_rewrite.rewrite_rules (pattern, replacement, pattern_database) VALUES (
'SELECT id, email, created_at FROM users WHERE created_at > ?',
'SELECT id, email, created_at FROM users USE INDEX (idx_created_at) WHERE created_at > ?',
'myapp'
);
CALL query_rewrite.flush_rewrite_rules();
Chú ý field pattern_database. Để NULL thì rule áp dụng cho tất cả database — nghe tiện nhưng dễ gây side effect không mong muốn. Luôn chỉ định database cụ thể.
Use case 3: Chuyển hướng query sang bảng archive
Giả sử bạn đã migrate dữ liệu cũ sang orders_archive nhưng code vẫn query orders. Thay vì sửa code, redirect luôn tại đây:
INSERT INTO query_rewrite.rewrite_rules (pattern, replacement, pattern_database) VALUES (
'SELECT ? FROM orders WHERE created_at < ?',
'SELECT ? FROM orders_archive WHERE created_at < ?',
'myapp'
);
CALL query_rewrite.flush_rewrite_rules();
Use case 4: Block query nguy hiểm tạm thời
Mình từng gặp sự cố database corruption lúc 3 giờ sáng, phải restore từ backup. Mất gần 2 tiếng. Từ đó mình học được một bài: block query có thể gây hại trước khi nó chạy, đừng đợi đến lúc dọn dẹp hậu quả. Plugin có thể trả về kết quả rỗng ngay lập tức:
-- Thay thế bằng query trả về kết quả rỗng
INSERT INTO query_rewrite.rewrite_rules (pattern, replacement, pattern_database) VALUES (
'SELECT * FROM users',
'SELECT * FROM users LIMIT 0',
'myapp'
);
CALL query_rewrite.flush_rewrite_rules();
Kiểm tra rule có hoạt động không
-- Xem tất cả rule và trạng thái
SELECT id, pattern, replacement, enabled, message
FROM query_rewrite.rewrite_rules;
-- Nếu cột 'message' có giá trị sau khi flush,
-- rule bị lỗi syntax hoặc không hợp lệ
SELECT id, message FROM query_rewrite.rewrite_rules
WHERE message IS NOT NULL;
Chạy query từ ứng dụng xong, kiểm tra ngay bằng:
-- Xem warnings của query vừa chạy
SHOW WARNINGS;
-- Output nếu query được rewrite:
-- Level: Note
-- Code: 1105
-- Message: Query 'SELECT * FROM orders WHERE status = 'pending'' rewritten to
-- 'SELECT * FROM orders WHERE status = 'pending' LIMIT 1000' by a query rewrite plugin
Tắt hoặc xóa rule
-- Disable rule tạm thời
UPDATE query_rewrite.rewrite_rules SET enabled = 'NO' WHERE id = 1;
CALL query_rewrite.flush_rewrite_rules();
-- Xóa rule vĩnh viễn
DELETE FROM query_rewrite.rewrite_rules WHERE id = 1;
CALL query_rewrite.flush_rewrite_rules();
Monitoring: Biết khi nào rule được áp dụng
MySQL có sẵn status variables để theo dõi:
SHOW STATUS LIKE 'Rewriter%';
+-----------------------------------+-------+
| Variable_name | Value |
+-----------------------------------+-------+
| Rewriter_number_loaded_rules | 3 |
| Rewriter_number_rewritten_queries | 1547 |
| Rewriter_number_unrecognized_hints| 0 |
| Rewriter_reload_error | OFF |
+-----------------------------------+-------+
Rewriter_number_rewritten_queries tăng liên tục — rule đang chạy tốt. Không thay đổi dù bạn chắc chắn query đã chạy? Thì pattern chưa match. Phần lớn trường hợp là do khoảng trắng thừa, thứ tự column khác, hoặc alias.
Debug pattern không match
Trick mình hay dùng: bật general log tạm thời để xem chính xác query text đang gửi lên MySQL:
SET GLOBAL general_log = 'ON';
SET GLOBAL general_log_file = '/tmp/mysql_general.log';
-- Chạy vài query từ ứng dụng...
SET GLOBAL general_log = 'OFF';
tail -f /tmp/mysql_general.log | grep "Query"
So sánh query text trong log với pattern đã định nghĩa. Thường vấn đề nằm ở những chỗ tưởng là giống nhau mà không phải.
Khi nào nên dùng, khi nào không nên
Nên dùng khi:
- Cần hotfix production ngay lập tức, không có thời gian deploy code.
- Làm việc với third-party application mà bạn không có quyền sửa source code.
- A/B test query optimization mà không cần thay đổi application.
- Gradual migration: redirect query từ bảng cũ sang bảng mới trong khi vẫn chạy song song.
Không nên dùng khi:
- Coi đây là fix vĩnh viễn — rewrite rule là giải pháp tạm thời, không thay thế việc sửa code đúng cách.
- Query logic phức tạp, rewrite có thể thay đổi semantic.
- Môi trường nhiều team cùng dùng mà rule không được document — đây là nguồn gốc của nhiều buổi debugging bí hiểm.
Kết luận
Mình đã dùng Query Rewrite Plugin để patch production không dưới chục lần. Mỗi lần tiết kiệm ít nhất 30 phút deploy — đôi khi là cả buổi on-call. Với DBA và DevOps engineer, đây là công cụ ít người biết nhưng đáng bỏ 30 phút để nắm vững.
Chỉ cần nhớ một nguyên tắc: mọi rule đều phải được document lại — comment trong rule, hoặc ghi vào wiki nội bộ. Không có gì tệ hơn là 3 tháng sau team khác debug điên đầu vì query từ code cho ra kết quả khác hoàn toàn với những gì họ viết.

