COUNT()是统计总行数的首选,它统计所有行(含NULL、空字符串、重复值),语义准确且被主流数据库优化;COUNT(col)会忽略该列NULL值,易漏计;COUNT()与COUNT(1)性能无差异,但前者更标准、推荐。

为什么 COUNT(*) 是统计总行数的首选
它直接告诉数据库“数所有行”,包括含 NULL 的列、空字符串、重复值,甚至全为 NULL 的行。比 COUNT(col) 更可靠——后者会跳过该列值为 NULL 的行,容易漏计。
常见误用:COUNT(id) 看似安全,但如果 id 允许为 NULL(比如外键未关联),结果就少于真实行数。
-
COUNT(*)在绝大多数 SQL 引擎(PostgreSQL、MySQL 8.0+、SQL Server、SQLite)中被优化为元数据读取或快速索引扫描,性能不输其他写法 - MySQL 5.7 及更早版本对 MyISAM 表的
COUNT(*)是 O(1),InnoDB 则需实际扫描——但仍是语义最准确的选择 - 不要用
SELECT * FROM table再在应用层计数:网络传输和内存开销大,且可能因超时或截断出错
COUNT(*) 和 COUNT(1) 有区别吗
没有实质区别。几乎所有主流数据库把 COUNT(1) 当作常量表达式等价处理,执行计划和性能与 COUNT(*) 完全一致。
但 COUNT(1) 是一种历史惯性写法,源于早期某些数据库对 * 解析较慢;现代引擎已无此问题。继续用它不会错,但没必要刻意替换已有 COUNT(*)。
- PostgreSQL 明确文档说明:
COUNT(*)和COUNT(1)行为相同,推荐用前者 - SQL 标准只定义了
COUNT(*)为“行计数”语义,COUNT(1)属于实现兼容写法 - 某些 ORM(如 Django 的
.count())底层生成的就是COUNT(*),不是COUNT(1)
带条件统计时,别在 WHERE 里误删空行
如果想统计“某个字段非空的记录数”,错误做法是:SELECT COUNT(*) FROM t WHERE col IS NOT NULL —— 这确实能算,但若你还想同时查总数,就得再跑一遍 COUNT(*),效率低且难维护。
更优解是用条件聚合:
SELECT COUNT(*) AS total, COUNT(col) AS non_null_col, COUNT(CASE WHEN status = 'active' THEN 1 END) AS active_count FROM users;
这样单次查询完成多个维度统计,避免多次全表扫描。
-
COUNT(col)天然跳过NULL,适合统计“该列有值”的行数 -
COUNT(CASE ...)中的THEN 1是关键:返回NULL的分支不计入,等效于SUM(CASE ... THEN 1 ELSE 0 END),但更简洁 - 避免写
COUNT(IF(...))(MySQL)或COUNT(IIF(...))(SQL Server)——可读性差,且部分旧版本不支持在COUNT内直接嵌套条件函数
分页场景下 COUNT(*) 的性能陷阱
做“显示共 N 条,当前第 M 页”时,很多人习惯先 SELECT COUNT(*) FROM ... WHERE ...,再 SELECT ... LIMIT ... OFFSET ...。当表大、条件复杂、没走好索引时,这个 COUNT(*) 可能比主查询还慢。
- 如果业务允许近似总数(比如后台管理看板),可用
EXPLAIN的rows估算值(PostgreSQL 用pg_class.reltuples,MySQL InnoDB 可查INFORMATION_SCHEMA.TABLES) - 对严格分页,考虑加覆盖索引:让
WHERE条件和排序字段都在索引中,COUNT(*)才可能走索引而不用回表 - 千万级表慎用
OFFSET分页,COUNT(*)+LIMIT/OFFSET组合在深分页时会双重扫描——改用游标分页(WHERE id > last_seen_id ORDER BY id LIMIT N)可绕过总数需求
真正棘手的不是语法怎么写,而是当 WHERE 条件涉及函数、隐式转换或跨表 JOIN 时,COUNT(*) 很可能无法利用索引,这时候执行计划里出现 Seq Scan 或 Using filesort 就得立刻警觉。

















