执行以下查询:
为什么
再看一个复杂点的例子,表达式本身也可能产生 NULL:
假设 一句话总结:
二、四种写法的执行逻辑与性能分析1、
|
| (3)实测差距(大表场景示例): | 写法 | 执行时间(1亿行) | 说明 |
|---|---|---|---|
COUNT(*) |
8 秒 | 扫描最小二级索引 | |
COUNT(1) |
8 秒 | 同上 | |
COUNT(id) |
12 秒 | 扫描聚簇索引 | |
COUNT(name) |
45 秒 | name 无索引,需回表 + 判断 NULL |
即使 name 列有索引且为 NOT NULL,其性能通常也略慢于 COUNT(*)。
(4)使用场景:仅当需要排除 NULL 值时才使用,例如统计有手机号的用户数 COUNT(phone)。切勿用它来统计总行数。
COUNT(DISTINCT 列名) 的说明用途:统计某列 非 NULL 且不重复 的值的数量。
性能:通常较慢,因为需要去重操作(可能使用临时表或排序)。
建议:仅在确实需要去重计数时使用。对于大表,可考虑使用 Redis 的 HyperLogLog 或引入 ClickHouse 等 OLAP 引擎进行近似统计。更多关于数据库中间件的深入探讨,可以参考 数据库/中间件/技术栈 的相关内容。
-- 统计不同部门数量
SELECT COUNT(DISTINCT department) FROM employees;
GROUP BY 场景下的执行差异在分组统计中,不同 COUNT 写法的差异依然存在。
SELECT department, COUNT(*) FROM employees GROUP BY department;
SELECT department, COUNT(salary) FROM employees GROUP BY department;
COUNT(*):对每个分组,直接统计行数。COUNT(salary):对每个分组,需逐行检查 salary 是否为 NULL 才计数。性能影响:如果 salary 无索引或允许 NULL,在分组多、数据量大时,性能差距可能达 10~100 倍。
建议:若只需统计“每个部门有多少人”,用 COUNT(*);若需统计“每个部门有多少人有工资记录”,才用 COUNT(salary)。
误区1:COUNT(1) 比 COUNT(*) 快。
事实:在 InnoDB 引擎下,两者执行计划与性能完全相同。
误区2:主键最小,所以 COUNT(主键) 最快。
事实:COUNT(*) 会优先扫描 最小的二级索引,通常比聚簇索引(主键)小得多,I/O 更少。
误区3:COUNT(列名) 和 COUNT(*) 差不多。
事实:若列无索引或含 NULL,性能可能差 5~10 倍甚至更多。
误区4:COUNT(NULL) 有用。
事实:COUNT(NULL) 永远返回 0,毫无意义。
误区5:可以用 MAX(id) 代替 COUNT(*) 统计总行数。
事实:主键可能因删除、回滚等操作不连续,导致结果严重偏小,不可靠。
| 需求 | 推荐写法 | 说明 |
|---|---|---|
| 统计总行数 | COUNT(*) |
最快、最标准 |
统计某列非 NULL 行数 |
COUNT(列名) |
仅在此场景使用 |
统计某列去重非 NULL 数量 |
COUNT(DISTINCT 列名) |
谨慎用于大表 |
| 高频查询总行数(不要求精确) | Redis 缓存 | 毫秒响应 |
| 高频查询总行数(要求精确) | MySQL 计数表 | 事务安全 |
| 分库分表统计 | 并行 COUNT(*) + 汇总 |
提升吞吐 |
对于高并发、大数据量的场景,即使使用了 COUNT(*) 也可能面临瓶颈。应根据业务对精确性、实时性、并发量的要求,选择以下优化方法:
场景:用户总数、订单总数等频繁展示但不要求强一致的指标。
方法:
INCR/DECR 在数据变更时同步更新计数。COUNT(列名) 加速创建索引前提:必须使用 COUNT(列名)(如统计非 NULL 手机号数量)。
方法:为该列创建单列索引。若允许 NULL 且业务允许,可考虑改为 NOT NULL DEFAULT ‘’。
效果:避免回表,性能提升显著。
COUNT(*)问题:SELECT COUNT(*) FROM big_table 在亿级表上可能耗时很久。
方法:
WHERE create_time > '2023-01-01')。COUNT(*)原理:COUNT(*) 需扫描一个索引确认行存在。InnoDB 会选择物理最小的索引。如果表没有合适的窄索引,可以显式创建一个。
示例:
ALTER TABLE big_table ADD COLUMN stat_flag TINYINT NOT NULL DEFAULT 0;
CREATE INDEX idx_stat ON big_table (stat_flag);
此时 COUNT(*) 会优先扫描这个极小的 idx_stat 索引。
场景:对实时性要求不高的后台报表、运营看板。
方法:通过定时任务定期执行 COUNT(*) 并将结果存入缓存或统计表。
优势:避免在线业务被统计查询拖慢。
场景:主库压力大,不允许长时间扫描。
方法:将 COUNT(*) 查询路由到只读从库。
注意:需评估业务是否能接受从库延迟带来的数据略微过期。
场景:统计去重数量(如 UV),且允许一定误差(通常 <2%)。
方法:MySQL 原生不支持,可使用 Redis 的 PFADD/PFCOUNT 命令或引入 ClickHouse 等系统。
information_schema.TABLES 快速估算场景:监控、告警、粗略展示等不要求精确值的场景。
用法:
SELECT TABLE_ROWS
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'your_db' AND TABLE_NAME = 'big_table';
注意:TABLE_ROWS 是采样估算值,可能存在 10%~50% 的偏差,但响应极快。
COUNT 函数看似简单,实则暗藏玄机。理解其底层执行机制是写出高性能 SQL 的关键。请牢记:*只要不是为了排除 NULL 值,就永远使用 `COUNT()`!** 它不仅是 SQL 标准写法,更是 MySQL 优化器最友好的选择。在亿级数据面前,括号内一个小小的差异,可能就是“秒出结果”与“漫长等待”的天壤之别。希望本文能帮助你在数据库查询优化中做出更明智的选择。更多技术深度讨论和资源,欢迎访问 云栈社区。