当前位置:   article > 正文

第十四讲笔记——count(x) 这么慢,怎么办?

第十四讲笔记——count(x) 这么慢,怎么办?

count(*) 的实现方式

在不同引擎中,count(*) 有不同的实现方式

  • MyISAM 引擎就把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回;
  • InnoDB 就麻烦了,它执行 count(*) 时,全表扫描,累计行数。

这里是没说过滤条件,加了 where 后,MyISAM 也是不能返回这么快的。

为什么 InnoDB 不把全表总行数存起来呢?

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

由于 InnoDB 默认的隔离级别是可重复读,代码上使用的多版本并发控制的。每一行记录都要自己判断可不可见,所以对于 count(*) 来说,只能一行一行的读。

当然 MySQL 还是对 count(*) 操作做了优化。

InnoDB 是索引组织表,普通索引比主键索引小很多。所以 MySQL 优化器会找到最小的那棵树来遍历。

在保证逻辑正确的前提下,尽量减少扫描的数据量,是数据库系统设计的通用法则之一。

使用 show table status 这个命令的话,在结果里有一个 TABLE_ROWS,这个命令跑的很快,可以代替 count(*) 吗? 不行,这个 TABLE_ROWS 是一个估值,不准。

那我们能又快又准确的算出行数吗?

我们可以自己计数。这些方法的基本思路是:我们自己找一个地方,把操作记录表的行数存起来。

用缓存系统保存计数

可以使用 Redis 服务来保存这个表的总行数。这个表每次加一减一,对应 redis 上跟着改变。

但是,可能会丢失更新。

Redis的数据不能永久地留在内存里,所以你会找一个地方把这个值定期地持久化存储起来。但即使这样,仍然可能丢失更新。试想如果刚刚在数据表中插入了一行,Redis中保存的值也加了 1,然后Redis异常重启了,重启后你要从存储redis数据的地方把这个值读回来,而刚刚加1的这个计数操作却丢失了。

当然了,这还是有解的。比如,Redis异常重启以后,到数据库里面单独执行一次count(*)获取真 实的行数,再把这个值写回到Redis里就可以了。异常重启毕竟不是经常出现的情况,这一次全 表扫描的成本,还是可以接受的。

即使 Redis 正常工作,这个值还是逻辑上不精确。

因为使用 Redis 计数,就会涉及到两个非原子操作。增、减数据,Redis上的数增、减。

在并发的时候,一个线程要查 count(*),但此时另一个线程刚刚插入一条数据,还没改 Redis。就会造成问题。

在数据库中保存

如果把这个计数直接放到数据库里单独的一张计数表 C 中,又会怎样呢?

首先,InnoDB 支持崩溃恢复不丢失数据。

并发问题解决了吗?是的,解决了,因为 InnoDB 支持可重复读隔离级别。

会话 B 在 T3 进行查询的时候,由于事务 A 并没有提交,所以这个加一操作对 B 不可见。

因此,会话 B 中看到的结果里,查计数值和“最近100记录”看到的结果一样,逻辑上是一致的。

不同的 count 用法

以下基于 InnoDB 引擎。

count() 的语义:count() 是一个聚合函数,对于返回结果的判断,一行行的判断,如果 count 的参数不是 NULL,累加值就加 1,否则不加。最后返回累计数。

所以 count(*)、count(主键) 和 count(1) 都表示返回满足条件结果集的总行数;而 count(字段) 则返回满足条件的数据行里,参数“字段”不为 NULL 的总个数。

对于 count(主键 id) 来说,InnoDB 引擎会遍历整张表,把每一行的 id 值取出来,返回给 server 层。server 层拿到 id 后,判断不可能为空,就按行累加。

对于 count(1) 来说,InnoDB 引擎会遍历整张表,但不取值。server 层对于返回的每一层,放一个数字“1”进去,判断不可能为空,按行累加。

单说这两个 count() 的话,count(1) 是比 count(主键id) 快,因为不涉及解析行数据,以及拷贝值的操作。

对于 count(字段) 来说

  1. 如果这个字段定义为 NOT NULL,一行行从记录里读出,判断不可能为空,则累加。
  2. 如果字段定义为运行为 NULL,那么读出后还得判断是否为空。不是空,才累加。

也就是 server 层要什么字段,InnoDB 就返回什么字段。

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

按照排序效率的话, c o u n t ( 字 段 ) < c o u n t ( 主 键 i d ) < c o u n t ( 1 ) ≈ c o u n t ( ∗ ) count(字段)\lt count(主键id)\lt count(1) \approx count(*) count()<count(id)<count(1)count(),所以建议使用 count(*)。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/535197
推荐阅读
相关标签
  

闽ICP备14008679号