深入理解count*为什么这么慢
- 不同存储引擎的实现方式
- InnoDB为什么不存起来
- MySQL的优化
- show tables status不准确
曾经在依次面试中被问到过这么一个问题,假设开发一个交易平台过程中,有遇到过需要计算交易记录总数的情况该怎么办。可能大多数人的回答都是用select count(*) from t 不就搞定了吗 但是,面试官又问到随着系统中记录数越来越多,这条语句执行得越来越慢。那么为什么这么慢呢,今天我们就来聊一聊原因吧。
不同存储引擎的实现方式
MySQL在不同的存储引擎中,count(*)的实现方式也不相同。
- 一般来说MyISAM引擎效率比较高,因为它是把一个表的总行数存到了磁盘上,因此执行count(*)的时候回直接返回计数值。但是如果加上where的话,MyISAM的表也不能反应这么快。
- InnoDB引擎它执行count(*)的时候比较麻烦,它需要把数据一行一行地从引擎里面读出来,然后再进行累计计数。
InnoDB为什么不存起来
即使同一时刻的多个查询,由于多版本并发控制(MVCC)的原因,事务T启动的时候会创建一个视图read-view,InnoDB的表根据视图进行计数的。因此不能确定应该返回多少行。下面我们举个例子来解释一下:
假设表t1有5000条记录,三个用户并行的会话。
- 会话1先启动事务并查询一次表的总行数
- 会话2先启动事务,插入一条记录后再查询表的总行数
- 会话3先启动一个单独的语句,插入一条语句后再查询表的总行数。
会话1 | 会话2 | 会话3 |
---|---|---|
begin; | ||
select count(*) from t1; | ||
insert into t1 () | ||
begin; | ||
insert into t1 () | ||
select count(*) from t1(返回5000) | select count(*) from t1(返回5002) | select count(*) from t1(返回5001) |
最后,三个会话会同时查询表t1的总行数,但拿到的结果却不相同。这个原因其实和事务的设计有一定关系。一般默认隔离级别是可重复读(REPATABLE-READ)
而可重复读是通过MVCC来实现。每一行记录都要判断是否对这个会话可见。因此就count(*)来说,InnoDB只好把数据逐行地读出并判断,只有可见的行才能被用来计算表的总行数。
MySQL的优化
MySQL实际上是做了优化的,InnoDB是索引组织表,主键索引树的叶子节点是数据,而普通索引树的叶子节点是主键值。因此普通索引树比主键索引树小很多。对于count(*)操作遍历哪棵树逻辑上来讲结果都是一样的。因此,为了遵循减少数据量遍历的原则,MySQL的优化器会找到最小的那颗树来进行遍历。
这里提到的主键索引树和普通索引树是什么意思?我们来举个例子:
假设有张InnoDB表t (id PK, name KEY, sex, flag)
表中有四条记录:
1, song, m, A
3, zhang, m, A
5, li, m, A
9, wang, f, B
则它们的索引树如下图所示:
假设要执行语句select * from t where name=’song’;
(1)首先在name普通索引上查询到PK=1;
(2)再在主键索引树上查询到(1,song, m, A)的行记录;
show tables status不准确
其实MySQL有条命令show tables status命令执行结果中有条Rows能用于显示表有多少行,比如我们有张表结构是这样的
插入数据后,执行show table status命令结果如下:
这条命令执行比count(* )快,那用它来代替count(*)岂不是很快。但是不行,为什么呢?
它不够准确,因为它的值是根据MySQL采样统计估算出来的,官方统计,误差还是可能会达到40%-50%。