mysql中count(*)是最慢的吗?

在使用数据库进行系统开发的过程中,会经常使用count函数进行数据行数统计,来满足我们的业务需求,如电商系统中,总的用户人数,某个用户总的下单数等。

count函数是什么:

这个问题,对于但凡接触过数据库的老铁来说,实在太简单了,但是对于笔者来说,学习任何一个事物,应该先对它的定义和概念一个清晰的认识,这样才能更好的理解和学习它。
首先 count函数是sql(Structured Query Language)中的一个聚合函数,用来统计满足“条件”数据的行数。满足的条件具体指什么呢?这个条件并不是指sql中where后的条件,而是count(?)函数的参数?要满足不为 null 这个条件。也就是count函数括号中的内容不能为null,如果参数值为null,那么该行数据,将不被统计。

count如何实现的:

在mysql中,当数据表数据比较少的时候,使用count可以很快进行返回需要统计的数据行数,当数量比较大的时候,count的返回结果的速度就变慢的很多,出现这种情况时,使用的mysql存储引擎大概率是InnoDB。因为在MyISAM引擎中,对每个表的总行数都会进行记录,并存在磁盘上。当执行count(*)的时候,会直接把已经统计好的表总行数直接返回,无需任何计算,效率很高。当然,如果统计行数的sql语句中使用了where条件过滤的话,那么sql的语义就变成了满足业务条件的数据集中数据的行数,而非表总行数,也就不能在直接使用保存在磁盘上的这个已经统计好的"表总行数"了。
在InnoDB存储引擎中,却无法像MyISAM这样,事先把整个表中总行数统计好,等执行count(*)时,直接把表总行数返回。我们都知道InnodB存储引擎是支持事务的,而且事务之间又是隔离的。事务A中插入了一条数据,对事务B可能暂时不可见,那么这时在事务B中,进行总行数的统计,就不能包含事务A刚刚插入进去的这条记录,然而在事务A中进行总行数的统计,又要包含这条刚刚插入的记录。两个事务中,统计的总行数是不一样的。所以,如果InnoDB像MyISAM引擎那样,把表的总行数记录在磁盘中,那么这个总行数是事务A统计的行数,还是事务B统计的行数呢?因此在InnoDB中,没有办法向MyISAM那样,事先就把总行数信息统计好保存起来。

因为InnoDB对事务的支持,所以在InnoDB引擎下,执行count(*)对表行数统计时,只能把数据一行一行的从存储引擎中读出来,然后累计计数。为了加深理解,可以结合一下场景:

创建一个空表t

CREATE TABLE `t` (
  `id` varchar(50) 
) ENGINE=InnoDB 

看一下下面的示意图:

在三个回话中执行相同的行数统计查询,返回的结果是不一样的。

count的多种形式

count函数有多种使用方式,主要有以下4种形式:

count(*)
count(主键)
count(1)
count(普通字段)

这几种方式有何差异呢?如笔者开头所说的count函数的定义,可以大致得出结论。
因为count函数是对满足参数值不为null的数据行数的统计,因此对于:

select count(?) from table; (?表示:主键,1,普通字段,*)

count(主键):InnoDB会遍历整张表,依次取出每一行的主键值,返回给server层,server层判断主键不可能为空,就直接按行累加。

count(1):InnoDB会遍历整张表,但是不取值,server层对于返回的每一行,放进数据集中一个数字1,因为数字1不可能为空,所以直接按行累加。

count(普通字段):InnoDB会遍历整张表,依次取出每一行指定的字段的值,返回给server层,如果server判断这个字段的定义为not null的话,也就是这个字段值不可能为空,直接按行累加。如果这个字段定义允许为null的话,那么server层要对从引擎层获取的每个字段值进行非空判断,不是null才会累加,否则过滤掉。

count(*)中,*表示一整行数据的值,在正常业务数据表中,必定不可能所有字段全为null,再加上把整行数据从引擎层复制到server层,涉及大量io,因此mysql对count(*)
进行了优化:不在取整行数据,而是不取值,直接按行累加。效率和count(1)相当。

对于这几种形式的count,执行 select count(?) from tabel; 按照执行效率排序的话:count(字段)<count(主键)<count(1)=count(*),因为count(*)进行过特殊的优化,因此在我们日常开发中,尽量使用count(*)。

不过需要注意的是,结合开头的定义:count统计参数不为null的行数的个数,所以count(*),count(1) 这两种统计方式统计的行的个数,可能要比count(普通字段)要多一些。因为一个表中,可能存在整行数据全部为null的情况,如下图所示:

CREATE TABLE `test` (
  `name` varchar(50) 
) ENGINE=InnoDB 

insert into test values (null);

select count(1) from test; // 返回1行
select count(*) from test; // 返回1行
select count(name) from test; // 返回0行

实现高效的数据条数统计

经过上面分析,当时数据量比较大的时候,在InnoDB引擎中,使用count函数统计行数信息是比较耗时的操作,那么我们业务开发过程中对于这类统计,该怎么做呢?

1.使用自增主键

当我们使用自增主键的情况下,每增加一条数据,主键值都会增加1,因为有自增锁的保证,即使并发情况下,自增主键也不会出现累加异常的情况。那是否可以用最大的那个主键值作为该表的总行数呢?如果粗略的估计的话,倒还可以,如果要精确统计,那就不可以了,具体缘由可以参考该篇文章

2.使用第三方存储

可以把表总行数信息保存第三方存储里,如缓存redis。数据库中每个表行数,对应redis中的一个key的值。每当向其中一个表增加1行数据时,redis中对应key的值就增加1,每当表中删除1行数据时,redis中对应key的值就减1。而且在redis中进行读写的效率都很高,看似很完美的一个方案。却存在以下两个问题:

1.redis本质上是一个缓存,可能存在数据丢失。

redis中的数据不能永久的存放在内存中,当然redis支持数据持久化(AOF和RDB),但是这样也难以保证数据一定不丢失。试想,刚插入了一条数据到数据库中,redis中对应key的值也加1了,然后redis就异常重启了,此时日志数据还没有进行持久化,那么redis重启后,最近的这次加1计数就丢失。

不过针对这问题,有一个解决方案:当redis重启后,可以直接通过数据库统计该表中的数据行数,然后加载到redis中,虽然这次从数据库中统计数据行数的过程比较耗时,但是redis重启的概率相对较低,因此这种查数据获取总行数的情况概率也比较低,工程实现上也是可以接受的。

2.并发问题引起逻辑统计错误

试想这样一个需求:一个接口要查询某个表中数据行数和最近一次插入的数据。由于插入数据到数据库和更新redis计数,是两个操作,而且这两个操作不是原子性的,如果在执行这个两个操作期间有接口的查询,那么就有可能导致接口查询的数据存在不一致,具体如下:
1.先插入数据到数据库,在更新redis计数

t1时刻插入一条数据,

t2时刻进行接口查询,

t3时刻redis计数更新。

那么接口返回的最近一次插入的数据是最新的,但是返回的数据表行数却没有累加上最新的那行数据。 
    
2.先更新redis计数,在插入数据到数据库中

t1时刻更新redis计数。

t2时刻进行接口查询。

t3时刻插入数据到数据库。

那么接口返回的数据表行数,累计上了最新增加的这行数据,但是返回的最近一次插入的数据却不是最新的。

3.使用mysql的事务特性

方案2中,之所以无法保证接口查询返回结果的一致性,主要原因在于:插入数据到数据库和更新redis中的计数,这两个操作不是原子性的,两个操作的中间状态可以被其他回话访问到。

在msyql数据库中,可重复读事务隔离级别,可以有效的实现一个事务的中间状态对另外一个事务的不可见。此时只需要将 redis更换成 mysql中的一个数据表,就可以有效避免方案2中的数据不一致的问题。

把redis替换成mysql表,再来看一下是否还会存在上述问题:
1.先插入数据到数据库,在更新mysql表计数

t1时刻开启事务并插入一条数据。

t2时刻进行接口查询。

t3时刻mysql数据表计数更新并提交事务。

​由于会话A在是在t3时刻才对事务进行提交,那么t2时刻,接口的对数据表的查询,是感知不到会话A对数据库的更新的。 因此接口查询的行数信息和最近一条数据信息是一致的。


2.先更新mysql表计数,在插入数据到数据库中


 同样的道理,事务B只能访问到事务A开启前的数据状态,和事务A提交后的后的数据状态,无法查看事务A的中间状态,所以接口的查询结果,也就不会存在不一致的问题。

希望通过这篇文章,老铁们可对count有一个更深的理解,对于计数的统计,如果你还有更好的方案,欢迎在在评论区留言。
 

猜你喜欢

转载自blog.csdn.net/weixin_45701550/article/details/111129508
今日推荐