COUNT(*)性能优于COUNT(列名)的根本原因是执行路径不同:前者仅需确认行存在性,可走最小索引的存在性扫描,不回表、不读字段;后者必须逐行读取并判空该列值,若列允许NULL且无索引,则触发全表扫描和列解码,I/O与CPU开销显著上升。

COUNT(*) 比 COUNT(列名) 性能更好,根本原因不是“写法更短”,而是执行路径不同:前者只需确认行存在,后者必须读取并判空该列值。
为什么 COUNT(*) 只扫索引就能完成计数
现代数据库(MySQL 8.0+、PostgreSQL、SQL Server)对 COUNT(*) 和 COUNT(1) 统一走「存在性扫描」——只要找到最小可用索引(比如主键 B+ 树),就只遍历叶子节点计数,不回表、不读字段值。
-
COUNT(*):引擎直接扫主键索引或最小二级索引(如KEY idx_status (status)),每页只读索引项,I/O 极少 - 哪怕表有 1 亿行,只要主键索引高度是 4 层,就只需读几千个索引页
- 如果表用 MyISAM 引擎,
COUNT(*)甚至直接查元数据,毫秒级返回(但 InnoDB 不支持)
为什么 COUNT(列名) 常常触发全表扫描
COUNT(列名) 必须逐行检查该列是否为 NULL。这个判断无法跳过,除非满足两个前提:该列定义为 NOT NULL + 有可用索引。
- 列允许
NULL且无索引 → 强制全表扫描 + 每行解码该列值 → I/O 和 CPU 开销显著上升 - 列有二级索引但
NULL占比高(比如 60%)→ 索引虽能跳过NULL,但因二级索引更宽、页更多,遍历成本仍高于扫主键索引 -
LEFT JOIN后用COUNT(user_name)→ 实际统计的是关联成功且user_name IS NOT NULL的行,语义已偏移,还拖慢执行
COUNT(列名) 看似合理却容易踩的坑
很多开发者写 COUNT(department_id) 是想“统计有部门的记录数”,但没意识到这会带来隐性代价。
- 误以为加了索引就安全 → 其实若
department_id允许NULL,优化器可能仍选择全表扫描(尤其当统计信息不准时) - 在宽表上统计
email非空数 → 若该列无索引、平均行长 100 字节,每次读行都多出百字节 I/O,比COUNT(*)慢 20%~40% - 依赖应用层保证不插
NULL→ 但导入脚本、历史数据、ORM 默认值遗漏都可能导致意外NULL,让性能突然恶化
真正决定快慢的不是括号里填什么,而是该列是否定义了 NOT NULL、有没有针对查询条件建对索引。别花时间纠结 COUNT(*) vs COUNT(1)——它们执行计划一致;但写 COUNT(列名) 前,务必先查 COUNT(*) - COUNT(列名) 看 NULL 占比,再决定要不要加约束或索引。


















