COUNT()统计行存在性,不读列值;COUNT(列名)仅计非NULL值,''、0等均有效,唯独NULL被跳过;LEFT JOIN后COUNT(右表列)统计匹配行数而非用户数;COUNT()性能通常更优。

COUNT(*) 统计的是“行是否存在”,不是“字段是否为空”
只要某行出现在最终结果集中,COUNT(*) 就算它 1 次,不管 id、email、created_at 全是 NULL 还是全为 0 或空字符串。它的判断粒度是“行级存在性”,不读任何列值。
而 COUNT(列名) 必须取该列的实际值,再执行一次 IS NOT NULL 判断——只有非 NULL 才计入。注意:''、0、false、字符串 'NULL' 全部被算作有效值,唯独数据库原生的 NULL 被跳过。
常见错误现象:COUNT(*) 返回 1000,COUNT(email) 返回 823 → 差值 177 就是 email 列中真实的 NULL 数量,不是数据丢失,是字段允许 NULL 且业务没填。
LEFT JOIN 后用 COUNT(右表.列名) 会静默改变统计语义
在 SELECT ... FROM users u LEFT JOIN orders o ON u.id = o.user_id 这类查询里,COUNT(o.order_id) 不是“用户数”,也不是“订单总数”,而是“成功关联的订单行数”。1 个用户有 3 笔订单,JOIN 后生成 3 行,COUNT(o.order_id) 就返回 3。
更危险的是:如果 o.user_id 允许 NULL(比如没加 NOT NULL 约束),COUNT(o.user_id) 还会把 o.user_id IS NULL 的行也过滤掉——结果既不是左表基数,也不是右表基数,语义完全错位。
- 想统计左表原始行数:用
COUNT(DISTINCT u.id)或COUNT(*)配合GROUP BY u.id - 想统计右表匹配行数:确保该字段定义为
NOT NULL(如主键o.id),再用COUNT(o.id)
COUNT(*) 性能通常优于 COUNT(列名),尤其字段无索引或 NULL 比例高时
COUNT(*) 只需确认行存在,优化器常走最小索引(如主键 B+ 树叶子节点)甚至直接查元数据;COUNT(列名) 必须把该列值读出来,逐行判空——哪怕该列在二级索引中存在,也可能触发回表。
典型性能陷阱:
- 该列无索引 + 允许
NULL→COUNT(列名)大概率全表扫描 - 该列有索引但允许
NULL→ 优化器仍需检查每条索引项对应行是否真实存在(防幻读),无法单纯复用索引行数 -
COUNT(主键)因天然NOT NULL,执行计划可能接近COUNT(*),但语义不同,不可混用
NULL 判断发生在 WHERE 和 GROUP BY 之后,不是原始表层面
COUNT(列名) 统计的永远是当前中间结果集里的非 NULL 数量。例如带 WHERE status = 'active' 后再 COUNT(email),结果是“活跃用户中填了邮箱的人数”,不是“全部用户中填邮箱的人数”。
这个时机点最容易被忽略:指标偏差不会报错,只会静默出错。复杂查询中一旦混淆,校验时很难定位问题源头——差值法 COUNT(*) - COUNT(列名) 是唯一可靠验证手段,别依赖表结构描述或业务文档猜。

















