因为优化器在语义分析阶段识别出所有非NULL常量(如1、42、'x')均不依赖列值,逻辑等价于每行必计1次,统一归一化为COUNT(*)的内部表示,后续执行计划完全相同。
因为优化器在语义分析阶段就识别出所有非 NULL 常量(1、42、'x')都不依赖列值,逻辑上等价于“每行必计 1 次”,直接统一归一化为行计数操作。MySQL 8.0+、PostgreSQL 14+ 等会把 COUNT(1) 和 COUNT('hello') 都重写成内部的 COUNT(*) 表示,后续走完全相同的索引扫描路径。
这说明数据库没读数据行,只遍历了最小可用索引的叶子节点——无论你写 COUNT(*) 还是 COUNT(1),只要表有主键或二级索引,优化器就会选 key_len 最小的那个索引快速计数。真正耗时的是 I/O 扫描页数,不是括号里填啥。
INDEX idx_status(status) → 可能扫这个二级索引(比主键窄)COUNT(id) 不是常量,它是列引用。哪怕 id 是主键且 NOT NULL,优化器也得把每个 id 值从索引页中解包、传给 Server 层做 NULL 判断——多了内存拷贝和类型处理。而 COUNT(*) 和 COUNT(1) 完全跳过字段读取,纯计页。
id 允许 NULL,必须逐行检查,无法跳过email 字段允许 NULL 且无索引,COUNT(email) 会触发全表扫描 + 每行判 NULL,比 COUNT(*) 慢数倍COUNT(*) WHERE status = 'active' 若命中二级索引,可能比 COUNT(status) 还快——因为后者仍要过滤 NULL那些说法基本来自三类已失效场景:MySQL 5.5 以前 MyISAM 引擎的元数据缓存 bug;SQLite 3.20 前未对 COUNT(*) 做常量折叠;或者 ORM 自动生成语句时绕过某个解析限制。现代数据库里,COUNT(1) 唯一真实开销是 Server 层多一次常量表达式解析——纳秒级,不可测,也不该成为决策依据。
真正该盯紧的,是表有没有主键、WHERE 条件能否走覆盖索引、以及你到底需要“总行数”还是“某列非空行数”。后者才是影响结果和性能的硬边界。