count(*)统计行存在性,不读列值;count(列名)统计该列非null值的行数,需读取并判断每行是否为null,二者语义、性能及结果均不同。

COUNT(*) 统计的是“行存在性”,不是“列值有效性”
因为 COUNT(*) 的语义是「当前结果集中有多少行」,它不检查任何列的内容,连字段读取都可能被优化器跳过。只要某行出现在最终结果里,无论所有列是不是全为 NULL,它就会计入。这不是宽容,是设计本意——* 在这里不代表“所有字段”,而是“整行存在”。
COUNT(列名) 漏计的根本原因是 NULL 的三值逻辑地位
COUNT(列名) 实际执行的是:对每一行,判断该列是否 IS NOT NULL;只有为真才加 1。而 NULL 在 SQL 中既不等于 NULL,也不等于任何值,更不是“空字符串”或“0”。所以当某行的 score 是 NULL,这一行就被跳过。
-
COUNT(*)返回 3 → 表里有 3 行物理记录 -
COUNT(score)返回 2 → 其中 1 行的score IS NULL - 差值
COUNT(*) - COUNT(score)就是该列的NULL数量,比查表结构更准
LEFT JOIN 后用 COUNT(右表.列) 等于默认加了 WHERE 右表.列 IS NOT NULL
这是最容易静默出错的场景。比如:
SELECT COUNT(*) FROM orders LEFT JOIN coupons ON orders.id = coupons.order_id;
返回的是连接后的总行数(含没领券的订单);但:
SELECT COUNT(coupons.discount) FROM orders LEFT JOIN coupons ON orders.id = coupons.order_id;
实际等价于只统计 coupons.discount IS NOT NULL 的行,也就是“成功发券的订单数”,而不是“订单总数”。
- 想统计左表原始行数?用
COUNT(DISTINCT orders.id)或子查询 - 想统计右表关联数?明确写成
COUNT(coupons.id),并确认业务是否允许id为NULL - 别依赖函数写法蒙混过关:
COUNT(1)和COUNT(*)行为一致,但改不了语义混淆
性能差异真实存在,但根源不在“NULL 判断本身”
真正影响快慢的是访问路径:
-
COUNT(*)在 InnoDB 8.0+ 中可能走元数据或最小索引(如主键),不读行数据 -
COUNT(列名)必须读取该列值来判空;若该列无索引,大概率触发全表扫描 - 即使该列有索引,若允许
NULL,优化器仍需回表确认行存在性(避免幻读)
所以不要为了“看起来快”把 COUNT(*) 改成 COUNT(1),真正要动的是索引和 NOT NULL 约束。










