Giới thiệu
Trong phát triển ứng dụng, việc đếm số lượng bản ghi trong MySQL là thao tác phổ biến. Tuy nhiên, giữa COUNT(*), COUNT(1) và COUNT(tên_cột), nhiều lập trình viên thường băn khoăn: chúng khác nhau thế nào? Cách nào nhanh hơn? Nên dùng khi nào để tránh lỗi hiệu suất? Bài viết này sẽ làm rõ thông qua phân tích thực tế.
1. Kết luận tổng quan: Sự khác biệt cốt lõi
Môi trường kiểm thử: MySQL 8.0.40, sử dụng bộ máy InnoDB
Gợi ý: Dùng lệnh EXPLAIN SELECT COUNT(...) để xem kế hoạch thực thi mà bộ tối ưu hóa chọn.
Xét bảng người dùng nguoi_dung với cột email cho phép giá trị NULL:
CREATE TABLE nguoi_dung (
id INT PRIMARY KEY AUTO_INCREMENT,
ten VARCHAR(50),
email VARCHAR(100)
);
INSERT INTO nguoi_dung (ten, email) VALUES
('Zhang San', 'zhangsan@example.com'),
('Li Si', NULL),
('Wang Wu', 'wangwu@example.com'),
('Zhao Liu', NULL);
Thực hiện các truy vấn sau:
SELECT COUNT(*) FROM nguoi_dung; -- Kết quả: 4 (tất cả bản ghi)
SELECT COUNT(1) FROM nguoi_dung; -- Kết quả: 4 (tương đương COUNT(*))
SELECT COUNT(email) FROM nguoi_dung; -- Kết quả: 2 (chỉ đếm những email không NULL)
SELECT COUNT(id) FROM nguoi_dung; -- Kết quả: 4 (id là khóa chính, luôn có giá trị)
Tóm tắt sự khác biệt:
COUNT(*): Đếm toàn bộ số dòng trong bảng, bất kể giá trị cột nào có phải NULL hay không.COUNT(1): Về mặt logic và hiệu suất, hoàn toàn tương đương vớiCOUNT(*).COUNT(ten_cot): Chỉ đếm những dòng màten_cotkhông phải NULL.
2. So sánh hiệu suất: Câu lệnh nào nhanh hơn?
Nhiều người cho rằng COUNT(1) nhanh hơn COUNT(*), hoặc COUNT(khoá_chính) là lựa chọn tối ưu. Thực tế ra sao? Hãy cùng kiểm chứng.
Chuẩn bị thử nghiệm: Bảng dữ liệu lớn
CREATE TABLE bang_lon (
id INT PRIMARY KEY AUTO_INCREMENT,
du_lieu VARCHAR(255),
INDEX idx_du_lieu (du_lieu)
);
Chèn khoảng 1 triệu bản ghi để kiểm tra hiệu năng.
Thử nghiệm 1: COUNT(*) vs COUNT(1)
SELECT COUNT(*) FROM bang_lon;
SELECT COUNT(1) FROM bang_lon;
Kết quả từ EXPLAIN cho thấy cả hai đều sử dụng cùng một chiến lược quét chỉ mục, thời gian thực thi gần như bằng nhau.
Kết luận: Không có khác biệt đáng kể về hiệu suất. Bộ tối ưu hóa MySQL xử lý COUNT(*) như một trường hợp đặc biệt, thậm chí còn được ưu tiên tối ưu hơn COUNT(1). Vì vậy, nên ưu tiên dùng COUNT(*) vì tính rõ nghĩa và chuẩn mực.
Thử nghiệm 2: COUNT(*) vs COUNT(khoá_chính) vs COUNT(chỉ_mục)
SELECT COUNT(id) FROM bang_lon; -- Khóa chính
SELECT COUNT(du_lieu) FROM bang_lon; -- Cột có chỉ mục
SELECT COUNT(*) FROM bang_lon; -- Tất cả bản ghi
Kết quả EXPLAIN cho thấy tất cả đều sử dụng chỉ mục phụ idx_du_lieu để quét — điều này chứng tỏ MySQL tự động chọn đường đi ngắn nhất.
Thứ tự hiệu suất: COUNT(*) ≥ COUNT(du_lieu) > COUNT(id)
COUNT(*): Hiệu quả cao nhất. MySQL biết đây là thao tác đếm thuần túy nên bỏ qua dữ liệu gốc, chỉ duyệt chỉ mục nhỏ nhất (thường là chỉ mục phụ), đồng thời giảm tải kiểm tra MVCC.COUNT(cột_có_chỉ_mục): Khá nhanh nếu cột đó có chỉ mục, nhưng vẫn cần kiểm tra điều kiện NOT NULL.COUNT(khoá_chính): Có thể chậm hơn do liên quan đến cấu trúc index clustered, dẫn tới I/O lớn hơn khi duyệt.
3. Trường hợp sử dụng phù hợp
Trường hợp 1: Đếm tổng số bản ghi (ví dụ: phân trang)
-- ✅ Khuyên dùng
SELECT COUNT(*) FROM nguoi_dung;
-- ❌ Không cần thiết
SELECT COUNT(1) FROM nguoi_dung;
Trường hợp 2: Đếm số lượng giá trị hợp lệ của một cột
-- ✅ Đếm người dùng có email
SELECT COUNT(email) FROM nguoi_dung;
Trường hợp 3: Đếm số lượng phần tử duy nhất
-- ✅ Đếm số email khác nhau
SELECT COUNT(DISTINCT email) FROM nguoi_dung;
4. Những hiểu lầm phổ biến
Sai lầm 1: "MyISAM luôn nhanh hơn với COUNT(*)"
Đúng là MyISAM lưu sẵn số dòng, nhưng chỉ áp dụng với câu lệnh:
SELECT COUNT(*) FROM bang_myisam;
Nếu thêm điều kiện:
SELECT COUNT(*) FROM bang_myisam WHERE ten LIKE 'A%';
—> Vẫn phải quét dữ liệu theo thời gian thực, mất nhiều thời gian.
Sai lầm 2: "COUNT(khoá_chính) luôn tốt nhất"
Theo kết quả thử nghiệm, COUNT(*) thường nhanh hơn hoặc bằng COUNT(khoá_chính) do được tối ưu sâu hơn bởi engine.
5. Gợi ý lựa chọn theo ngữ cảnh
| Tình huống | Cách viết khuyên dùng | Lý do |
|---|---|---|
| Đếm tất cả bản ghi | COUNT(*) |
Rõ nghĩa, được tối ưu tốt nhất |
| Đếm số giá trị không NULL | COUNT(ten_cot) |
Tự loại bỏ NULL |
| Đếm trên cột có chỉ mục | COUNT(ten_cot) |
Hiệu suất khá nếu đã có chỉ mục |
| Đếm giá trị riêng biệt | COUNT(DISTINCT ...) |
Loại bỏ trùng lặp |
6. Tổng kết
- Dùng
COUNT(*)khi cần đếm toàn bộ bản ghi — đây là lựa chọn an toàn và hiệu quả nhất. - Tránh dùng
COUNT(cột_không_có_chỉ_mục)trên bảng lớn. - Không tin vào "kinh nghiệm truyền miệng", hãy dùng
EXPLAINđể kiểm tra kế hoạch thực thi thực tế.