COUNT(*)统计所有行(含NULL),语义明确且性能更优;COUNT(列名)仅统计该列非NULL行,需逐行判空,易导致结果偏少、I/O放大且语义易偏移。

COUNT(*) 统计的是行存在性,COUNT(列名) 统计的是非 NULL 值
这是语义根本差异,不是“写法偏好”问题。COUNT(*) 的结果永远等于当前事务快照下满足条件的行数(含所有 NULL 行);而 COUNT(列名) 会逐行取该列值并判断是否为 NULL,只累加非 NULL 行。哪怕字段定义为 NOT NULL,优化器也必须走取值流程——因为约束可能被绕过(如 INSERT ... SELECT 漏传、触发器动态设 NULL),SQL 标准强制要求判空。
常见静默错误包括:
-
COUNT(updated_at)在软删除表中漏计未更新记录,结果比COUNT(*)少几十行,但查询无任何提示 -
LEFT JOIN后用COUNT(user_name),实际统计的是关联成功且user_name IS NOT NULL的行,语义已偏移 - 字段允许
NULL但业务上“几乎不为空”,仍会触发全行加载,只为确认那 0.1% 的NULL
COUNT(*) 能选最窄索引,COUNT(列名) 被字段绑定死
InnoDB 是索引组织表,COUNT(*) 被优化器识别为纯行数需求,可自由选择「最窄可用索引」扫描:比如表上有 INDEX idx_status (status)(TINYINT,1 字节),它就扫这个,而不是主键索引(BIGINT,8 字节),I/O 直接降 8 倍。COUNT(列名) 没这待遇——优化器必须确保拿到该列值,所以:
- 若该列无索引,只能走聚簇索引,加载整行再提取字段(行宽 2KB 时,I/O 放大 5–10 倍)
- 若只有前缀索引(如
INDEX idx_name (name(10))),仍需回表取完整值判空,索引完全失效 - 即使该列有索引且
NOT NULL,优化器也不敢换更小的索引,路径被锁死
COUNT(*) 和 COUNT(1) 执行路径不同,但性能差在纳秒级
COUNT(*) 是 server 层不构造任何值,InnoDB 只推进游标累加;COUNT(1) 是 server 层对每行压入常量 1 再判非空(恒真),多一次栈操作。差距通常在 ±0.5ms 内,受缓冲池命中率影响远大于写法本身。但在某些 MySQL 5.7 早期版本中,COUNT(1) 曾误选较大索引,而 COUNT(*) 从未出现这类偏差——鲁棒性更高。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
别信“COUNT(id) 走主键就最快”这种经验:它仍需从主键叶子节点取出每个 id 值传给 server 层;COUNT(*) 连这个取值动作都省了。实测百万行表,二者耗时差通常 COUNT(*) 的索引选择更稳定。
真正该盯住的不是括号里填什么,而是 NULL 和索引
花时间纠结 COUNT(*) vs COUNT(1) 没意义,它们执行计划一致。真正决定快慢的是:
- 该列是否定义了
NOT NULL约束?没加就别指望优化器跳过判空 - 有没有针对
WHERE条件建联合索引?COUNT(*) WHERE status = 'active'必须有INDEX idx_status (status)才能走索引扫描 - 业务真需要
COUNT(列名)吗?先查COUNT(*) - COUNT(列名)看NULL占比,再决定是否加索引或改约束
最容易被忽略的一点:NULL 处理是 SQL 标准强制行为,不可绕过。哪怕你插入时没显式写 NULL,只要字段没设 NOT NULL,就存在被 INSERT DEFAULT 或应用层漏传导致 NULL 的风险。


















