Hiệu năng cơ sở dữ liệu là yếu tố then chốt trong các hệ thống có lưu lượng truy vấn cao. Với MySQL — giải pháp RDBMS phổ biến nhất — việc tối ưu không chỉ nằm ở cấu hình server hay phần cứng, mà bắt đầu từ những quyết định thiết kế cấp ứng dụng: kiểu dữ liệu, cấu trúc bảng, chiến lược lập chỉ mục và cách viết truy vấn.
Tối ưu kiểu dữ liệu
Mục tiêu cốt lõi là giảm thiểu chi phí lưu trữ và I/O: kiểu nhỏ hơn → ít byte hơn → ít bộ nhớ đệm hơn → tốc độ quét nhanh hơn. Dưới đây là một số nguyên tắc thực tiễn:
- Số điện thoại: Thay vì
VARCHAR(15)(30–45 byte với UTF-8), nên dùngBIGINT UNSIGNED(8 byte). Lưu ý: loại bỏ ký tự như+,-, khoảng trắng trước khi lưu. - Địa chỉ IP v4: Dùng
INT UNSIGNED(4 byte) kết hợp hàmINET_ATON()vàINET_NTOA(). Tránh dùngVARCHAR(15)gây lãng phí 11–15 byte. - Giá trị phân loại nhỏ: Với tuổi, trạng thái (active/inactive), loại sản phẩm… nên ưu tiên
TINYINT UNSIGNED(1 byte, phạm vi 0–255). - Chuỗi cố định: Dùng
CHAR(N)cho dữ liệu có độ dài không đổi như mã quốc gia (CHAR(2)), mã băm MD5 (CHAR(32)) hoặc UUID (CHAR(36)). TránhVARCHARkhi không cần tiết kiệm không gian. - Thời gian:
TIMESTAMP(4 byte) thường hiệu quả hơnDATETIME(8 byte) nếu yêu cầu thời gian nằm trong khoảng1970–2038. Lưu ý:TIMESTAMPtự động chuyển đổi theo múi giờ, cònDATETIMElưu giá trị nguyên bản.
Tối ưu thao tác trên bảng
Khi bảng đạt kích thước lớn (>10M dòng), các lệnh ALTER TABLE trở thành điểm nghẽn nghiêm trọng do MySQL tạo bảng tạm, sao chép toàn bộ dữ liệu rồi đổi tên — kèm theo khóa toàn bộ bảng.
Hai phương án an toàn:
- Triển khai ngoại tuyến: Sao chép cơ sở dữ liệu sang máy test, thực hiện
ALTER, sau đó đồng bộ dữ liệu mới sinh trong thời gian chờ bằng script dựa trên cộtcreated_athoặcupdated_at. - Chiến lược "bảng bóng" (shadow table):
-- Bước 1: Tạo bảng mới với cấu trúc cập nhật CREATE TABLE users_v2 LIKE users; -- Bước 2: Thêm chỉ mục, thay đổi kiểu cột, v.v. ALTER TABLE users_v2 MODIFY COLUMN phone BIGINT UNSIGNED; -- Bước 3: Sao chép dữ liệu theo từng khối để tránh lock lâu INSERT INTO users_v2 SELECT * FROM users WHERE id BETWEEN 1 AND 100000; -- ... lặp lại cho các khối tiếp theo -- Bước 4: Đổi tên (atomic operation) RENAME TABLE users TO users_v1, users_v2 TO users;Công cụ như gh-ost hoặc twin tự động hóa toàn bộ quy trình này.
Tối ưu chỉ mục
Với bộ máy InnoDB (dùng B+Tree), chỉ mục không chỉ hỗ trợ tìm kiếm nhanh mà còn ảnh hưởng trực tiếp đến chi phí đọc đĩa và bộ nhớ đệm.
1. Tránh tính toán trên cột có chỉ mục
Các truy vấn như WHERE YEAR(created_at) = 2024 hoặc WHERE price * 1.1 > 100 khiến MySQL không thể sử dụng chỉ mục. Thay vào đó, viết lại điều kiện để giữ cột "nguyên vẹn": WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01'.
2. Chỉ mục tiền tố (prefix index)
Khi cột VARCHAR quá dài (ví dụ: description 1000 ký tự), lập chỉ mục toàn bộ sẽ tốn nhiều dung lượng và làm chậm truy vấn. Giải pháp là chỉ lập chỉ mục trên n ký tự đầu tiên — nhưng phải đảm bảo tính chọn lọc (selectivity) gần bằng toàn bộ cột.
Để xác định n, chạy các truy vấn sau:
-- Tính selectivity toàn bộ cột
SELECT COUNT(DISTINCT first_name) / COUNT(*) AS full_selectivity FROM users;
-- So sánh với các độ dài tiền tố
SELECT
COUNT(DISTINCT LEFT(first_name, 3)) / COUNT(*) AS len3,
COUNT(DISTINCT LEFT(first_name, 4)) / COUNT(*) AS len4,
COUNT(DISTINCT LEFT(first_name, 5)) / COUNT(*) AS len5
FROM users;
Nếu len5 đạt ≥95% giá trị full_selectivity, thì tạo chỉ mục tiền tố 5 ký tự:
ALTER TABLE users ADD INDEX idx_first_name_5 (first_name(5));
3. Thứ tự cột trong chỉ mục ghép
Chỉ mục (last_name, first_name) KHÔNG tương đương (first_name, last_name). Vì B+Tree sắp xếp theo thứ tự từ trái sang phải, nên cột có tính chọn lọc cao hơn nên đặt ở vị trí đầu.
Đo selectivity từng cột:
SELECT COUNT(DISTINCT last_name) / COUNT(*) AS last_name_sel FROM users;
SELECT COUNT(DISTINCT first_name) / COUNT(*) AS first_name_sel FROM users;
Nếu first_name_sel > last_name_sel, ưu tiên chỉ mục (first_name, last_name).
4. Chỉ mục bao phủ (covering index)
Khi tất cả cột trong mệnh đề SELECT và WHERE đều nằm trong một chỉ mục duy nhất, MySQL không cần truy cập bảng gốc — gọi là "covering index". Điều này loại bỏ hoàn toàn thao tác "lookup" qua khóa chính (clustered index), giảm đáng kể I/O.
Ví dụ: Nếu truy vấn thường xuyên là SELECT email, status FROM users WHERE status = 'active', hãy tạo chỉ mục:
ALTER TABLE users ADD INDEX idx_status_email (status, email);
Kết quả EXPLAIN sẽ hiển thị Using index — dấu hiệu rõ ràng của chỉ mục bao phủ.
Tối ưu truy vấn
Quy trình xử lý truy vấn của MySQL gồm: phân tích cú pháp → kiểm tra cache → tối ưu hóa kế hoạch thực thi → gọi storage engine → trả kết quả. Việc tối ưu tập trung vào hai điểm: giảm số dòng được quét và tăng tỷ lệ dòng trả về so với dòng quét.
Phát hiện truy vấn chậm
- Kích hoạt slow query log:
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 2.0; - Xem danh sách truy vấn chậm nhất trong file log hoặc thông qua
performance_schema.events_statements_summary_by_digest.
Một số kỹ thuật tối ưu điển hình
• Đếm có điều kiện
Thay vì SELECT COUNT(*) FROM orders WHERE status IN ('shipped', 'delivered'), dùng:
SELECT
SUM(status = 'shipped') AS shipped,
SUM(status = 'delivered') AS delivered
FROM orders;
Cách này tận dụng khả năng đánh giá biểu thức boolean trong MySQL (trả về 1/0), tránh quét hai lần.
• Phân nhóm hiệu quả
Tránh GROUP BY trên cột không có chỉ mục. Nếu liên kết nhiều bảng, ưu tiên nhóm theo khóa ngoại đã được lập chỉ mục:
-- Không hiệu quả
SELECT a.name, COUNT(*)
FROM orders o
JOIN accounts a ON o.account_id = a.id
GROUP BY a.name;
-- Hiệu quả hơn (giả sử account_id đã có chỉ mục)
SELECT a.name, COUNT(*)
FROM orders o
JOIN accounts a ON o.account_id = a.id
GROUP BY o.account_id;
• Phân trang với offset lớn
Truy vấn LIMIT 100000, 20 buộc MySQL quét 100.020 dòng rồi mới trả 20 dòng cuối. Cách thay thế là "keyset pagination":
-- Lần đầu: lấy 20 bản ghi đầu
SELECT id, title FROM articles ORDER BY id DESC LIMIT 20;
-- Lần sau: dùng id của bản ghi cuối làm mốc
SELECT id, title FROM articles
WHERE id < 123456
ORDER BY id DESC LIMIT 20;
Phương pháp này có độ phức tạp O(1) cho mỗi trang, không phụ thuộc vào tổng số bản ghi.