COUNT
导学
COUNT 是 SQL 中使用频率最高的聚合函数,也是面试中的"重灾区"。COUNT(*)、COUNT(1)、COUNT(列) 有什么区别?为什么 COUNT(*) 有时很慢?本节把 COUNT 彻底讲透。
定义
COUNT:聚合函数,用于统计行数或非 NULL 值的数量。根据参数不同,行为差异很大。
演示数据准备
DROP TABLE IF EXISTS employees;
CREATE TABLE employees (
emp_id INT PRIMARY KEY AUTO_INCREMENT,
emp_name VARCHAR(20),
dept VARCHAR(20),
score DECIMAL(5,2)
);
INSERT INTO employees (emp_name, dept, score) VALUES
('大翔', '技术部', 100),
('白歌', '技术部', NULL);
当前 employees 表中的完整数据如下:
| emp_id | emp_name | dept | score |
|---|---|---|---|
| 1 | 大翔 | 技术部 | 100 |
| 2 | 白歌 | 技术部 | NULL |
SQL 示例
场景一:COUNT(*) — 统计所有行
当前数据状态:见上文 employees 表完整数据。
执行语句:
SELECT COUNT(*) FROM employees;
操作后结果:
| COUNT(*) |
|---|
| 2 |
结果解读:统计 employees 表的总行数,包括所有列都为 NULL 的行(如果有的话)。这是获取表总行数的标准写法。COUNT(*) 不会展开所有列,而是有特殊的优化路径。
场景二:COUNT(列) — 统计非 NULL 行
当前数据状态:见上文 employees 表完整数据。
执行语句:
SELECT COUNT(score) FROM employees;
操作后结果:
| COUNT(score) |
|---|
| 1 |
结果解读:只统计 score 列不为 NULL的行。白歌的 score 为 NULL,因此被排除,返回 1。用于统计"已录入绩效分的员工数"。
再看 COUNT(emp_id):
SELECT COUNT(emp_id) FROM employees;
操作后结果:
| COUNT(emp_id) |
|---|
| 2 |
结果解读:emp_id 是主键,不可能为 NULL,因此 COUNT(emp_id) 等同于 COUNT(*),返回 2。
场景三:COUNT(DISTINCT 列) — 去重计数
当前数据状态:见上文 employees 表完整数据。
执行语句:
SELECT COUNT(DISTINCT dept) FROM employees;
操作后结果:
| COUNT(DISTINCT dept) |
|---|
| 1 |
结果解读:先对 dept 去重,再统计不同值的数量。employees 表中只有 1 种部门(技术部),因此返回 1。用于统计"有多少种不同的部门"。
也可以对多个列的组合去重:
SELECT COUNT(DISTINCT dept, score) FROM employees;
操作后结果:
| COUNT(DISTINCT dept, score) |
|---|
| 2 |
结果解读:(技术部, 100)、(技术部, NULL) 被视为不同的组合。表中实际存在的组合有 2 种,因此返回 2。
场景四:COUNT(*) 与 WHERE 结合
当前数据状态:见上文 employees 表完整数据。
执行语句:
SELECT COUNT(*) FROM employees WHERE dept = '技术部';
操作后结果:
| COUNT(*) |
|---|
| 2 |
结果解读:先过滤 dept = '技术部' 的行(大翔、白歌),再计数。只统计满足条件的行数。
再看一个多条件结合的例子:
SELECT COUNT(*) FROM employees WHERE dept = '技术部' AND score >= 90;
操作后结果:
| COUNT(*) |
|---|
| 1 |
结果解读:技术部部门的员工有大翔、白歌,其中大翔的 score >= 90(白歌为 NULL),因此返回 1。
场景五:同时获取总数和条件计数(CASE WHEN + COUNT)
当前数据状态:见上文 employees 表完整数据。
执行语句:
SELECT
COUNT(*) AS 总人数,
COUNT(CASE WHEN dept = '技术部' THEN 1 END) AS 技术部人数,
COUNT(CASE WHEN dept = '产品部' THEN 1 END) AS 产品部人数,
COUNT(CASE WHEN score >= 90 THEN 1 END) AS 优秀人数,
COUNT(CASE WHEN score IS NULL THEN 1 END) AS 绩效分未录入人数,
COUNT(CASE WHEN dept = '运营部' THEN 1 END) AS 运营部人数
FROM employees;
操作后结果:
| 总人数 | 技术部人数 | 产品部人数 | 优秀人数 | 绩效分未录入人数 | 运营部人数 |
|---|---|---|---|---|---|
| 2 | 2 | 0 | 1 | 1 | 0 |
结果解读:CASE WHEN 配合 COUNT 是单查询实现条件统计的经典技巧。不满足条件的行返回 NULL,COUNT 忽略 NULL,因此只统计满足条件的行。
- 总人数:2
- 技术部人数:大翔、白歌 = 2
- 产品部人数:0(没有产品部员工)
- 优秀人数(score >= 90):大翔 100 = 1(白歌 NULL 不满足)
- 绩效分未录入人数(score IS NULL):白歌 = 1
- 运营部人数:0(没有运营部员工)
场景六:COUNT(1) 与 COUNT(*) 的对比
当前数据状态:见上文 employees 表完整数据。
执行语句:
SELECT COUNT(*) FROM employees;
SELECT COUNT(1) FROM employees;
SELECT COUNT(0) FROM employees;
SELECT COUNT('abc') FROM employees;
操作后结果:
| COUNT(*) | COUNT(1) | COUNT(0) | COUNT('abc') |
|---|---|---|---|
| 2 | 2 | 2 | 2 |
结果解读:在 MySQL 5.7 中,COUNT(*)、COUNT(1)、COUNT(0)、COUNT('abc') 的结果完全相同。优化器会同等对待这些写法,都会统计所有行数。COUNT(1) 中的 1 只是一个常量表达式,对每一行都计算为 1,然后统计非 NULL 的行数(常量永远非 NULL),因此等同于 COUNT(*)。
场景七:COUNT 与 NULL 的陷阱
当前数据状态:见上文 employees 表完整数据(白歌的 score 为 NULL)。
执行语句:
SELECT
COUNT(*) AS 总人数,
COUNT(score) AS 有绩效分人数,
COUNT(*) - COUNT(score) AS 无绩效分人数
FROM employees;
操作后结果:
| 总人数 | 有绩效分人数 | 无绩效分人数 |
|---|---|---|
| 2 | 1 | 1 |
结果解读:COUNT(*) 统计所有员工(2 条),COUNT(score) 只统计 score 非 NULL 的员工(1 条)。两者相减得到 score 为 NULL 的员工数(1 条:白歌)。
场景八:空表的 COUNT 行为
当前数据状态:创建一张空表。
CREATE TABLE IF NOT EXISTS empty_employees (
emp_id INT PRIMARY KEY,
emp_name VARCHAR(20)
);
执行语句:
SELECT COUNT(*) FROM empty_employees;
SELECT COUNT(emp_name) FROM empty_employees;
操作后结果:
| COUNT(*) | COUNT(emp_name) |
|---|---|
| 0 | 0 |
结果解读:空表没有任何行,COUNT(*) 和 COUNT(列) 都返回 0。这是 COUNT 与其他聚合函数(如 SUM、AVG)的重要区别——空表时 COUNT 返回 0,而其他聚合函数返回 NULL。
COUNT(*) 的性能问题
InnoDB 的 COUNT(*) 是慢操作
- MyISAM:
COUNT(*)不带WHERE时,直接返回表的元数据,速度极快 - InnoDB:即使不带
WHERE,也需要遍历聚簇索引统计行数(因为 InnoDB 支持事务,不同事务看到的行数不同,无法缓存精确值)
优化方案
-- 方案1:用 SHOW TABLE STATUS 估算(不准确)
SHOW TABLE STATUS LIKE 'employees';
-- 查看 Rows 字段(估算值)
-- 方案2:维护计数器表(精确但需额外维护)
UPDATE counter_table SET total = total + 1 WHERE table_name = 'employees';
-- 方案3:用二级索引(比聚簇索引小,扫描更快)
SELECT COUNT(emp_id) FROM employees; -- emp_id 有索引时可能比 COUNT(*) 快
结果解读:
SHOW TABLE STATUS的Rows字段是基于索引统计的估算值,不是精确值,但获取速度极快- 计数器表需要应用层在插入/删除时维护,适合对实时性要求不高的场景
COUNT(emp_id)如果emp_id列有二级索引,可能扫描更小的索引树,比扫描聚簇索引的COUNT(*)更快
COUNT(*) vs COUNT(1) vs COUNT(列)
| 写法 | 统计对象 | 性能(InnoDB) |
|---|---|---|
COUNT(*) | 所有行 | 遍历聚簇索引 |
COUNT(1) | 所有行 | 与 COUNT(*) 相同,优化器会统一处理 |
COUNT(列) | 列非 NULL 的行 | 可能走该列的二级索引,比聚簇索引小 |
COUNT(PK) | 主键非 NULL 的行 | 等同于 COUNT(*),主键不会为 NULL |
面试结论:在 MySQL 5.7 中,
COUNT(*)和COUNT(1)没有性能差异,优化器会同等对待。COUNT(列)如果列上有二级索引,可能扫描更小的索引树。
常见误区
| 误区 | 正解 |
|---|---|
COUNT(1) 比 COUNT(*) 快 | MySQL 5.7 优化器会同等优化,无差异。 |
COUNT(*) 会统计所有列 | 不会。COUNT(*) 是特殊优化路径,不展开列。 |
COUNT 可以对多个列使用 | COUNT(列1, 列2) 在 MySQL 5.7 中语法不合法,应分别计数或用子查询。 |
COUNT 总是返回非 NULL | 空表时 COUNT 返回 0,不是 NULL。 |
面试考点
Q:COUNT(*)、COUNT(1)、COUNT(emp_id) 哪个快?
COUNT(*)和COUNT(1)无差异。COUNT(emp_id)如果emp_id是主键,等同于COUNT(*);如果emp_id是二级索引列,可能扫描更小的索引树。实际差异微乎其微,应关注查询条件和索引设计。
Q:为什么 InnoDB 的 COUNT(*) 比 MyISAM 慢?
MyISAM 将总行数存储在表元数据中(因为不支持事务,行数唯一);InnoDB 支持 MVCC,不同事务看到不同的快照,必须实时遍历索引统计,无法缓存精确值。
Q:如何快速获取 InnoDB 表的近似行数?
SHOW TABLE STATUS或查询information_schema.tables的table_rows字段。这是基于索引统计的估算值,不是精确值,但获取速度极快。
Q:COUNT(DISTINCT 列1, 列2) 合法吗?
在 MySQL 5.7 中合法,表示对列1和列2的组合去重后计数。例如
(dept, score)的组合去重。
小结
COUNT(*)统计所有行,COUNT(列)统计非 NULL 行COUNT(*)和COUNT(1)在 MySQL 5.7 中无性能差异- InnoDB 的
COUNT(*)需要遍历索引,大数据量时较慢 CASE WHEN+COUNT是单查询条件统计的利器- 空表时
COUNT返回 0,其他聚合函数返回 NULL COUNT(DISTINCT 列)用于去重计数
下一章引子:聚合函数配合 GROUP BY,就能实现"分组统计"——这是数据分析中最常用的 SQL 模式之一。