MySQL COUNT(*) Chậm? Đừng Để Dashboard ‘Treo’ Khi Data Chạm Mốc Triệu Dòng

MySQL tutorial - IT technology blog
MySQL tutorial - IT technology blog

Bối cảnh: Cú lừa mang tên COUNT(*)

Ngày đầu làm dự án, mình từng dính một “cú lừa” kinh điển. Task yêu cầu hiển thị tổng số đơn hàng trên dashboard của một trang E-commerce. Mình tự tin gõ SELECT COUNT(*) FROM orders rồi đẩy thẳng lên production. Kết quả? Mọi thứ êm đẹp cho đến khi bảng orders chạm mốc 10 triệu dòng. Dashboard bắt đầu xoay vòng vô tận. Cuối cùng, hệ thống báo lỗi Timeout trắng xóa.

Vấn đề nằm ở chỗ: InnoDB không lưu tổng số dòng trong metadata như MyISAM. MyISAM chỉ mất 0.00s để trả về kết quả vì nó đọc con số có sẵn. Ngược lại, để đảm bảo tính nhất quán (MVCC – Multi-Version Concurrency Control), InnoDB phải thực hiện quét dữ liệu. Database cần đếm xem tại đúng thời điểm đó, có bao nhiêu dòng thực sự tồn tại với transaction của bạn.

Nếu bạn đang làm tính năng phân trang (pagination) cho các bảng dữ liệu lớn, việc tối ưu COUNT(*) là sống còn. Đừng để server database phải “gồng mình” gánh I/O mỗi khi người dùng F5 trang web.

Mô phỏng: Khi 5 triệu dòng làm khó Database

Để thấy rõ sự khác biệt, hãy thử tạo một bảng log đơn giản. Chúng ta sẽ nạp khoảng 5 triệu bản ghi ảo để kiểm chứng hiệu năng.

-- Tạo bảng test
CREATE TABLE logs_test (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT,
    action VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

-- Sau khi nạp 5 triệu dòng, hãy thử chạy lệnh đếm:
SELECT COUNT(*) FROM logs_test;

Trên một server cấu hình tầm trung (2 vCPU, 4GB RAM), câu lệnh này có thể tốn từ 3 đến 8 giây. Khi dùng EXPLAIN, bạn sẽ thấy MySQL phải thực hiện index scan. Đây là một thao tác cực kỳ tốn kém tài nguyên.

EXPLAIN SELECT COUNT(*) FROM logs_test;

4 Chiến thuật lấy số dòng “tức thì”

Sau nhiều đêm thức trắng xử lý sự cố server treo lúc 2 giờ sáng, mình đúc kết được 4 cách tiếp cận tùy theo bài toán cụ thể.

1. Tận dụng Secondary Index (Chỉ số phụ)

MySQL khá thông minh khi xử lý COUNT(*). Nó sẽ chọn index có kích thước nhỏ nhất để quét thay vì dùng Primary Key (thường rất nặng). Nếu bảng chỉ có cột id, hãy thử thêm một index vào cột kiểu TINYINT hoặc INT.

-- Thêm index nhỏ để MySQL quét nhanh hơn
ALTER TABLE logs_test ADD INDEX idx_user_id (user_id);

Cách này giúp tốc độ tăng khoảng 2-3 lần. Tuy nhiên, nó vẫn là độ phức tạp O(N). Với bảng hàng trăm triệu dòng, đây vẫn chưa phải là cứu cánh cuối cùng.

2. Dùng Metadata (Chấp nhận sai số)

Thực tế, không phải lúc nào người dùng cũng cần con số chính xác đến từng đơn vị. Nếu bạn chỉ cần hiển thị “Có khoảng 1.2 triệu kết quả”, hãy hỏi information_schema.

SELECT TABLE_ROWS 
FROM information_schema.tables 
WHERE table_name = 'logs_test' 
AND table_schema = 'your_db_name';

Ưu điểm: Kết quả trả về gần như 0ms.
Nhược điểm: Sai số có thể lên tới 10-20%. Con số này chỉ là ước lượng từ bộ tối ưu hóa (Optimizer) của InnoDB.

3. Kỹ thuật Counter Table (Bảng đếm riêng)

Đây là giải pháp “vàng” cho các hệ thống cần độ chính xác 100% nhưng yêu cầu tốc độ cao. Bạn tạo một bảng riêng chỉ để lưu tổng số dòng của các bảng quan trọng.

CREATE TABLE table_counts (
    table_name VARCHAR(100) PRIMARY KEY,
    total_rows BIGINT DEFAULT 0
);

Sử dụng Triggers để tự động cập nhật con số này. Mỗi khi INSERT, bạn cộng thêm 1; khi DELETE, bạn trừ đi 1. Lúc này, việc lấy tổng số dòng chỉ đơn giản là một câu lệnh SELECT theo Primary Key, tốc độ đạt mức miligiây.

4. Dùng Redis để đếm (High Write Load)

Nếu hệ thống của bạn nhận hàng ngàn lượt ghi mỗi giây, Trigger có thể gây ra hiện tượng nghẽn cổ chai (lock contention). Lúc này, hãy đẩy gánh nặng sang Redis. Sử dụng các lệnh nguyên tử như INCRDECR của Redis để quản lý bộ đếm. Sau đó, định kỳ 5 phút một lần, bạn đồng bộ con số này vào database để dự phòng.

Lời kết

Đừng bao giờ đặt niềm tin tuyệt đối vào COUNT(*) khi dữ liệu đang lớn dần theo thời gian. Hãy luôn kiểm tra Slow Query Log thường xuyên. Nếu câu lệnh đếm xuất hiện trong log, đó là tín hiệu đỏ cho thấy bạn cần thay đổi chiến thuật index hoặc chuyển sang dùng Counter Table. Hãy chọn giải pháp phù hợp ngay từ khâu thiết kế để tránh những cuộc gọi khẩn cấp lúc nửa đêm.

Share: