MySQL数据库中的count的用法

 

一、概念

在开发系统的时候,可能常常须要计算一个表的行数,好比一个交易系统的全部变动记录总数。这时候你可能会想,一条select count(*) from t 语句不就解决了吗?redis

 

可是,会发现随着系统中记录数愈来愈多,这条语句执行得也会愈来愈慢。而后你可能就想了,MySQL怎么这么笨啊,记个总数,每次要查的时候直接读出来,不就行了吗。数据库

 

二、count(*)实现方式

在不一样的MySQL引擎中,count(*)有不一样的实现方式。缓存

 

1)MyISAM引擎把一个表的总行数存在了磁盘上,所以执行count(*)的时候会直接返回这个数,效率很高;安全

2)而InnoDB引擎就麻烦了,它执行count(*)的时候,须要把数据一行一行地从引擎里面读出来,而后累积计数。并发

 

这里须要注意的是,咱们在这篇文章里讨论的是没有过滤条件的count(*),若是加了where 条件的话,MyISAM表也是不能返回得这么快的。分布式

 

为何要使用InnoDB,由于不管是在事务支持、并发能力仍是在数据安全方面,InnoDB都优于MyISAM。我猜你的表也必定是用了InnoDB引擎。这就是当你的记录数愈来愈多的时候,计算一个表的总行数会愈来愈慢的缘由。函数

 

为何innodb不和myisam同样呢,也把数字存起来。性能

这是由于即便是在同一个时刻的多个查询,因为多版本并发控制(MVCC)的缘由,InnoDB表“应该返回多少行”也是不肯定的。优化

 

假设表t中如今有10000条记录,咱们设计了三个用户并行的会话。线程

 

会话A先启动事务并查询一次表的总行数;

会话B启动事务,插入一行后记录后,查询表的总行数;

会话C先启动一个单独的语句,插入一行记录后,查询表的总行数。

在最后一个时刻,三个会话A、B、C会同时查询表t的总行数,但拿到的结果却不一样。

 

这和InnoDB的事务设计有关系,可重复读是它默认的隔离级别,在代码上就是经过多版本并发控制,也就是MVCC来实现的。每一行记录都要判断本身是否对这个会话可见,所以对于count(*)请求来讲,InnoDB只好把数据一行一行地读出依次判断,可见的行才可以用于计算“基于这个查询”的表的总行数。

 

InnoDB是索引组织表,主键索引树的叶子节点是数据,而普通索引树的叶子节点是主键值。因此,普通索引树比主键索引树小不少。对于count(*)这样的操做,遍历哪一个索引树获得的结果逻辑上都是同样的。所以,MySQL优化器会找到最小的那棵树来遍历。在保证逻辑正确的前提下,尽可能减小扫描的数据量,是数据库系统设计的通用法则之一。

 

 

若是你用过show table status 命令的话,就会发现这个命令的输出结果里面也有一个TABLE_ROWS用于显示这个表当前有多少行,这个命令执行挺快的,那这个TABLE_ROWS能代替count(*)吗?

 

实际上,TABLE_ROWS就是从这个采样估算得来的,所以它也很不许。有多不许呢,官方文档说偏差可能达到40%到50%。因此,show table status命令显示的行数也不能直接使用。

 

这里进行总结一下:

MyISAM表虽然count(*)很快,可是不支持事务;

show table status命令虽然返回很快,可是不许确;

InnoDB表直接count(*)会遍历全表,虽然结果准确,但会致使性能问题。

 

三、用缓存系统保存计数

对于更新很频繁的库来讲,你可能会第一时间想到,用缓存系统来支持。

你能够用一个Redis服务来保存这个表的总行数。这个表每被插入一行Redis计数就加1,每被删除一行Redis计数就减1。这种方式下,读和更新操做都很快,但你再想一下这种方式存在什么问题吗?

没错,缓存系统可能会丢失更新。

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

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

但实际上,将计数保存在缓存系统中的方式,还不仅是丢失更新的问题。即便Redis正常工做,这个值仍是逻辑上不精确的。

你能够设想一下有这么一个页面,要显示操做记录的总数,同时还要显示最近操做的100条记录。那么,这个页面的逻辑就须要先到Redis里面取出计数,再到数据表里面取数据记录。

咱们是这么定义不精确的:

  1. 一种是,查到的100行结果里面有最新插入记录,而Redis的计数里还没加1;
  2. 另外一种是,查到的100行结果里没有最新插入的记录,而Redis的计数里已经加了1。

这两种状况,都是逻辑不一致的。

咱们一块儿来看看这个时序图。

图2中,会话A是一个插入交易记录的逻辑,往数据表里插入一行R,而后Redis计数加1;会话B就是查询页面显示时须要的数据。

在图2的这个时序里,在T3时刻会话B来查询的时候,会显示出新插入的R这个记录,可是Redis的计数还没加1。这时候,就会出现咱们说的数据不一致。

你必定会说,这是由于咱们执行新增记录逻辑时候,是先写数据表,再改Redis计数。而读的时候是先读Redis,再读数据表,这个顺序是相反的。那么,若是保持顺序同样的话,是否是就没问题了?咱们如今把会话A的更新顺序换一下,再看看执行结果。

 

你会发现,这时候反过来了,会话B在T3时刻查询的时候,Redis计数加了1了,但还查不到新插入的R这一行,也是数据不一致的状况。

在并发系统里面,咱们是没法精确控制不一样线程的执行时刻的,由于存在图中的这种操做序列,因此,咱们说即便Redis正常工做,这个计数值仍是逻辑上不精确的。

 

四、数据库保存计数

根据上面的分析,用缓存系统保存计数有丢失数据和计数不精确的问题。那么,若是咱们把这个计数直接放到数据库里单独的一张计数表C中,又会怎么样呢?

 

首先,这解决了崩溃丢失的问题,InnoDB是支持崩溃恢复不丢数据的。

 

 

五、不一样的count的用法

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

 

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

 

至于分析性能差异的时候,你能够记住这么几个原则:

 

server层要什么就给什么;

 

InnoDB只给必要的值;

 

如今的优化器只优化了count(*)的语义为“取行数”,其余“显而易见”的优化并无作。

 

这是什么意思呢?接下来,咱们就一个个地来看看。

 

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

 

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

 

单看这两个用法的差异的话,你能对比出来,count(1)执行得要比count(主键id)快。由于从引擎返回id会涉及到解析数据行,以及拷贝字段值的操做。

 

可是count(*)是例外,并不会把所有字段取出来,而是专门作了优化,不取值。count(*)确定不是null,按行累加。

 

看到这里,你必定会说,优化器就不能本身判断一下吗,主键id确定非空啊,为何不能按照count(*)来处理,多么简单的优化啊。

 

固然,MySQL专门针对这个语句进行优化,也不是不能够。可是这种须要专门优化的状况太多了,并且MySQL已经优化过count(*)了,你直接使用这种用法就能够了。

 

因此结论是:按照效率排序的话,count(字段)<count(主键id)<count(1)≈count(*),因此建议,尽可能使用count(*)。

 

六、小结

提到了在不一样引擎中count(*)的实现方式是不同的,也分析了用缓存系统来存储计数值存在的问题。

 

其实,把计数放在Redis里面,不可以保证计数和MySQL表里的数据精确一致的缘由,是这两个不一样的存储构成的系统,不支持分布式事务,没法拿到精确一致的视图。而把计数值也放在MySQL中,就解决了一致性视图的问题。

 

InnoDB引擎支持事务,咱们利用好事务的原子性和隔离性,就能够简化在业务开发时的逻辑。这也是InnoDB引擎备受青睐的缘由之一。

相关文章
相关标签/搜索