MySQL之count(*)查询原理

我们在实际开发的过程中肯定常常会遇到需要统计某个表总数的场景,我们自然而然的会想到使用

select count(*) from talbe 

count(*)实现方式

在MySQL中不同的存储引擎对于count(*)有不同的实现方式。

  • MyISAM引擎把一个表的总行数存在了磁盘上,因此执行count(*)的时候会直接返回这个数,效率很高
  • InnoDB引擎在执行count(*)时,需要把数据一行一行地从引擎中读取出来,然后累计计数。

上述描述的是在没有where条件下的情况,如果加了where条件MyISAM也不会返回这么快。

这里我们会有一个疑问,为什么InnoDB不像MyISAM一样把数字存储起来呢?

这是因为即使在同一时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB表,应该返回对少行也是不确定的。

下面有一个场景:

假设表 t 中现在有 10000 条记录,我们设计了三个用户并行的会话。

  • 会话 A 先启动事务并查询一次表的总行数;
  • 会话 B 启动事务,插入一行后记录后,查询表的总行数;
  • 会话 C 先启动一个单独的语句,插入一行记录后,查询表的总行数

最后会看到结果 ,三个会话的结果各不相同。

这个和InnoDB的事务设计有关系,可重复读是它的默认隔离级别,在代码上就是通过多版本并发控制,也就是MVCC来实现的。每一行记录都要判断自己是否对这个会话可见,因此对count(*)请求来说 ,InnoDB只好把所有的数据一行行的读出来,可见行可能用于计算总行数。

InnoDB是索引组织表,主索引树的叶子节点是数据,而普通索引树的叶子节点是主键值,所以普通索引树比主键索引树小很多。对于count(*)这样的操作,遍历主键索引树的结果和普通所引述的结果是一样的,因此,MySQL优化器会找到那个最小的那棵树来遍历,在保证逻辑正确的前提下,尽量少的扫描数据量。

我们也可以通过 

show table status

来获取到TABLE_ROWS字段,但是这个是通过索引统计的值估算出来的 ,所以并不准确。

  • MyISAM 表虽然 count(*) 很快,但是不支持事务;
  • show table status 命令虽然返回很快,但是不准确;
  • InnoDB 表直接 count(*) 会遍历全表,虽然结果准确,但会导致性能问题;

那么如果我们需要一个页面计数的功能要怎么实现呢?

自己进行计数: 也就是说需要自己找个地方,把操作记录的行数存起来。

我们第一个想到的就是放到缓存中。例如,用redis进行缓存计数信息。

每添加一行数据,对redis缓存中数加1,删除一行数据,对缓存数据减1。这种方式的读写都很快,但是会存在一个问题。

缓存系统可能丢失更新。

redis并不能把数据永久的留在内存里面,所以需要找一个地方定期的把数据持久化存储起来,即使这样也会存在丢失更新的问题,假如你刚插入一条数据到数据库中,redis数据值也加1了,在这是redis挂了,还没来得及持久化数据到磁盘,等我们恢复了redis,就会丢失本次更新的数值。当然,你可以选择在每次redis挂了之后重新对数据库进行count(*)操作,由于redis宕机情况属于小概率事件,所以这种程度的查库也是可以接受的。

但是就算这样,使用redis缓存还是会存在逻辑上的不精确问题。

设想一下有一个页面需要查询显示操作总记录数,同时也要显示最近操作的100条记录。那么我们的逻辑是,先到redis取出计数,再到数据库中取出数据记录。

逻辑不精确存在下面两种情况

  • 一种是,查到100行结果里面有最新的插入记录,而redis里面还没加1
  • 另一种是,查询到100行结果里面没有最新插入记录,而redis记录加1

会话A,向数据库插入一条记录R ,在向redis计数加1之前,会话B查询了最近的100条记录。这是就存在会话B查询的记录包含最新的值,但redis还每来得及加1

会话A先计数加1,还没来的及去插入最新记录去数据库,这是会话B查询了最新的100条记录,最后redis加1了,但是查询的100条记录没有最新的插入值。

在并发系统里面,我们是无法精确控制不同线程的执行时刻的,因为存在图中的这种操作序列,所以,我们说即使 Redis 正常工作,这个计数值还是逻辑上不精确的。

既然在redis里面不行,那么我们考虑在数据库保存计数。

单独新建一个C表用来保存计数记录。

首先InnoDB是支持数据的崩溃恢复的。

现在其实无非就是把redis的操作,改成了在数据库种操作C表。首先InnoDB是支持事务的,从而导致InnoDB表不能直接把count(*)直接保存起来,然后在查询的时候返回,但是我们恰恰可以利用事务来解决这个问题。

会话A中给C表中数据加1,在还没来的及插入R数据是,会话A的事务并没有结束,所以对会话B是不可见的。因此会话B在T3时刻查询的最新100条记录是不包含新增1的值的。从而保证了逻辑一致性。

看完了这些,我们有一个疑问,既然用count(*) 那么count(1) count(id)呢? 这些不同的count方法 ,谁更快呢?

首先我们需要知道count()函数,count()是一个聚合函数,对于返回的结果集,一行行的判断,如果count函数的参数不是NULL,累计就加1,否则不加,最后返回累加值。

所以 count(*) count(1) count(id)都表示返回满足条件的结果集的总行数,而count(字段),则表示返回满足条件行数里,参数字段不为NULL的总个数。

性能差别从以下几点进行判断

  • server层要什么就给什么
  • InnoDB只要必要的值
  • 现在优化器对count(*)的语义取行数进行优化,没有优化其他的。
  1. 对于count(id)来说,InnoDB引擎会遍历整张表,把每一行的id取出来,返回给server层,server层拿到id,判断不为空 就按行累加。
  2. 对于count(1)来说,InnoDB引擎会遍历整张表,但不取值,server层会对返回的每一行,放一个数字1进去,判断不为空,按行累加。

但从上面两个来看 count(1)是比count(id)快的,因为从引擎返回id会设计到解析数据行,以及拷贝字段值操作。

对于 count(字段) 来说

如果这个“字段”是定义为 not null 的话,一行行地从记录里面读出这个字段,判断不能为 null,按行累加;

如果这个“字段”定义允许为 null,那么执行的时候,判断到有可能是 null,还要把值取出来再判断一下,不是 null 才累加。

也就是前面的第一条原则,server 层要什么字段,InnoDB 就返回什么字段。

但是 count(*) 是例外:

并不会把全部字段取出来,而是做了专门的优化,不取值,count(*)肯定不是NULL,按行累加。

所以结论是:按照效率排序的话,count(字段)<count(id)<count(1)≈count(*)。所以建议尽量使用count(*)。

其实,把计数放在 Redis 里面,不能够保证计数和 MySQL 表里的数据精确一致的原因,是这两个不同的存储构成的系统,不支持分布式事务,无法拿到精确一致的视图。而把计数值也放在 MySQL 中,就解决了一致性视图的问题。

InnoDB 引擎支持事务,我们利用好事务的原子性和隔离性,就可以简化在业务开发时的逻辑。这也是 InnoDB 引擎备受青睐的原因之一。

 

おすすめ

転載: blog.csdn.net/JIY743761374/article/details/105339709