MySQL 中 COUNT (*) 与 COUNT (col) 哪个效率更高
在 MySQL 日常开发中,
COUNT(*)与COUNT(col)是最常用的计数函数,但多数开发者易将二者混淆,误认为仅是语法差异。实际上,二者在功能逻辑和执行性能上存在显著区别,且受 InnoDB、MyISAM 两大存储引擎的特性影响极大。本文基于 MySQL 8.0.30 版本的实测数据,从功能本质、引擎差异、认知误区三方面,拆解计数函数的核心逻辑与优化思路。一、功能本质:计数范围的核心差异
COUNT(*)与COUNT(col)的根本区别,在于计数时是否排除 NULL 值,这直接决定了二者的结果差异,也是性能差异的源头。1. COUNT (*):计数所有行,无视 NULL
COUNT(*)的作用是统计表中所有符合条件的行数量,无论行中列值是否为 NULL(包括主键为 NULL 的情况,若表允许)。它不关注具体列的内容,仅判断 “行是否存在”。例如,对包含 1000 万行数据的表
count_innodb(含允许 NULL 的val_with_nulls列)执行COUNT(*),结果始终为 1000 万,与列值是否为 NULL 无关:mysql> select count(*) from count_innodb;
+----------+
| count(*) |
+----------+
| 10000000 |
+----------+
1 row in set (0.38 sec)
2. COUNT (col):仅计数非 NULL 列值
COUNT(col)(col为具体列名)的作用是统计该列值不为 NULL 的行数量。若列中存在 NULL 值,这些行将被排除,导致结果小于COUNT(*)。仍以
count_innodb表为例,val_with_nulls列允许 NULL,执行COUNT(val_with_nulls)后,结果为 9990001(约 1 万行为 NULL),明显少于COUNT(*):mysql> select count(val_with_nulls) from count_innodb;
+-----------------------+
| count(val_with_nulls) |
+-----------------------+
| 9990001 |
+-----------------------+
1 row in set (2.14 sec)
若列定义为
NOT NULL(如val_no_null),COUNT(val_no_null)的结果会与COUNT(*)一致,但这仅是 “结果巧合”,二者的执行逻辑仍不同 ——COUNT(col)需额外判断列值是否为 NULL(即使定义为NOT NULL,MySQL 仍会做隐性校验)。二、引擎特性:性能差异的关键变量
MySQL 中存储引擎的核心特性(如事务支持、行数缓存),直接影响计数函数的执行效率。InnoDB 与 MyISAM 在计数性能上的差异,本质是 “事务安全性” 与 “查询速度” 的权衡。
1. InnoDB 引擎:事务优先,无行数缓存
InnoDB 是事务型引擎,需满足 ACID 特性,不缓存表的总行数。原因在于:并发事务中,不同事务看到的行数量可能不同(如 A 事务插入的行,B 事务未提交前无法看到),只能实时计算当前事务可见的行数。
(1)全表计数场景
- COUNT (*) 与 COUNT (NOT NULL 列):性能接近。InnoDB 会优先选择最小的索引(如主键索引)进行 “索引覆盖扫描”(
Extra: Using index),无需访问表数据,直接通过索引计数。实测中,二者执行时间均约 0.38 秒(1000 万行数据)。 - COUNT (NULLABLE 列):性能骤降。因需判断列值是否为 NULL,无法仅通过索引完成,必须扫描全表数据。实测中执行时间达 2.14 秒,是前两者的 5.6 倍。
(2)带 WHERE 条件计数场景
若查询含
WHERE条件(如id<1000000),InnoDB 会通过条件匹配的索引(如主键索引)定位行范围,再进行计数:COUNT(*)仍可通过索引覆盖扫描完成,执行时间约 0.30 秒;COUNT(val_no_null)需额外校验列值(虽为NOT NULL),执行时间约 0.33 秒,差异微小;COUNT(val_with_nulls)需判断 NULL 值,但因WHERE条件已通过索引过滤部分行,执行时间降至 0.33 秒,与前两者差距缩小。
(3)粗略计数的替代方案
若业务无需精确行数(如统计报表),可通过
information_schema.tables获取估算值,速度极快但精度低(误差通常在 1%-5%):mysql> select table_rows from information_schema.tables where table_name='count_innodb';
+------------+
| TABLE_ROWS |
+------------+
| 9980586 | # 与实际1000万行存在近2万行误差
+------------+
1 row in set (0.00 sec)
2. MyISAM 引擎:非事务,行数缓存加速
MyISAM 是非事务引擎,不支持行锁和事务隔离,会在表头部缓存总行数。这一特性使其在全表计数场景下速度极快,但牺牲了事务安全性。
(1)全表计数场景
- COUNT (*) 与 COUNT (NOT NULL 列):瞬间完成。直接读取缓存的行数,无需扫描表或索引,实测执行时间为 0 秒(1000 万行数据);
- COUNT (NULLABLE 列):性能灾难。需全表扫描判断 NULL 值,实测执行时间达 14.18 秒,是 InnoDB 同场景的 6.6 倍,差距悬殊。
(2)带 WHERE 条件计数场景
若查询含
WHERE条件,MyISAM 的行数缓存失效,需通过索引过滤行后再计数,此时性能差异主要源于 “是否需判断 NULL 值”:COUNT(*):通过索引覆盖扫描,执行时间约 0.41 秒;COUNT(val_no_null)与COUNT(val_with_nulls):需校验列值(前者隐性校验NOT NULL,后者判断 NULL),执行时间均约 2.55 秒,是COUNT(*)的 6.2 倍;- 优化方案:若需频繁执行
COUNT(col),可建立 “条件列 + 目标列” 的联合索引(如idx(id, val_with_nulls)),使查询重新支持索引覆盖扫描,执行时间可降至 0.42 秒,与COUNT(*)接近。
三、认知误区:COUNT (1) 与 COUNT (*) 的 “性能神话”
很多开发者认为 “
COUNT(1)比COUNT(*)更快”,理由是 “COUNT(*)会扫描所有列,COUNT(1)只需计数常量 1”。但实测数据表明,二者在功能和性能上完全一致。COUNT(1)中的 “1” 是非 NULL 常量,MySQL 会将其视为 “每行都存在的非 NULL 值”,计数逻辑等同于 “统计所有行”,与COUNT(*)无区别:- 执行结果相同:对
count_innodb表执行COUNT(1)和COUNT(0),结果均为 1000 万; - 执行性能相同:实测执行时间均约 0.30 秒,与
COUNT(*)一致; - 执行计划相同:均采用索引覆盖扫描(
Extra: Using index),无额外开销。
结论:
COUNT(1)与COUNT(*)是 “语法不同,逻辑相同”,不存在性能差异,无需刻意替换。四、实践优化:计数函数的选择与调优
基于上述分析,结合业务场景选择合适的计数函数,并通过索引优化性能,是 MySQL 计数查询的核心原则。
1. 函数选择优先级
- 优先使用 COUNT (*):无论列是否为 NULL,无论引擎类型,
COUNT(*)的性能均最优(或与其他函数持平),且结果为 “所有行的准确计数”; - 仅当需统计非 NULL 列值时,使用 COUNT (col):如 “统计有手机号的用户数”(手机号列允许 NULL),此时必须用
COUNT(phone); - 避免使用 COUNT (NULLABLE 列) 做全表计数:除非业务必需,否则优先用
COUNT(*)替代,避免全表扫描。
2. 性能调优技巧
- InnoDB 表:
- 为频繁计数的
WHERE条件列建立索引(如主键、唯一索引),确保索引覆盖扫描; - 粗略计数用
information_schema.tables,精确计数用COUNT(*);
- 为频繁计数的
- MyISAM 表:
- 适合读多写少、无需事务的场景(如日志表、统计报表),全表计数用
COUNT(*); - 若需
COUNT(NULLABLE列),建立 “条件列 + 目标列” 的联合索引,避免全表扫描。
- 适合读多写少、无需事务的场景(如日志表、统计报表),全表计数用
总结
COUNT(*)与COUNT(col)的差异,本质是 “计数范围” 与 “引擎特性” 共同作用的结果:COUNT(*)统计所有行,性能最优;COUNT(col)统计非 NULL 列值,性能受列是否为 NULL 和引擎类型影响。MyISAM 靠行数缓存实现全表计数加速,但不支持事务;InnoDB 为事务安全牺牲缓存,需通过索引优化计数性能。
浙公网安备 33010602011771号