概述
在 SQL 查询中,COUNT 函数是最常用的聚合函数之一,用于统计行数。然而,很多开发者会对 COUNT(*) 和 COUNT(1) 的区别感到困惑,甚至在一些老教程中还能看到'COUNT(1) 比 COUNT(*) 更快'的说法。本文将深入探讨这两者的本质、性能差异以及正确的使用场景。
一、基本概念
COUNT(*)
COUNT(*) 统计表中的所有行数,包括值为 NULL 的行。这里的星号并不代表'所有列',而是一个特殊的语法,表示'整个行'。
COUNT(1)
COUNT(1) 统计表达式 1 不为 NULL 的行数。由于常量 1 永远不为 NULL,所以它同样会统计表中的所有行,结果与 COUNT(*) 完全一致。
示例:
假设有一张用户表 users,包含 3 行数据,其中一行的 email 字段为 NULL:
SELECT COUNT(*) FROM users; -- 返回 3
SELECT COUNT(1) FROM users; -- 返回 3
SELECT COUNT(email) FROM users; -- 返回 2(忽略 NULL)
二、性能对比:现代 MySQL 中无差异
在 MySQL 5.7 及更高版本中,优化器会将 COUNT(1) 自动重写为 COUNT(*),因此两者的执行计划完全相同。我们可以通过 EXPLAIN 来验证:
EXPLAIN SELECT COUNT(*) FROM users;
EXPLAIN SELECT COUNT(1) FROM users;
两者的输出结果(如 id、select_type、Extra 等字段)没有任何区别。这意味着它们在性能上是完全等价的,不存在谁比谁快的问题。
为什么早期会有'COUNT(1) 更快'的说法?
在非常古老的 MySQL 版本(如 3.23)中,COUNT(*) 的实现方式可能涉及读取所有列,而 COUNT(1) 只需读取常量,因此确有性能差异。但这个时代早已结束,现代优化器对 COUNT(*) 做了专门优化,它会选择最优的索引(通常是二级索引)来统计行数,而不是全表扫描。
三、关键区别:COUNT(列名) 与 COUNT(*)/COUNT(1)
真正需要区分的是 COUNT(列名) 与 COUNT(*)/COUNT(1),它们的行为有本质差异:
| 函数 | 统计对象 | 是否包含 NULL |
|---|---|---|
COUNT(*) | 所有行 | 是 |
COUNT(1) | 所有行(因为 1 非 NULL) | 是 |
COUNT(列名) | 该列非 NULL 的行 | 否 |
示例验证:
-- 创建测试表
CREATE TABLE t (id INT, name VARCHAR(10));
INSERT INTO t VALUES (1, 'Alice'), (2, NULL), (NULL, 'Bob');
SELECT COUNT(*) FROM t; -- 3
SELECT COUNT(1) FROM t; -- 3
SELECT COUNT(id) FROM t; -- 2(id 为 NULL 的行被忽略)
SELECT COUNT(name) FROM t; -- 2(name 为 NULL 的行被忽略)
四、存储引擎的影响
不同的存储引擎对 COUNT 的处理略有不同,但这并不影响 COUNT(*) 与 COUNT(1) 的等价性。
- MyISAM:会缓存表的总行数,因此不带
WHERE条件的COUNT(*)极快,直接返回缓存值。COUNT(1)同样利用缓存,两者速度一致。 - InnoDB:由于支持 MVCC(多版本并发控制),不同事务看到的数据版本可能不同,因此无法缓存总行数。每次
COUNT(*)都需要实时统计,优化器会优先选择扫描最小的二级索引来减少 IO。同样,COUNT(1)也采用相同的优化策略。
五、最佳实践建议
- 统计总行数:推荐使用
COUNT(*),因为它最直观地表达了'统计所有行'的意图,且没有性能损失。 - 统计非 NULL 值的行数:使用
COUNT(列名)。 - 带条件统计:一律使用
COUNT(*)配合WHERE子句,如SELECT COUNT(*) FROM table WHERE status=1;。 - 代码一致性:团队内统一使用
COUNT(*),避免混用COUNT(1)引起不必要的困惑。
六、结论
在当今的 MySQL 版本中,COUNT(*) 和 COUNT(1) 没有任何区别——无论是语义还是性能。它们都统计所有行,执行计划相同,运行速度相同。开发者无需再纠结选择哪一个,只需记住与 COUNT(列名) 区分即可。
如果你还看到有人争论'COUNT(1) 比 COUNT(*) 快',可以友善地告诉他:那是上个世纪的故事了。😄
延伸阅读:

