面试官:MySQL单表过亿数据,如何优化count(*)全表的操作?

教育   2024-12-10 09:02   湖南  
欢迎点击下方👇关注我,记得星标哟~
文末会有重磅福利赠送

最近有好几个同学跟我说,他在技术面试过程中被问到这个问题了,让我找时间系统地讲解一下。

其实从某种意义上来说,这并不是一个严谨的面试题,接下来 show me the SQL,我们一起来看一下。

如下图所示,一张有 3000多万行记录的 user 表,执行全表 count 操作需要 14.8 秒的时间。

接下来我们稍作调整再试一次,神奇的一幕出现了,执行全表 count 操作竟然连 1 毫秒的时间都用不上。

这是为什么呢?

其实原因很简单,第一次执行全表 count 操作的时候,我用的是 MySQL InnoDB 存储引擎,而第二次则是用的 MySQL MyISAM 存储引擎。

这两者的差别在于,前者在执行 count(*)  操作的时候,需要将表中每行数据读取出来进行累加计数,而后者已经将表的总行数存储下来了,只需要直接返回即可。

当然,InnoDB 存储引擎对 count(*)  操作也进行了一些优化,如果该表创建了二级索引,其会通过全索引扫描的方式来代替全表扫描进行累加计数,

毕竟,二级索引值只存储了索引列和主键列两个字段,遍历计数肯定比存储所有字段的数据表的 IO 次数少很多,也就意味着其执行效率更高。

而且,MySQL 的优化器会选择最小的那个二级索引的索引文件进行遍历计数。

所以,这个技术面试题严谨的问法应该是 —— MySQL InnoDB 存储引擎单表过亿数据,如何优化 count(*) 全表的操作?

下面我们就来列举几个常见的技术解决方案,如下图所示:

(1)Redis 累加计数

这是一种最主流且简单直接的实现方式。

由于我们基本上不会对数据表执行 delete 操作,所以当有新的数据被写入表的时候,通过 Redis 的 incr 或 incrby 命令进行累加计数,并在用户查询汇总数据的时候直接返回结果即可。

如下图所示:

该实现方式在查询性能和数据准确性上两者兼得,Redis 需要同时负责累加计数和返回查询结果操作,缺点在于会引入缓存和数据库间的数据一致性的问题。

(2)MySQL 累加计数表 + 事务

这种实现方式跟“Redis 累加计数”大同小异,唯一的区别就是将计数的存储介质从 Redis 换成了 MySQL。

如下图所示:

但这么一换,就可以将写入表操作和累加计数操作放在一个数据库事务中,也就解决了缓存和数据库间的数据一致性的问题。

该实现方式在查询性能和数据准确性上两者兼得,但不如Redis 累加计数”方式的性能高,在高并发场景下数据库会成为性能瓶颈。

(3)MySQL 累加计数表 + 触发器

这种实现方式跟“MySQL 累加计数表 + 事务”的表结构是一样的,如下图所示:


唯一的区别就是增加一个触发器,不用在工程代码中通过事务进行实现了。

CREATE TRIGGER `user_count_trigger` AFTER INSERT ON `user` FOR EACH ROW BEGIN    UPDATE user_count SET count = count + 1 WHERE id = NEW.id;END

该实现方式在查询性能和数据准确性上两者兼得,与MySQL 累加计数表 + 事务”方式相比,最大的好处就是不用污染工程代码了。

(4)MySQL 增加并行线程

在 MySQL 8.014 版本中,总算增加了并行查询的新特性,其通过参数 innodb_parallel_read_threads 进行设定,默认值为 4。

下面我们做个实验,将这个参数值调得大一些:

set local innodb_parallel_read_threads = 16;


然后,我们再来执行一次上文中那个 3000 多万行记录 user 表的全表 count 操作,结果如下所示:


参数调整后,执行全表 count 操作的时间由之前的 14.8 秒,降低至现在的 6.1 秒,是可以看到效果的。

接下来,我们继续将参数值调整得大一些,看看是否还有优化空间:

set local innodb_parallel_read_threads = 32;


然后,我们再来执行一次上文中那个 3000 多万行记录 user 表的全表 count 操作,结果如下所示:


参数调整后,执行全表 count 操作的时间竟然变长了,从原来的 6.1 秒变成了 6.8 秒,看样子优化空间已经达到上限了,再多增加执行线程数量只会适得其反。

该实现方式一样可以保证数据准确性,在查询性能上有所提升但相对有限,其最大优势是只需要调整一个数据库参数,在工程代码上不会有任何改动。

不过,如果数据库此时的负载和 IOPS 已经很高了,那开启并行线程或者将并行线程数量调大,会加速消耗数据库资源。

(5)MySQL 增加二级索引

还记得我们在上文中说的内容吗?

InnoDB 存储引擎对 count(*)  操作也进行了一些优化,如果该表创建了二级索引,其会通过全索引扫描的方式来代替全表扫描进行累加计数,

毕竟,二级索引值只存储了索引列和主键列两个字段,遍历计数肯定比存储所有字段的数据表的IO次数少很多,也就意味着执行效率更高。

而且,MySQL 的优化器会选择最小的那个二级索引的索引文件进行遍历计数。

为了验证这个说法,我们给 user 表中最小的 sex 字段加一个二级索引,然后通过 EXPLAIN 命令看一下 SQL 语句的执行计划:

果然,这个 SQL 语句的执行计划会使用新建的 sex 索引,接下来我们执行一次看看时长:

果不其然,执行全表 count 操作走了 sex 二级索引后,SQL 执行时间由之前的 14.8 秒降低至现在的 10.6 秒,还是可以看到效果的。

btw:大家可能会觉得效果并不明显,这是因为我们用来测试的 user 表中算上主键 ID 只有七个字段,而且没有一个大字段。

反之,user 表中的字段数量越多,且包含的大字段越多,其优化效果就会越明显。

该实现方式一样可以保证数据准确性,在查询性能上有所提升但相对有限,其最大优势是只需要创建一个二级索引,在工程代码上不会有任何改动。

(6)SHOW TABLE STATUS

如下图所示,通过 SHOW TABLE STATUS 命令也可以查出来全表的行数:

我们常用于查看执行计划的 EXPLAIN 命令也能实现:

只不过,通过这两个命令得出来的表记录数是估算出来的,都不太准确。那到底有多不准确呢,我们来计算一下。

公式为:33554432 / 33216098 = 1.01

就这个 case 而言,误差率大概在百分之一左右。

该实现方式一样可以保证查询性能,无论表中有多大量级的数据都能毫秒级返回结果,且在工程代码方面不会有任何改动,但数据准确性上相差较多,只能用作大概估算。

早日上岸!

我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。

没准能让你能刷到自己意向公司的最新面试题呢。

感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。

点击下方文章,看看他们是怎么找到好工作的!

Go就业陪跑训练营,辅导到就业为止!

Java就业陪跑训练营,辅导到就业为止!

我们又出成绩啦!大厂Offer集锦!遥遥领先!

王中阳
公司技术总监,创办就业陪跑服务,辅导学员拿到600多个offer。专注程序员的就业辅导、简历优化、模拟面试等。
 最新文章