COUNT(*)统计所有行,COUNT(列名)仅统计该列非NULL行,故后者恒≤前者;因NULL判断机制、LEFT JOIN影响及性能差异,二者语义与结果均不同。

COUNT(*) 和 COUNT(列名) 结果不一致,根本不是 bug,而是 SQL 标准强制要求的行为:前者统计所有行,后者只统计该列非 NULL 的行。
为什么 COUNT(列名) 总是 ≤ COUNT(*)
因为 COUNT(列名) 在执行时会对每一行做一次 IS NOT NULL 判断,只有该列值不为 NULL 才计入。而 COUNT(*) 完全不看列内容,只要行存在就算一个。
- 常见现象:
COUNT(id)返回 98,COUNT(*)返回 100 → 说明有 2 行的id列值为NULL(哪怕它是主键字段,只要没加NOT NULL约束,就可能存NULL) -
''(空字符串)、0、false都不是NULL,COUNT(列名)会把它们全算进去 -
'NULL'(字符串字面量)也不是NULL,同样会被计入
COUNT(列名) 在 LEFT JOIN 后更危险
LEFT JOIN 会让右表字段在匹配失败时变成 NULL,这时 COUNT(右表.列名) 实际统计的是“成功关联的行数”,而不是左表原始行数。
- 例如:
SELECT COUNT(*) FROM orders LEFT JOIN users ON orders.user_id = users.id→ 结果是连接后总行数,可能因一对多膨胀而大于订单数 - 而
COUNT(users.id)会跳过所有users.id IS NULL的行,结果可能远小于COUNT(*) - 真正想统计“有多少订单”,应该用
COUNT(DISTINCT orders.id)或先子查询聚合再 JOIN
性能差异真实存在,但常被误判
COUNT(*) 在现代数据库(MySQL InnoDB 8.0+、PostgreSQL 等)中通常走最小索引或元数据优化;COUNT(列名) 则必须读取该列并逐行判空,开销略高。
- 如果该列无索引,
COUNT(列名)很可能触发全表扫描 - 即使该列有索引,若允许
NULL,优化器仍需检查索引项对应行是否真实存在(避免幻读),不能简单复用索引行数 -
COUNT(主键)因主键列天然NOT NULL,某些场景下执行计划接近COUNT(*),但语义不同,不可混用
怎么快速验证某列有多少 NULL 值
别查表结构、别猜业务逻辑,直接用差值法:
SELECT COUNT(*), COUNT(email), COUNT(*) - COUNT(email) AS null_count FROM users;
这个 null_count 就是 email 列中 NULL 的真实数量。一旦发现差值 > 0,就说明 COUNT(email) 绝对不能代表总记录数——这点在指标口径对齐和报表校验时最容易被跳过。

















