MySQL count() 函数咱们并不陌生,用来统计每张表的函数。但若是你的表愈来愈大,而且是 InnoDB 引擎的话,会发现计算的速度会愈来愈慢。在这篇文章里,会先介绍 count() 实现的原理及缘由,而后是 count 不一样用法的性能分析,最后给出须要频繁改变并须要统计表行数的解决方案。html
InnoDB 和 MyISAM 是 MySQL 经常使用的数据引擎,因为二者实现的不一样,致使 count() 操做计算的效率也不一样。数据库
对于 MyISAM 来讲,它把每一个表的总行数都存在了磁盘上,所以使用 count(*) 计算时,效率很高直接返回结果。但若是加入了 where 条件,依然会进行搜索,因此效率是不高的。缓存
对于 InnoDB 来讲,在进行 count(*) 运算时,会把数据从引擎中一行行读出来,而后累计计数,天然表大了以后,效率就变低了。并发
那么,为何 InnoDB 不能像 MyISAM 在表中记录呢?缘由就在于 InnoDB 比 MyISAM 多了支持事务的特性,同时也须要必定的取舍。因为 MVCC 的控制,使得 MySQL 具备并发的能力,也就是说对于同一时刻,InnoDB 返回的表的行数是不必定的,事务看到的行数与开启后的一致性视图有关,换句话说,每一个事务能看到的数据版本是不同的,只能一行行拿出来进行判断。函数
像下面的事务,假设表 t 有 10000 条数据:性能
Session A | Session B | Session C |
---|---|---|
select count(*) from t; | ||
insert into t (); | ||
begin; | ||
insert into t(); | ||
select count(*) from t; | select count(*) from t; | select count(*) from t; |
10000; | 结果是 10002 | 结果是 10001 |
对于 Session A 来讲,Session B 未提交不可见,Session C 提交了,可是在 Session A 启动后提交的,也不可见。因此是 10000.优化
而对于 Session B 而言,Session C 在启动以前提交,本身又插入了一条,因此结果是 10002.设计
其实 InnoDB 在进行 count(*) 操做时,仍是作了优化的,在进行 count(*) 操做时,因为普通索引会保存主键的 id 值,因此会找到最小的那颗普通索引树进行查找,而不是去遍历主键索引树。code
在保证逻辑正确的前提下,减小扫描的数据量,是数据库系统设计的通用法则。server
另外在使用 show table status
时,也能够查询出行数,并且速度很快,但须要注意的是,该命令是经过索引统计的值来采样估算的。官方文档说偏差能够有 40%-50%.
但若是咱们真的须要实时的获取的某个表的行数,应该怎么办呢?
对于进行更新的表,可能会想到用缓存系统来支持。好比 Redis 里来保存某个表总行数。
每次插入数据库时,Redis 计数加一,相反则减一,这样看起来读写操做都很快,但会存在一些问题。
缓存系统会丢失更新:
对于 Redis 在内存中的数据,须要按期的同步到磁盘中,但对于 Redis 异常重启,就没有办法了。好比在 Redis 中插入后,Redis 重启,数据没有持久化到硬盘。这时能够在重启 Redis 后,从数据库执行下 count(*) 操做,而后更新到 Redis 中。一次全表扫描仍是可行的。
逻辑不精确:
假设一个页面中,须要显示一张表的行数,以及每一条数据。在实现时,能够先从 Redis 取数量,而后从数据库里取记录。
但可能会出现这样的状况:
Session A | Session B | |
---|---|---|
插入一条数据; | T1 | |
读 Redis 计数; | T2 | |
从数据库中查记录; | ||
Redis 计数加 1; | T3 |
对于 Session B 来讲,在 T2 时刻,会发现 Redis 的数量比数据库少 1 条。
Session A | Session B | |
---|---|---|
Redis 计数加 1; | T1 | |
读 Redis 计数; | T2 | |
从数据库中查记录; | ||
插入一条数据; | T3 |
对于 Session B 来讲,在 T2 时刻,会发现 Redis 的数量比数据库多 1 条。
其实产生问题的缘由就是由于 Redis 和数据库查记录没有在同一个事务中。
因为 InnoDB 引擎的支持,MySQL 自己是支持事务的,因此将 Redis 的插入操做换成在数据库的更新操做,就能够利用在RR级别下的事务特性,进而保证数据的精确性。
并且还有一点,因为 redo log 的支持,在 MySQL 发生异常时,是能够保证 crash-safe。
count() 自己是一个聚合函数,对于返回的结果集,一行行地判断。若是参数不是 NULL 的话,会一直累加,最后返回结果。
因此 count(*), count(id), count(1) 表示都是返回知足条件的结果集总行数。
而 count(字段),则表示知足条件的数据行里,不为 NULL 的字段。
对于 count(id) 来讲,InnoDB 会遍历整张表,把每行 id 取出来,给 server 层。Server 判断 id 是否为空,而后累加。
对于 count(1) 来讲,InnoDB 会遍历整张表,但不取值。Server 层会本身放入 1,而后累加。
因此对于 count(1) 的执行会比 count(*) 要快,少了解析数据行以及拷贝字段值的操做。
对于 count(字段) 来讲,若是字段定义时是 not null, 会一行行读出,并判断不能为 null,而后累加。若是定义时能够为 null,执行时,须要将值去除,判断不是 null 才累加。
count(*) 除外,专门作了优化,不取值,直接按行累加,而且会找到最小的索引树进行计算。
MySQL count() 函数的执行效率和底层的数据引擎有关。MyISAM 不加 where 条件,查询会很快,但不支持事务。InnoDB 支持事务,因为 MVCC 的实现,致使每次查询都须要一行行的扫描,效率不高。
解决方法能够经过设计外部缓存如 Redis,保存记录。但存在异常重启和数据不许确的状况。能够经过在 InnoDB 中新建一张表,保存记录这样的解决方案。
最后,InnoDB 对 count(*) 作了独立的优化,而其余的 count 操做,则须要额外的操做。