count(*)统计所有行,count(列名)仅统计该列非null值;差值即该列null行数,是sql标准行为。left join后count(右表列名)统计有效关联行数,非左表原始行数。

COUNT(列名)只数非NULL值,COUNT(*)数所有行
根本原因就一条:COUNT(*) 统计结果集里的每一行物理存在,不管任何列是否为NULL;而COUNT(列名) 仅统计该列值不为NULL的行。差值就是该列含NULL的行数——这是SQL标准强制行为,不是bug,也不是数据库配置问题。
常见现象:COUNT(*) 返回100,COUNT(id) 返回97 → 说明有3行的id列是NULL。哪怕id是主键字段,只要建表时没加NOT NULL约束,它就能存NULL。
注意:''(空字符串)、0、false都算非NULL,会被COUNT(列名)计入;只有真正的NULL被跳过;字符串'NULL'也照样计入。
LEFT JOIN后COUNT(右表.列名)“少得离谱”的真实原因
在LEFT JOIN中,右表字段匹配失败时会补NULL。此时COUNT(右表.列名)统计的其实是“左表中能成功关联到右表的行数”,不是左表原始行数。
例如:SELECT COUNT(*), COUNT(users.id) FROM orders LEFT JOIN users ON orders.user_id = users.id
-
COUNT(*)是连接后的总行数(可能因一对多膨胀) -
COUNT(users.id)只算users.id IS NOT NULL的那些行
真正想统计“有多少订单”,应该用COUNT(DISTINCT orders.id)或改写为子查询;别用COUNT(右表.列名)反推主表基数——它的语义是“有效关联数”,不是“主表行数”。
COUNT(*)通常比COUNT(列名)快,但原因常被误解
性能差异核心不在“要不要读数据”,而在“要不要判空”:
-
COUNT(*)只需确认行存在,优化器可走最小索引扫描,甚至直接查元数据(如MySQL InnoDB 8.0+在无WHERE时可能跳过聚簇索引读取) -
COUNT(列名)必须读取该列实际值,并逐行判断是否为NULL;即使该列有索引,若允许NULL,仍可能触发回表或额外空值检查 - 字段无索引 + 允许
NULL→ 几乎必然全表扫描
COUNT(1)和COUNT(*)执行计划基本一致,无需刻意替换;但如果你真要统计“某字段有效值数量”,那就必须用COUNT(列名),别指望COUNT(*)能替代。
最容易被忽略的NULL处理时机:WHERE和GROUP BY之后
COUNT(列名)统计的是当前结果集里该列的非空数量,不是原始表分布。这个“当前结果集”已经经过WHERE过滤、JOIN扩展、GROUP BY分组 —— 所以NULL判断发生在这些步骤之后。
典型静默偏差:SELECT COUNT(email) FROM users WHERE status = 'active',统计的是“活跃用户中填了邮箱的人数”,不是“全部用户中填邮箱的人数”。指标偏差不会报错,只会悄悄错。
复杂查询里一旦混淆这点,后续分析就全偏了;建议把COUNT(*) - COUNT(列名)作为例行校验项,快速定位该列的NULL占比。











