在MySQL中count(*)函数的优化

写这篇文章以前已经看过了不少数据库方面的优化内容,大部分都是加索引、使用事务、要什么select什么等等。然而,只是停留在阅读的层面上,不多有实践,由于没有遇到真实的项目,一切都是纸上谈兵。实践是检验真理的惟一标准,因而就想在数据库上测试一些性能优化的方案,好比索引之类的,可是不想使用假的数据,因而就想着能不能抓取网上的一些数据来做分析,后来本身经过PHP抓取了一些数据(这个博文即将补上),抓了大约110W的用户数据以后,固然须要统计一下具体的数量,因而我使用了如下的SQL语句(我使用的存储引擎是InnoDB):mysql

SELECT COUNT(*) FROM tbl_name;linux

然而,发现须要运行14-20s的时间才能看到结果。sql

这样的时间开销在真实的环境的用户体验是十分差的,试想一下,打开一个页面还要等接近20s才能看到数据,别说20s,就算是3s也是十分差的,因而便想在这方面作优化。数据库

存储引擎

在MySQL中,平常开发中比较经常使用的有MyISAM和InnoDB两种存储引擎。二者之间的其中一个区别是使用count(*)函数计算表的具体行数。性能优化

由于MyISAM会保存表的具体行数,所以这段代码在MyISAM存储引擎中执行,MyISAM只要简单地读出保存好的行数便可。所以,若是表中没有使用事务之类的操做,这是最好的优化方案。然而,InnoDB存储引擎不会保存表的具体行数,所以,在InnoDB存储引擎中执行这段代码,InnoDB要扫描一遍整个表来计算有多少行。数据结构

查询优化命令--Explain

要弄懂查询性能在哪,首先,须要知道致使查询缓慢的瓶颈在哪。explain命令显示的rows是核心的性能指标,rows大,说明mysql须要扫描的行数就多,绝大部分rows大的语句执行必定很快。因此优化语句基本上都是在优化rows。函数

首先,看看上面的语句:性能

能够看到,mysql扫描了整个表来执行本次查询。测试

奇怪的地方

在数据表的设计中,我是添加了惟一索引的,可是后来有一个语句是根据其中一个字段统计数量,当时添加了一个普通的索引,当我再执行了一遍上面的SQL语句,发现只须要0.2-0.3s的时间就能统计出表中的行数。优化

不由吓了一跳,误打误撞就发现了优化的方法:在InnoDB中,除了惟一索引以外,在其余字段添加一个普通索引(称为辅助索引)就可以提高count(*)函数的性能。可是这是为何呢?explain一下:

一样是扫描同样的行数,为何添加一个普通索引就能够提升这么多的性能?因而便开始查找资料和阅读文档弄懂这个问题。

count(*)函数执行原理

正如在不一样的存储引擎中,count(*)函数的执行是不一样的。在MyISAM存储引擎中,count(*)函数是直接读取数据表保存的行记录数并返回,而在InnoDB存储引擎中,count(*)函数是先从内存中读取表中的数据到内存缓冲区,而后扫描全表得到行记录数的。在使用count函数中加上where条件时,在两个存储引擎中的效果是同样的,都会扫描全表计算某字段有值项的次数。

索引原理

由于是添加了索引以后才获得性能上的提高,因而便想到从索引的角度来探索。

根据官方文档上的定义:索引是帮助MySQL高效获取数据的数据结构。能够得知,索引的本质就是数据结构,添加索引的目的就是为了提升查询的效率。

使用索引的查询能够类比到字典,若是要查”mysql“这个单词,咱们首先会定位到m字母,而后在m字母下面的单词中找y字母,以此类推,直到找到mysql这个单词,就能看到它在第几页,而后就去该页获取该单词更多的信息。想象一下,若是没有索引,那你就要在字典里一页一页的翻阅,效率十分低下。使用索引就是经过这样不断地缩小查询的范围来筛选出最终的结果。

那么在数据库也是同样的,但显然在数据库里使用索引要复杂许多。

磁盘存取与预读

通常来讲,索引自己也很大,不可能所有存储在内存中,所以索引每每以索引文件的形式存储在磁盘上。那么数据库在构建索引的时候就须要先从磁盘读取数据了,此时就要产生磁盘I/O消耗。而每次的数据读取,都要经历寻道时间、旋转延迟、传输时间三个部分。寻道时间是指磁臂移动到指定磁道所须要的时间,通常在5ms之内;旋转延迟就是磁盘转速;传输时间指的是将数据从磁盘读出并写入到内存的时间,这个时间较短,能够忽略不计。相对于内存存取,I/O存取的消耗要高几个数量级。所以,评价一个数据结构做为索引的优劣最重要的指标就是查找过程当中磁盘I/O操做次数的渐进复杂度。换句话说,索引的结构组织要尽可能减小查找过程当中磁盘I/O的存取次数。

从上面的描述能够得知磁盘I/O是很是高昂的操做,根据操做系统的局部性原理:

当一个数据被用到时,其附近的数据也一般会立刻被使用。

计算机操做系统在这方面作了一些优化,当一次I/O时,不光把当前磁盘地址的数据读取到内存缓冲区内,并且把相邻的数据也都读取到内存缓冲区内。这样一来,在读取数据时产生的I/O就少了不少了。由于在数据库中,每一次I/O读取的数据咱们称之为一页(page),通常为4k或8k,也就是说,咱们读取一页内的数据时,实际上才发生了一次I/O。

根据以上的描述,咱们能够初步得出结论,增长索引先后的性能差距体如今磁盘读取过程。可是在添加新的索引以前,我是添加了一个惟一索引的,后来发如今mysql中,我添加的惟一索引被称为聚簇索引,然后面添加的索引称为辅助索引,所以,让咱们再来看看聚簇索引和辅助索引的区别。

聚簇索引(clustered index)和辅助索引(secondary index)

聚簇索引(clustered index)

每个InnoDB存储引擎下的表都有一个特殊的索引用来保存每一行的数据,称为聚簇索引。一般状况下,聚簇索引是主键的同义词。在InnoDB中,mysql是这样选择聚簇索引的:

若是表中定义了PRIMARY KEY,那么InnoDB就会使用它做为聚簇索引;

不然,若是没有定义PRIMARY KEY,InnoDB会选择第一个有NOT NULL约束的惟一索引做为PRIMARY KEY,而后InnoDB会使用它做为聚簇索引;

若是表中没有定义PRIMARY KEY或者合适的惟一索引。InnoDB会在一个合成的列中自动生成一个包含行ID的隐含的聚簇索引。这些行使用InnoDB赋予这些表的ID进行排序。行ID是6个字节的字段,且做为新行单一地自增。所以,根据行ID排序的行数据在物理上是根据插入的顺序进行排序。

聚簇索引如何加速查询

由于全部的行数据都跟聚簇索引存放在同一个地方,所以,经过聚簇索引访问数据行会更快。若是表十分大,跟使用不一样地方保存数据和索引的存储组织来讲,聚簇索引的结构会节省不少的I/O操做。(好比说,MyISAM使用了一个文件来保存数据以及另外一个文件保存索引记录)。

辅助索引(secondary index)

除了聚簇索引以外的全部索引都被称为辅助索引。在InnoDB里,辅助索引的每一行记录都包含每一行的主键列,辅助索引指向主键。InnoDB使用这个主键来查找在聚簇索引中的行。若是主键很长,辅助索引会使用更多的空间,所以辅助索引有利于存储引擎拥有长度更短的主键。

结论

所以能够得出结论:

在第一次使用了惟一索引(u_id)的时候,InnoDB使用了惟一索引做为表的聚簇索引。而在InnoDB存储引擎中,count(*)函数是先从内存中读取表中的数据到内存缓冲区,而后扫描全表得到行记录数的。所以,使用惟一索引做为聚簇索引的时候,InnoDB须要先读取110W条的数据到数据缓冲区中,这里发生了不少次I/O,所以形成了主要的时间消耗。而添加了辅助索引后,mysql在执行查询时会使用内部的优化机制:即便用辅助索引来统计数量。辅助索引保存的是index的值,此时只须要读取一个字段,I/O减小了,性能就提升了。所以在InnoDB中,若是有统计整张表的数量的需求,能够考虑增长一个辅助索引。

MySQL InnoDB存储引擎锁机制实验 http://www.linuxidc.com/Linux/2013-04/82240.htm

InnoDB存储引擎的启动、关闭与恢复 http://www.linuxidc.com/Linux/2013-06/86415.htm

MySQL InnoDB独立表空间的配置 http://www.linuxidc.com/Linux/2013-06/85760.htm

MySQL Server 层和 InnoDB 引擎层 体系结构图 http://www.linuxidc.com/Linux/2013-05/84406.htm

InnoDB 死锁案例解析 http://www.linuxidc.com/Linux/2013-10/91713.htm

MySQL Innodb独立表空间的配置 http://www.linuxidc.com/Linux/2013-06/85760.htm