前言
在 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
('张三', '[email protected]'),
('李四', NULL),
('王五', '[email protected]'),
('赵六', 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 分析执行计划才是王道。