写SQL的时候,统计行数几乎是绕不开的操作。不少人习惯性地写COUNT(1),也有人坚持用COUNT(*),还有人直接COUNT(某个字段)。大多数时候三种写法结果一样,可一旦表里出现了NULL值,COUNT(字段)返回的数字就会莫名其妙地变小,甚至和真实行数差出一大截。这篇文章就把这个问题彻底讲透。

先搞清楚COUNT的统计规则:NULL才是关键
SQL标准里,COUNT聚合函数的行为定义很明确:它统计的是“非NULL的表达式出现的次数”。也就是说,COUNT(expr)在扫描每一行时,会先对表达式expr求值,如果求值结果是NULL,这一行就不计入统计;只有结果非NULL才累加1。
理解了这一点,三种写法的差异就清晰了。COUNT(1)中的1是一个常量表达式,对任何一行求值结果都是1,永远不为NULL,所以它统计的就是物理上的总行数。COUNT(*)是SQL的一种特殊语法,数据库引擎会直接把它识别为“统计行数”,同样不会跳过任何行,这一点和COUNT(1)完全等价。
而COUNT(字段名)则完全不同,字段值可能为NULL。只要某一行该列的值是NULL,这一行就不会被计入。下面用一个简单例子验证一下:
CREATE TABLE t_user (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50),
age INT
);
INSERT INTO t_user (name, age) VALUES
('张三', 25),
('李四', NULL), -- age 为 NULL
(NULL, 30), -- name 为 NULL
('王五', 28);
SELECT COUNT(1) FROM t_user; -- 结果:4,统计所有行
SELECT COUNT(*) FROM t_user; -- 结果:4,同上
SELECT COUNT(name) FROM t_user; -- 结果:3,跳过 name 为 NULL 的那一行
SELECT COUNT(age) FROM t_user; -- 结果:3,跳过 age 为 NULL 的那一行从结果可以看到,表里明明有4行数据,COUNT(name)和COUNT(age)都只返回3。这就是很多人遇到的“COUNT结果不一致”的根本原因:不是数据库出了bug,而是NULL行被聚合函数主动过滤掉了。
三种写法的性能差异:索引选择才是重点
解决了正确性问题,再来看性能。网上流传一种说法:COUNT(1)比COUNT(*)快。这个结论在现代数据库中基本不成立。以MySQL为例,官方文档明确说明COUNT(*)会被优化为直接统计行数,它的执行效率与COUNT(1)没有任何差别,甚至某些版本中COUNT(*)的语义更明确,优化器处理起来更直接。
真正影响性能的是索引选择。InnoDB是聚簇索引存储引擎,执行COUNT时优化器会挑选一棵“最小”的二级索引来做全索引扫描,因为二级索引比主键索引(聚簇索引)包含的列更少,单个数据页能容纳更多索引项,扫描的页面数更少,IO开销自然更低。如果表上没有任何二级索引,才退回去扫描主键索引。
而COUNT(字段)的性能就复杂一些。如果该字段上恰好有索引,优化器可以利用这棵索引;如果字段没有索引且不为NOT NULL,引擎还需要回表或全表扫描去取字段值再判断NULL,代价反而更高。另外提一句,MyISAM引擎会在磁盘上直接维护总行数,不带WHERE条件的COUNT可以做到O(1),但这不代表InnoDB应该这么干——InnoDB支持事务,不同事务看到的行数可能不同,硬存一个总行数反而破坏MVCC语义。
-- 查看执行计划,确认走了哪个索引 EXPLAIN SELECT COUNT(*) FROM t_user; EXPLAIN SELECT COUNT(age) FROM t_user;
用EXPLAIN观察一下,你会在key列看到优化器实际选择的索引。想优化大表的COUNT查询,与其纠结写法,不如保证表上存在一个字段尽量短的二级索引,或者从业务层面用计数表、缓存来维护统计数。
不同场景下该怎么选:一份实用决策清单
选择哪种写法,本质上是先想清楚你要统计什么。如果目标是“表里有多少行”,也就是统计行数本身,直接用COUNT(*)。这是SQL标准写法,语义清晰,各大数据库都做了充分优化,可读性也最好,团队协作时不会产生歧义。
如果目标是“这个字段有多少个有效值”,比如统计“填写了邮箱的用户数”、“有下单记录的用户数”,那COUNT(字段)反而是正确的选择,它跳过NULL的行为恰好符合业务需求。等价写法是COUNT(CASE WHEN 字段 IS NOT NULL THEN 1 END),但显然直接COUNT字段更简洁。
还有一种容易被忽视的场景:用COUNT( DISTINCT 字段 )统计去重后的数量,它同样会忽略NULL。此外要提醒一点,空字符串和NULL不是一回事,COUNT(字段)不会跳过空字符串,如果业务上把空串当“未填写”,统计前需要先用条件过滤,否则数字依然会有偏差。
-- 统计填写了邮箱的用户数(推荐写法) SELECT COUNT(email) FROM t_user; -- 统计有效邮箱(排除空字符串) SELECT COUNT(CASE WHEN email <> '' THEN 1 END) FROM t_user; -- 统计不重复的部门数量(NULL 不参与去重统计) SELECT COUNT(DISTINCT dept_id) FROM t_user;
总结成一句话:COUNT(*)和COUNT(1)统计所有行,性能等价,随便选但推荐前者;COUNT(字段)统计非NULL值,写之前务必确认列是否允许NULL。当发现统计数字和预期不符时,第一步就执行SELECT COUNT(*) - COUNT(字段) FROM 表,差值就是NULL行的数量,问题定位往往就在这一瞬间完成。
SQL COUNT函数COUNT(1)COUNT(字段)修改时间:2026-09-14 04:54:34