COUNT(*)统计行数而非字段值,只判断行是否存在;COUNT(字段)仅统计该字段非NULL的行数,且在WHERE/GROUP BY之后计算。

COUNT(*) 统计的是行数,不是“所有字段”
很多人看到 COUNT(*) 就下意识认为它在“遍历所有列”,其实完全不是。它只关心“这一行是否存在”,哪怕整行全是 NULL,只要该行被纳入结果集,就会计入。比如:
-
SELECT COUNT(*) FROM users WHERE status = 'inactive'→ 返回匹配的行数,不管email、phone是否为NULL -
SELECT COUNT(*) FROM (SELECT NULL AS a, NULL AS b) t→ 结果是 1,因为子查询产生了一行
关键点:它不读取任何列值,优化器常走主键索引或元数据估算,所以快。
COUNT(字段) 只数该字段非 NULL 的行
COUNT(email) 不是“统计 email 这一列有多少个单元格”,而是“统计 email 值不为数据库 NULL 的行数”。空字符串 ''、数字 0、布尔 false 全部算有效值,只有真正意义上的 NULL 被跳过。
- 常见错误现象:
COUNT(*)返回 1000,COUNT(email)返回 942 → 直接说明有 58 行email IS NULL - JOIN 后尤其危险:在
LEFT JOIN orders ON u.id = o.user_id中写COUNT(o.order_id),实际统计的是“有订单的用户关联行数”,不是用户数,也不是订单总数 - 正确替代:想统计“有多少用户下过单”,应该用
COUNT(DISTINCT u.id)或COUNT(o.order_id)配合WHERE o.order_id IS NOT NULL
COUNT(*) 比 COUNT(字段) 更快,尤其当字段没索引或 NULL 多时
COUNT(*) 只需确认行存在;COUNT(email) 必须把 email 列的每个值读出来,再逐行判断是否为 NULL。这会导致:
- 如果
email没索引,大概率触发全表扫描 - 即使
id是主键,只要定义为INT NULL,COUNT(id)仍要判空,无法跳过回表 - 日志类表里
error_code字段 95% 是NULL?那COUNT(error_code)就是在主动扫一堆无效数据
现代优化器(MySQL 8.0.13+、PostgreSQL、SQL Server)对无条件 COUNT(*) 有专门路径优化,别为了“看起来更具体”去换 COUNT(1) 或 COUNT(pk) —— 它们语义没优势,还可能失去优化红利。
NULL 判断发生在 WHERE 和 GROUP BY 之后
这是最容易被忽略的一点:COUNT(email) 数的不是原始表里 email 的 NULL 分布,而是当前结果集里该列的非空数量。比如:
-
SELECT COUNT(email) FROM users WHERE created_at > '2025-01-01'→ 只看筛选后结果中email非空的行 -
SELECT department, COUNT(email) FROM users GROUP BY department→ 每组内分别统计,和整表 NULL 数量无关
换句话说,它反映的是“过滤/分组后的现场状态”,不是表结构定义。想查原始 NULL 比例,直接 SELECT COUNT(*) - COUNT(email) FROM users 最直白可靠。

















