做数据查询的人,几乎绕不开 COUNT。它的语法看着就那么几行,可一旦进了真实报表、统计口径和慢查询场景,很多人立刻会露怯:到底写 COUNT(*) 还是 COUNT(1)?为什么同一张表,COUNT(student_name) 和 COUNT(*) 结果不一样?COUNT(DISTINCT ...) 凭什么在几百万行上能把接口拖死?这篇文章不聊空泛概念,直接用一张考试成绩表和一张订单明细表,把所有常用 COUNT 写法从语法、语义到执行计划逐个拆开。适合刚学会 SQL 正想深入的同学,也适合天天写统计 SQL 但没时间抠细节的开发者。
1. COUNT 在数什么:一张成绩表里的行与空值
1.1 先建一张演示表,后面的例子都在它身上跑
为了不空谈,我们先建一张成绩表。业务场景很常见:每个学生报名了语文、数学、英语三门考试,考了就有分数,缺考则 score 保持 NULL。
sql复制CREATE TABLE t_exam (
id INT NOT NULL AUTO_INCREMENT,
student_no VARCHAR(16) NOT NULL COMMENT '学号',
student_name VARCHAR(32) NOT NULL COMMENT '姓名',
subject VARCHAR(16) NOT NULL COMMENT '科目',
score DECIMAL(5,2) NULL COMMENT '分数,NULL表示缺考',
exam_date DATE NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
插入 12 条记录,覆盖“正常成绩、缺考、不及格、不同科目数”这些情况。为了让 NULL 的影响看得清楚,我自己造数据时故意留了 4 个空分数:
sql复制INSERT INTO t_exam (student_no, student_name, subject, score, exam_date) VALUES
('S001', '张三', '语文', 88.00, '2024-06-30'),
('S001', '张三', '数学', NULL, '2024-07-05'),
('S001', '张三', '英语', 62.00, '2024-07-10'),
('S002', '李四', '语文', NULL, '2024-06-30'),
('S002', '李四', '数学', 91.00, '2024-07-05'),
('S002', '李四', '英语', NULL, '2024-07-10'),
('S003', '王五', '语文', 77.50, '2024-06-30'),
('S003', '王五', '数学', 84.00, '2024-07-05'),
('S003', '王五', '英语', 96.00, '2024-07-10'),
('S004', '赵六', '语文', NULL, '2024-06-30'),
('S004', '赵六', '数学', 59.00, '2024-07-05'),
('S004', '赵六', '英语', 80.00, '2024-07-10');
这张表有 12 行记录,也就是“学生-科目”报名关系有 12 条。score 为空的记录共 4 条,实际有成绩的记录是 8 条。这个数字后面会反复用到。
1.2 COUNT(*) 与 COUNT(1),本质都是“数行”,不关心空值
很多人会把 COUNT(*) 理解成“统计非空字段的数量”,这是个根深蒂固的误会。COUNT(*) 的语义是:统计结果集中所有行的行数,它只看“这一行存不存在”,不去管这一行里哪些字段是 NULL。
执行下面的查询:
sql复制SELECT COUNT(*) AS total_rows
FROM t_exam;
结果是 12。就算把成绩字段删光,或让整行所有业务字段都变成 NULL,只要 B+ 树里还存在着这条记录,COUNT(*) 就会把它算进去。
COUNT(1) 也完全可以理解为对每一行生成一个常量 1,然后统计这些 1 的数量,所以同样返回 12。很多新手写 COUNT(1) 时心里想的是“我只要主键那一列”,但数据库执行时并没有真的去每一行读某个列再判断空值,它本质上还是在数行。
1.3 COUNT(列) 才挑剔:NULL 会被丢掉,这是口径翻车的重灾区
COUNT(score) 和 COUNT(*) 看起来差不多,实际语义天差地别:
sql复制SELECT COUNT(*) AS total_rows,
COUNT(score) AS real_score_rows
FROM t_exam;
结果:
| total_rows | real_score_rows |
|---|---|
| 12 | 8 |
COUNT(score) 会遍历每一行,只把 score 不为 NULL 的行计入总数。于是 4 个缺考记录被自动排除,得到 8。
这恰恰是实际业务里最容易出错的地方。我经常见到有人在统计“实际参加考试人数”时,本意是统计有成绩的记录,却写了 COUNT(*),把缺考生也当成参考人数算进去;反而在统计“应到人数”时写了 COUNT(score),结果人数少了 4 个。所以用 COUNT 之前,第一件事一定是确认:你到底是要“数行”,还是要“数某个字段非空的行的数量”。两种需求写法完全不同,没有哪个写法天然“更正确”,只有是不是匹配业务口径。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. COUNT 的几种常见写法:COUNT(1)、COUNT(DISTINCT)、COUNT(CASE WHEN) 该怎么选
2.1 COUNT(1) 比 COUNT(*) 快?这个争论该结束了
很多老项目的 SQL 里都留着 COUNT(1),理由是从前辈那里听来的“这样不走全表,更快”。这个说法在很早的数据库版本里可能还有一定历史背景,但在现代主流数据库中,COUNT(*) 和 COUNT(1) 在执行计划层面几乎没有区别。
以 MySQL 的 InnoDB 为例,COUNT(*) 并不会真的把每个字段都取出来再数一遍,优化器会把 * 当作“不需要读取具体列值,只需要确认行存在”。COUNT(1) 同样是确认行存在后生成常量。两种写法最终通常会走同一条扫描路径。MySQL、PostgreSQL、SQL Server 的优化器早就不是逐行展开处理了,真正决定性能的是扫描了多少行、读取了多少个数据页,而不是括号里写 * 还是写 1。
所以不用在这两个写法上做无谓的性能纠结。我会建议团队统一用 COUNT(*),理由很朴实:* 是标准写法,语义清晰,也方便审 SQL 的人一眼看出这是在统计行数。把精力留到后面说的索引和扫描范围上,收益会大得多。
2.2 COUNT(DISTINCT 列):去重统计的价值和代价
COUNT(DISTINCT ...) 是另一种高频但昂贵的写法。比如想查这张成绩表里到底有几个学生参加了考试,不能直接 COUNT(student_no),因为每个学生在表里出现了多次。
sql复制SELECT COUNT(student_no) AS total_rows_with_student_no,
COUNT(DISTINCT student_no) AS distinct_students
FROM t_exam;
执行结果:
| total_rows_with_student_no | distinct_students |
|---|---|
| 12 | 4 |
也就是说,COUNT(DISTINCT student_no) 返回 4,表示 4 个不同的学号。这个语义适合用来统计用户数、商品数、渠道数等需要去重的指标。
代价也很直接:去重意味着数据库要做排序或哈希聚合。在几万行的表上几乎没有感知,可一旦表里是几百万、几千万行,COUNT(DISTINCT ...) 往往是接口超时的头号嫌疑犯。关于它的优化思路,后面第 5 部分会专门讲。
2.3 COUNT(CASE WHEN ...):SQL 里的条件计数器
想做条件计数,最灵活的写法不是 WHERE 后面接条件,因为 WHERE 会把不满足条件的行先过滤掉,导致你无法在同一个查询里同时统计多个口径。更推荐这样:
sql复制SELECT
student_name,
COUNT(*) AS total_subjects,
COUNT(CASE WHEN score IS NULL THEN 1 END) AS absent_cnt,
COUNT(CASE WHEN score >= 60 THEN 1 END) AS pass_cnt
FROM t_exam
GROUP BY student_name;
结果:
| student_name | total_subjects | absent_cnt | pass_cnt |
|---|---|---|---|
| 张三 | 3 | 1 | 2 |
| 李四 | 3 | 2 | 1 |
| 王五 | 3 | 0 | 3 |
| 赵六 | 3 | 1 | 1 |
这里的关键在于:CASE 不写 ELSE 时,条件不满足的返回值是 NULL,而 COUNT 会忽略 NULL,所以只统计满足条件的行。如果你想精简写法,COUNT(CASE WHEN score >= 60 THEN 1 END) 和 SUM(CASE WHEN score >= 60 THEN 1 ELSE 0 END) 都行,但前者更符合“只数满足条件的行”的直觉。
有个堪称经典的坑我要单独提醒:不要写成
