从原理分析count(*) count(1) count(col)

前言

count(*)和count(1):计算表一共有多少行,包含字段为NULL的行。
count(字段):统计该字段在表中值为非NULL的行数。

count()原理简述:count()是MySQL提供的聚合函数,执行时,数据库引擎层会把需要的数据返回给server层,server层拿到数据之后符合条件的数据就在结果总数中累加一个值。

覆盖索引:如果一个索引包含(或覆盖)所有需要查询的字段的值,称为“覆盖索引”。即只需要扫描索引而无需回表取数据。使用explain查看执行计划,Extra取值为Using where。

INnoDB引擎采用聚簇索引,数据文件和索引文件存放在一个文件中(XX.ibd),比如数据库有一张表为user.sql,则在磁盘上的存储文件为user.ibd和user.frm(结构文件)。

MYISAM引擎,数据文件(XX.MYD)和索引文件(XX.MYI)分开存放,比如数据库有一张表为user.sql,则在磁盘上的存储文件为user.myi和user.myd、user.frm(结构文件)。

InnoDB 是索引组织表,主键索引树的叶子节点是数据,而普通索引树的叶子节点是主键值。所以,普通索引树比主键索引树小很多。对于 count() 这样的操作,遍历哪个索引树得到的结果逻辑上都是一样的。因此,MySQL 优化器会找到最小的那棵树来遍历。因此表有二级索引,则使用二级索引key_len最小的索引进行扫描,尽管这个二级索引的key_len的值大于主键,都使用二级索引。

关于提到的主键索引和辅助索引及更多相关知识点击:
《MySQL索引详解》

这里主要讨论MySQL的默认存储引擎:InnoDB。

对比

count(主键id)

InnoDB引擎会遍历索引,把每一行的id值都取出来返回给server层,server层判断到这是用主键id查询的,值不会为NULL,所以server层拿到id后就在结果总数中累加一个值。

count( * )

InnoDB引擎会遍历索引,把每一行的id值都取出来返回给server层,所以server层拿到id后就在结果总数中累加一个值。

count(1)

InnoDB引擎遍历索引,但不取数据,而是每读一条记录返回一个“1”,server层也不会去判断是否为NULL,然后在结果总数中累加一个值。可以看出count(1)比count(主键id)块,因为从引擎返回id会涉及到解析数据行,以及拷贝字段值的操作。

以上三种方式,用explain查看执行计划,Extra的值都是Using index,type值都为index。如果表只有主键索引,则key值为PRIMARY(用主键索引),如果表还有辅助索引,则选择辅助索引列名最短的一个作为索引扫描表,因为上面第五点提到使用哪个索引数并不会影响结果。但是如果使用了where,并且where后的字段没有使用主键索引或者自建索引,则会走全变扫描(type值为ALL,Extra值为where)

count(字段)

  • 如果这个“字段”是定义为not null的话,InnoDB引擎遍历整个表,把“字段”取出来,server层收到数据,按行累加。
  • 如果这个“字段”定义允许为null,InnoDB引擎遍历整个表,把“字段”取出来,server层收到数据后判断不是null才累加。

这种方式使用explain查看执行计划,type值为ALL(全表扫描)

总结

  • 执行效率:count(字段)
  • count(字段)走全表扫描
  • count(主键id)、count(1)、count(*)走索引,如果有辅助索引,则走辅助索引,如果where后用了非索引项,则走全表扫描

扩展思考

问题1

MYISAM引擎直接存了一个变量来记录表的总行数,每次执行count(*)的时候直接返回这个数字,为什么InnoDB不行,反而要每次去遍历表?

因为InnoDB支持事务,MyISAM不支持事务,不同的事务隔离级别在同一时刻能查询到的count( * )值可能是不同的,比如在mysql默认隔离级别(可重复读)下,开启事务,进行count( * )操作,假如得到10条数据,insert一条记录,再进行count(*)操作,则会得到11条数据,但是,在未开启事务的程序中查询数据,始终是10条。

问题2

show table status 命令返回结果中就有一个值是ROWS来表示表当前有多少行,这个值能代替count(*)吗?

不能,因为show table status中的rows是估计值
.
EXPLAIN 和 SHOW TABLE STATUS LIKE 里返回的 rows 不准确?
.
MySQL 有一个优化器,它会对你的 SQL 进行一些优化。优化器的原理就是在真正执行 SQL 之前来估算一些每种执行策略的代价,选择一种它认为最好的方案去执行!

由于,优化器是在真正执行前做出的判断,所以它不可能是准确的。优化器,只能根据索引的“区分度”来统计、估算要扫描的行数。
在这里,MySQL 为了操作数据更高效。利用数学知识,做了一个抽样统计。
抽样,就是 InnoDB 默认会选择 N 个数据页,统计这些页面上的不同值,得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数。

问题3

实际应用中表中数据量较大,几十万级别数据,频繁执行count(*)有什么问题?如何解决?

会频繁读取磁盘,导致应用性能下降

解决办法:

(1)建立二级索引(我在MYsql5.6下测试过2067513条数据,只建立主键索引,count( * )需要大约1s,而建立二级索引,count(*)需要大约0.34s)

(2)建立一张表专门用于保存表的数据行数,但是删除新增的时候会多一次sql操作

(3)使用缓存,在缓存中记录数据总行数。什么时候加减数目,不同的隔离级别下是不同的,我做过这样的测试:

  1. 设置程序A和程序B 的隔离级别:set @@session.tx_isolation=‘repeatable-read’
    提交方式都是自动提交
  2. 开启程序B的事务,执行count(*)得到2067513条数据
  3. 在查询B中向数据库中插入一条记录,然后再执行count(*)得到2067514条数据
  4. 在程序A中执行count(*)得到2067513条数据
  5. 改变程序A的隔离级别:set @@session.tx_isolation=‘read-uncommitted’
  6. 在程序A中执行count(*)得到2067514条数据

因此,如果隔离级别是读为提交,则只要一插入数据就需要在缓存中将总数加一,但是如果是读已提交,则只有当事务提交后才可以在缓存中将总数加一。此外,还需要保证缓存和数据库双写一致性问题。

你可能感兴趣的:(MySQL)