前言
在 MySQL 中统计行数是开发中最常见的需求之一,但面对 count(*)、count(1)、count(字段) 这些写法,很多开发者会感到困惑:它们到底有什么区别?哪种方式性能更好?如何避免踩坑?本文将通过实例来验证这些问题。
一、先看结论:它们到底有什么区别?
测试环境:MySQL 8.0.40,InnoDB 引擎
提示:使用
EXPLAIN SELECT COUNT(...)可以查看优化器选择的执行计划。
我们从一个简单的例子开始。假设有一张用户表 user,其中包含一个可为空的字段 email:
sql
CREATE TABLE user (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50),
email VARCHAR(100)
);
-- 插入测试数据
INSERT INTO user (name, email) VALUES
('张三', 'zhangsan@example.com'),
('李四', NULL),
('王五', 'wangwu@example.com'),
('赵六', NULL);
执行以下查询:
sql
SELECT COUNT(*) FROM user; -- 结果:4(统计所有行数)
SELECT COUNT(1) FROM user; -- 结果:4(统计所有行数)
SELECT COUNT(email) FROM user; -- 结果:2(只统计 email 不为 NULL 的行)
SELECT COUNT(id) FROM user; -- 结果:4(统计主键非 NULL 的行)
📌 核心区别总结:
COUNT(*):统计表中所有行的数量,不关心具体列的值是否为 NULL。COUNT(1):与COUNT(*)完全等价,性能几乎无差异。COUNT(字段):只统计该字段不为 NULL 的行数。
二、性能对比:哪种写法更快?
很多人认为 COUNT(1) 比 COUNT(*) 快,或者 COUNT(主键) 是最优选择。但实际情况如何?我们通过实验验证。
实验准备:创建百万级数据表
sql
CREATE TABLE big_table (
id INT PRIMARY KEY AUTO_INCREMENT,
data VARCHAR(255),
index idx_data (data)
);
随机生成100w 数据
测试 1:COUNT(*) vs COUNT(1)
sql
SELECT COUNT(*) FROM big_table;
SELECT COUNT(1) FROM big_table;
执行查询结果,如下图:
✅ 结论 :两者性能几乎完全一致,MySQL 优化器对它们的处理方式相同(COUNT(*) 是 MySQL 内部优先优化的查询模式,优化程度超过 COUNT(1) ,在阅读性上也比更好些)。推荐优先使用COUNT(*)。
测试 2:COUNT(*) vs COUNT(主键) vs COUNT(索引字段)
sql
-- 使用主键索引
SELECT COUNT(id) FROM big_table;
-- 使用普通索引字段
SELECT COUNT(data) FROM big_table;
-- 全表扫描
SELECT COUNT(*) FROM big_table;
执行查询结果,如下图:
执行SQL EXPLAIN,三条SQL的结果都是一样 ,如下图:
✅ 结论 : MySQL对执行查询都有优化,查询索引优化为 (idx_data)二级索引 ,最终结果:COUNT(*) > COUNT(主键) > COUNT(索引字段)
COUNT(*)性能最优 :MySQL 对COUNT(*)进行了特殊优化,COUNT(*)不会真的取出所有列的数据,而是会自动选择最优的索引 进行扫描,避免不必要的 IO,同时避免了一些事务和MVCC 机制的部分检查。COUNT(主键)比COUNT(*)稍慢:主键索引(聚簇索引)天然包含数据行,统计时需要读取完整的索引结构(包含所有行数据),也可能MySQL可能优化为查询其他的二级索引。COUNT(普通索引)性能较慢:需要过滤非NULL值,可能需要回表。
三、使用场景:如何正确选择?
场景 1:统计所有行数(如分页总条数)
sql
-- ✅ 推荐写法
SELECT COUNT(*) FROM user;
-- ❌ 不推荐(性能无提升,可读性略差一点)
SELECT COUNT(1) FROM user;
场景 2:统计某字段的有效值数量
sql
-- 统计有邮箱的用户数量(自动忽略为null的数据)
SELECT COUNT(email) FROM user;
场景 3:统计去重后的数量
sql
-- 统计不重复的邮箱数量
SELECT COUNT(DISTINCT email) FROM user;
四、常见误区与避坑指南
误区 1:"MyISAM 的 COUNT(*) 一定快"
虽然 MyISAM 会缓存表的总行数,但仅适用于没有 WHERE 条件的情况:
sql
-- MyISAM 引擎下,瞬间返回(缓存)
SELECT COUNT(*) FROM user;
-- 带上 WHERE 条件后,依然需要实时统计
SELECT COUNT(*) FROM user WHERE name LIKE '张%';
误区 2:"COUNT(主键) 是最优解"
通过前文的测试可见,COUNT(主键) 的性能可能不如 COUNT(*)。
五、选择建议
| 场景 | 推荐写法 | 原因 |
|---|---|---|
| 统计所有行数 | COUNT(*) |
语义清晰,性能最优 |
| 统计非 NULL 值数量 | COUNT(字段) |
自动过滤 NULL |
| 需要最高性能统计和具体业务字段 | COUNT(索引字段) |
利用索引快速统计 |
| 统计去重后的数量 | COUNT(DISTINCT) |
DISTINCT去重 |
六、总结
COUNT(*)是通用场景的最佳选择,既符合直觉又有良好性能。- 避免使用
COUNT(非索引字段),尤其是大表。 - 不要盲目相信"经验之谈",用 EXPLAIN 分析执行计划才是王道。