相信每个人在写代码时都有遇到过要获取 MYSQL 表里数据行数的情况,多数人获取数据表行数时都用 COUNT(*),但同时也流传了不少其他版本,比如说 COUNT(1)、COUNT(主键)、COUNT(字段)。到底那个版本 MYSQL 执行起来更快也是众说纷纭,其实之前我也不知道到底哪个执行起来快,到底谁说的对 (笑哭)。好在最近在认真学习极客时间的 MySQL 专栏,其中专门有一节是对这个问题的讨论,看完后也是解除了长久以来的疑惑。
文章中都是针对 MySQL 的 InnoDB 引擎展开讨论的,MyISAM 引擎是把一个表的总行数记录在了磁盘里,查询时效率很高 (如果加了 where 条件也不能直接从磁盘返回)。而 InnoDB 由于多版本并发控制(MVCC)的原因,即使时同一时刻的查询 InnoDB 表应该 ” 返回多少行 ” 也是不确定的,比如假设表 t 中有 10000 行数据:
时刻
会话 A
会话 B
会话 C
T1
begin(默认可重复读隔离级别拿到一致性视图);
T2
select count(*) from tbegin(默认可重复读隔离级别拿到一致性视图);
T3
insert into t (插入一行);
T4
select count(*) from t
begin 拿到一致性视图 );
T5
insert into t (插入一行);
insert into t (插入一行);
T6
select count(*) from t;(返回 10000)
select count(*) from t;(返回 10000)begin;
select count(*) from t;(返回 10002)
会话 A 在 T1 开启事务拿到一致性视图,可重复读级别下在事务中任何时刻读到数据都一样,其他事务的更新对会话 A 没影响所以 count(*) 的结果是 10000,会话 B 在 T4 开启事务拿到一致性视图,T4 之前会话 C 已经新插入了一条语句并提交(单独执行一条更新语句,InnoDB 会自己启动一个事务,语句执行完马上提交)。会话 B 在 T5 插入一条新数据,在 T6 查询时 count(*) 的结果是 10002(T4 begin 时会话 C insert 语句已经提交,所以在会话 B 的事务中能看到这个更新)。由于会话 B 在 T6 时事务还没有提交,会话 C 看不到会话 B 的更新,所以会话 C 在 T6 时 count(*) 的结果是 10001。
COUNT 是一个聚合函数,它的功能是对返回的结果集中每一行进行判断,如果 COUNT 函数的参数不是 NULL 则累加 1,否则不累加,最后返回累计值。接下来看一下每个 COUNT 版本的执行效率:
COUNT(主键 ID) InnoDB 遍历全表,把每一行的主键值都取出来返回给 MySQL 的 Server 层,因为主键不可能为 NULL,Server 层直接按行累加最后返回累计值给客户端。
COUNT(1) 遍历全表但不取值,Server 层对返回的每一行放个数字 ”1″ 进去,按行累加。COUNT(1) 比 COUNT(主键) 快,因为不需要取值,减少了数据传输。
COUNT(字段) 遍历全表,一行行从记录中读出字段值给 Server 层,Server 层判断值不为 NULL 了再累加。
COUNT(*) MySQL 专门做了优化,会找到表中最小的索引树,InnoDB 普通索引树比主键索引小很多,对于 COUNT(*) 遍历哪个树是一样的,count(*) 时 MySQL 不取记录值,count(*) 也肯定不为 NULL,Server 层中直接按行累加。
所以这个版本 COUNT 的从低到高分别为:
COUNT(字段) < COUNT(主键) < COUNT(1) ≈ COUNT(*)
所以建议你尽量使用 count(*) 来获取记录行数。
另外要注意,很多人为了销量会把表的行数记录到 Redis 中,但这样不能保证 Redis 里的计数和 MySQL 表里的数据保持精确一致,这是两个不同的存储系统不支持分布式事务所以就无法拿到精确的一致性视图,如果为了效率把表行数单独存储那么最好存放在一个单独的 MySQL 表里,这样无法拿到一致性视图的问题就能解决了。
关于 MySQL 更详细的分析,推荐关注 MySQL 实战 45 讲