count(*)统计所有行(含null),不受列值影响;count(列名)仅统计该列非null行,遇null则跳过,易导致结果偏少且性能更差。

统计总行数时,COUNT(*) 不受 NULL 值干扰
COUNT(*) 统计的是结果集的物理行数,完全不检查任何列的内容。哪怕所有字段都是 NULL,只要这一行被 WHERE 或 JOIN 保留下来,它就算一个。而 COUNT(列名) 必须逐行判断该列是否为 NULL,一旦列里有 NULL(比如未填写的 email、历史迁移缺失的 user_id),结果就会比实际行数少。
常见错误现象:COUNT(email) 返回 1200,COUNT(*) 却是 1500 → 说明有 300 行的 email 是 NULL。如果误用前者做分页总数或数据一致性校验,逻辑就错了。
MySQL InnoDB 对 COUNT(*) 有专属优化路径
在无 WHERE 条件的全表统计场景下,MySQL 8.0+ 的 InnoDB 会自动选择最小索引(通常是主键)扫描,甚至跳过读取行数据本身,只遍历索引页的记录数。而 COUNT(列名) 即使该列有索引,也得确认每条索引项对应的真实行是否有效、该列值是否为 NULL,无法跳过值检查。
性能差异在以下情况更明显:
- 列允许
NULL且无索引 →COUNT(列名)很可能触发全表扫描 - 列有索引但
NULL比例高(如 70%)→ 索引覆盖无效,仍要回表判空 - 使用
LEFT JOIN后统计左表行数 →COUNT(*)数的是连接后行数,但至少语义明确;而COUNT(右表.列)会把没匹配上的NULL行全剔除,结果既不是左表数也不是关联数
COUNT(列名) 的语义容易被误读,尤其在 JOIN 场景
写 SELECT COUNT(*) FROM orders LEFT JOIN users ON orders.user_id = users.id,返回的是“连接产生的总行数”,不是订单数——如果某订单因测试数据异常匹配了 3 个用户,它就被算 3 次。这时若改用 COUNT(users.id),反而会漏掉那些没关联用户的订单(因为 users.id 为 NULL 被跳过),结果变成“有多少订单成功关联了用户”,和原始需求彻底偏离。
真正想统计左表原始行数,必须用:
COUNT(DISTINCT orders.id)- 或先子查询
SELECT COUNT(*) FROM (SELECT DISTINCT id FROM orders) t - 绝不能依赖
COUNT(*)在 JOIN 后的结果,除非你清楚知道它数的是什么
别指望优化器自动等价替换
即使你确定 id 列是主键、NOT NULL,写 COUNT(id) 也不等于 COUNT(*)。MySQL 优化器不一定能把两者执行计划合并,尤其在复杂查询中。而 COUNT(*) 是 SQL 标准里明确定义“统计行数”的唯一函数,所有主流数据库(MySQL、PostgreSQL、SQL Server)都对其做了深度适配。
最容易被忽略的一点:NULL 处理是强制的、不可关闭的。哪怕你给列加了 DEFAULT 'N/A',只要没加 NOT NULL 约束,显式插入 NULL 就依然会被 COUNT(列名) 跳过。上线前用 SELECT COUNT(*), COUNT(列名), COUNT(*) - COUNT(列名) FROM 表 查一遍 NULL 占比,比事后排查逻辑错成本低得多。











