count(列名)只统计非null值,是sql标准语义而非bug;count(*)统计所有行,count(列名)仅计该列非null行,空字符串、0、false均不被排除,仅数据库null被跳过;join后对右表字段使用count(列名)会误统计关联行数而非主表基数。

COUNT(列名)只统计非NULL值,不是“漏数”而是语义明确
结果不同不是bug,是SQL标准强制行为:COUNT(*)统计结果集里的所有行,COUNT(列名)只统计该列值不为NULL的行。哪怕整行其他字段全非空,只要目标列是NULL,这行就被跳过。
常见现象:COUNT(*)返回100,COUNT(email)返回92 → 说明有8行email为NULL。注意:''(空字符串)、0、false都算非NULL,只有数据库层面真正的NULL被排除。
差值就是该列NULL数量,比查表结构或翻业务文档更直接可靠。
JOIN后COUNT(列名)容易误判主表基数
在LEFT JOIN中对右表字段(如orders.user_id)用COUNT(orders.user_id),实际统计的是“成功关联的行数”,不是左表原始用户数。
例如1个用户关联3个订单,JOIN后变成3行;COUNT(orders.user_id)会返回3,但你想知道的是“有多少用户”,正确写法是COUNT(DISTINCT users.id)。
若右表字段本身允许NULL(比如外键没加NOT NULL约束),COUNT(orders.user_id)还会进一步剔除user_id IS NULL的行——结果既不是用户数,也不是订单总数,语义完全错位。
COUNT(列名)性能比COUNT(*)差,尤其当列无索引或NULL比例高时
COUNT(*)只需确认行存在性,优化器常走最小索引(如主键)甚至元数据估算;COUNT(列名)必须读取该列值并逐行判空,尤其当该列允许NULL且无索引时,大概率触发全表扫描。
这些隐式开销比函数写法本身影响更大:
- 字段未加
NOT NULL约束 - 该列没有索引
- 表中
NULL占比高(比如日志表里error_code只在出错时有值)
即使id是主键,只要它允许NULL,COUNT(id)仍要判空,无法完全跳过回表。
COUNT(*)和COUNT(1)在主流引擎中性能几乎一致,别为“看起来快”改写
MySQL 8.0+、PostgreSQL、SQL Server等都会将COUNT(*)和COUNT(1)优化为相同执行计划。MySQL官方文档明确说二者处理方式完全相同。
MyISAM表对COUNT(*)有特殊缓存优化(直接读表头行数),而COUNT(1)不一定享受同等待遇;InnoDB虽无缓存,但从8.0.13起也对无条件COUNT(*)做了扫表路径优化。
所以优先用COUNT(*):它是SQL92标准语法,语义最清晰,且现代优化器最熟悉它。
真正容易被忽略的一点:NULL判断发生在WHERE和GROUP BY之后。COUNT(列名)反映的是当前结果集里该列的非空数量,不是原始表分布。复杂查询中一旦混淆这点,指标偏差不会报错,只会静默出错。











