浅谈MySQL 统计行数的 count
MySQLcount()函数我们并不陌生,用来统计每张表的行数。但如果你的表越来越大,且是InnoDB引擎的话,会发现计算的速度会越来越慢。在这篇文章里,会先介绍count()实现的原理及原因,然后是count不同用法的性能分析,最后给出需要频繁改变并需要统计表行数的解决方案。
Count()的实现
InnoDB和MyISAM是MySQL常用的数据引擎,由于两者实现的不同,导致count()操作计算的效率也不同。
对于MyISAM来说,它把每个表的总行数都存在了磁盘上,因此使用count(*)计算时,效率很高直接返回结果。但如果加入了where条件,依然会进行搜索,所以效率是不高的。
对于InnoDB来说,在进行count(*)运算时,会把数据从引擎中一行行读出来,然后累计计数,自然表大了之后,效率就变低了。
那么,为什么InnoDB不能像MyISAM在表中记录呢?原因就在于InnoDB比MyISAM多了支持事务的特性,同时也需要一定的取舍。由于MVCC的控制,使得MySQL具有并发的能力,也就是说对于同一时刻,InnoDB返回的表的行数是不一定的,事务看到的行数与开启后的一致性视图有关,换句话说,每个事务能看到的数据版本是不一样的,只能一行行拿出来进行判断。
像下面的事务,假设表t有10000条数据:
SessionA | SessionB | SessionC |
selectcount(*)fromt; | ||
insertintot(); | ||
begin; | ||
insertintot(); | ||
selectcount(*)fromt; | selectcount(*)fromt; | selectcount(*)fromt; |
10000; | 结果是10002 | 结果是10001 |
对于SessionA来说,SessionB未提交不可见,SessionC提交了,但是在SessionA启动后提交的,也不可见。所以是10000.
而对于SessionB而言,SessionC在启动之前提交,自己又插入了一条,所以结果是10002.
其实InnoDB在进行count(*)操作时,还是做了优化的,在进行count(*)操作时,由于普通索引会保存主键的id值,所以会找到最小的那颗普通索引树进行查找,而不是去遍历主键索引树。
在保证逻辑正确的前提下,减少扫描的数据量,是数据库系统设计的通用法则。
另外在使用showtablestatus时,也可以查询出行数,而且速度很快,但需要注意的是,该命令是通过索引统计的值来采样估算的。官方文档说误差可以有40%-50%.
但如果我们真的需要实时的获取的某个表的行数,应该怎么办呢?
手动保存表的数量
用缓存系统来保存计数
对于进行更新的表,可能会想到用缓存系统来支持。比如Redis里来保存某个表总行数。
每次插入数据库时,Redis计数加一,相反则减一,这样看起来读写操作都很快,但会存在一些问题。
缓存系统会丢失更新:
对于Redis在内存中的数据,需要定期的同步到磁盘中,但对于Redis异常重启,就没有办法了。比如在Redis中插入后,Redis重启,数据没有持久化到硬盘。这时可以在重启Redis后,从数据库执行下count(*)操作,然后更新到Redis中。一次全表扫描还是可行的。
逻辑不精确:
假设一个页面中,需要显示一张表的行数,以及每一条数据。在实现时,可以先从Redis取数量,然后从数据库里取记录。
但可能会出现这样的情况:
- 数据库查到100行结果里有最新插入的记录,而Redis计数里少1.
- 数据库查到100行结果没有最新的记录,但Redis计数却多了1.
SessionA | SessionB | |
插入一条数据; | T1 | |
读Redis计数; | T2 | |
从数据库中查记录; | ||
Redis计数加1; | T3 |
对于SessionB来说,在T2时刻,会发现Redis的数量比数据库少1条。
SessionA | SessionB | |
Redis计数加1; | T1 | |
读Redis计数; | T2 | |
从数据库中查记录; | ||
插入一条数据; | T3 |
对于SessionB来说,在T2时刻,会发现Redis的数量比数据库多1条。
其实产生问题的原因就是因为Redis和数据库查记录没有在同一个事务中。
用数据库保存
由于InnoDB引擎的支持,MySQL本身是支持事务的,所以将Redis的插入操作换成在数据库的更新操作,就可以利用在RR级别下的事务特性,进而保证数据的精确性。
而且还有一点,由于redolog的支持,在MySQL发生异常时,是可以保证crash-safe。
不同count用法的执行效率
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(字段)来说,如果字段定义时是notnull,会一行行读出,并判断不能为null,然后累加。如果定义时可以为null,执行时,需要将值去除,判断不是null才累加。
count(*)除外,专门做了优化,不取值,直接按行累加,并且会找到最小的索引树进行计算。
总结
MySQLcount()函数的执行效率和底层的数据引擎有关。MyISAM不加where条件,查询会很快,但不支持事务。InnoDB支持事务,由于MVCC的实现,导致每次查询都需要一行行的扫描,效率不高。
解决方法可以通过设计外部缓存如Redis,保存记录。但存在异常重启和数据不准确的情况。可以通过在InnoDB中新建一张表,保存记录这样的解决方案。
最后,InnoDB对count(*)做了独立的优化,而其他的count操作,则需要额外的操作。
以上就是浅谈MySQL统计行数的count的详细内容,更多关于Mysqlcount的资料请关注毛票票其它相关文章!