从头开始搞懂 MySQL(08)count(*) 很慢怎么办

本文探讨了MySQL中count(*)查询的实现方式,解释了InnoDB与MyISAM的不同,以及为何InnoDB不直接存储表行数。针对频繁的行计数需求,提出了使用缓存系统(如Redis)或数据库额外表存储计数的解决方案,并分析了这两种方法的优缺点。此外,还对比了count(*)、count(主键)、count(字段)、count(1)的效率差异。
摘要由CSDN通过智能技术生成

在开发系统的时候,我们可能经常需要计算一个表的行数,我们一般都是用 select count(*) from table 来查询,如果系统中记录越来越多,这条 SQL 也越来越慢。为什么 MySQL 不直接记录一个总数,每次要查询的时候直接读出来呢?我们先来了解一下 count(*) 语句是怎么实现的,为什么这么实现

1、count(*) 的实现方式

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

MyISAM

MyISAM 引擎将一个表的总行数存在了磁盘上,所以执行 count(*) 的时候会直接返回这个个数,效率很高。如果加了 where 条件的话,MyISAM 返回的也不能返回这么快

InnoDB

InnoDB 执行 count(*) 的时候会将数据一行行的从引擎中读出来然后累积计数

InnoDB 之所以不跟 MyISAM 一样直接把表的行数存起来,是因为 InnoDB 的事务默认隔离级别是可重复读, 在代码中是通过 MVCC(多版本并发控制)的来实现的,每行记录都要进行判断在当前事务中是否可见,所以 InnoDB 只能把数据一行行的从表中读取出来,可见的行才是基于当前查询的总行数

虽然 InnoDB 是一行行读取数据,但是 MySQL 在 count(*) 的时候还是做了一些优化的

InnoDB 主键索引树的叶子节点是数据,普通索引树的叶子结点是主键值,所以,普通索引树会比主键索引树小很多,count(*) 在遍历哪个索引树得到的结果逻辑上都是一样的,MySQL 优化器会找到最小的那个树来遍历,尽量减少扫描的数量。

MyISAMySQL 和 InnoDB 的区别
  • MyISAM count(*) 比较快,但是不支持事务
  • InnoDB 表 count(*) 会遍历全表,虽然结果准确但是会导致性能问题

2、保存计数的方法

如果我们的系统经常需要展示总数的话,MyISAM 和 InnoDB 又各有缺点,我们可以自己计数,找一个地方将表的行数存起来

2.1 用缓存系统保存计数

我们可以使用 Redis 来保存表的总行数,每插入一条数据计数就加1,删除一行就减 1,这样的话读和更新操作都会很快

但是 Redis 缓存不仅可能会丢失更新,如果在我们向数据表中插入一条数据,Redis 中保存的值也加 1,Redis 异常重启了,重启后我们从 Redis 中把这个值读回来,这个加 1 的计数操作却丢失了。异常重启的话我们可以再重新执行一次 count(*) 获取行数,再将这个值写入到 Redis 中

除了可能会丢失更新,在我们并发的时候,因为我们无法精确控制不同线程的执行时刻,所以可能会出现 Redis 计数不精确的情况

先改 Redis 记录,再写数据表,查询到的数据中可能没有最新插入的,但是 Redis 的计数里面已经加 1 了

先写数据表,再更新 Redis 记录,可能会出现查询到的数据中已经显示最新插入的记录,但是 Redis 的计数还没有加上,出现数据不一致的情况

2.2 在数据库保存计数

用 Redis 来保存 count(*) 查出来的计数可能会有数据丢失或者数据不一致的问题,我们可不可以使用 MySQL 单独设计一张表来存储

首先 InnoDB 是支持崩溃恢复不丢失数据的,而且数据不一致的问题,InnoDB 支持事务,在它的默认隔离级别下,查询使用的是快照读,这样就可以保证在一个查询中,查询数据和数量的结果在逻辑上是一致的

如果我们用事务来保证计数准确,在并发时考虑到系统性能,在事务中我们应该先插入操作记录再更新计数表,InnoDB 语句执行时加锁,事务提交时释放锁,更新计数的语句后执行以缩短持有锁的时间

3、count(*)、count(主键)、count(字段)、count(1)

InnoDB 引擎下,SQL 语句使用 count(*)、count(主键)、count(字段)、count(1) 这些不同的 count() 有什么差别

count() 是一个聚合函数,对于返回的结果集一行行进行判断,如果 count 函数的参数不是 null,累计值就 加 1,最后返回累计值

所以,count(*)、count(主键)、count(1) 都返回的是满足条件的结果集的总行数,count(字段) 返回的是该字段不为 null 的总行数

  • count(字段) 如果字段定义的是 not null 的话,会一行行从记录中读出这个字段,判断不能为 null 后按行累加,如果字段定义的可以为 null,会先把值取出来再判断一下,不是 null 才会累加
  • count(主键) InnoDB 会遍历整张表,把每行的主键取出来,返回给 server 层,判断不为空后再按行累加
  • count(1) InnoDB 也会遍历整张表,但不取值,server 层对返回的每一行,放一个数字 1 进去,判断不能为空后按行累加,count(1) 比 count(主键) 效率要高,使用主键会涉及到解析数据行,拷贝字段值的操作
  • count(*) 专门做了优化,不会把全部字段取出来,不取值,count(*) 肯定不是 null,直接按行累加

按照效率排序的话,count(*) ≈ count(1) > count(主键) > count(字段),我们可以尽量使用 count(*)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一起来搬砖呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值