一文解答为什么MySQL的count()方法这么慢

目录
  • 前言
  • count()的原理
  • 各种count()方法的原理
  • 允许粗略估计行数的场景
  • 必须精确估计行数的场景
  • 总结

前言

mysql用count方法查全表数据,在不同的存储引擎里实现不同,myisam有专门字段记录全表的行数,直接读这个字段就好了。而innodb则需要一行行去算。

比如说,你有一张短信表(sms),里面放了各种需要发送的短信信息。

sms建表sql:

sms表;

需要注意的是state字段,为0的时候说明这时候短信还未发送。

此时还会有一个异步线程不断的捞起未发送(state=0)的短信数据,执行发短信操作,发送成功之后state字段会被置为1(已发送)。也就是说未发送的数据会不断变少。

异步线程发送短信:

假设由于某些原因,你现在需要做一些监控,比如监控的内容是,你的sms数据表里还有没有state=0(未发送)的短信,方便判断一下堆积的未发送短信大概在什么样的一个量级。

为了获取满足某些条件的行数是多少,我们一般会使用count()方法。

这时候为了获取未发送的短信数据,我们很自然就想到了使用下面的sql语句进行查询。

select count(*) from sms where state = 0;

然后再把获得数据作为打点发给监控服务。

当数据表小的时候,这是没问题的,但当数据量大的时候,比如未发送的短信到了百万量级的时候,你就会发现,上面的sql查询时间会变得很长,最后timeout报错,查不出结果了。

为什么?

我们先从count()方法的原理聊起。

count()的原理

count()方法的目的是计算当前sql语句查询得到的非NULL的行数。

我们知道mysql是分为server层和存储引擎层的。

Mysql架构:

存储引擎层里可以选择各种引擎进行存储,最常见的是innodb、myisam。具体使用哪个存储引擎,可以通过建表sql里的ENGINE​字段进行指定。比如这篇文章开头的建表sql里用了ENGINE=InnoDB,那这张表用的就是innodb引擎。

虽然在server层都叫count()方法,但在不同的存储引擎下,它们的实现方式是有区别的。

比如同样是读全表数据  select count(*) from sms;语句。

使用 myisam引擎的数据表里有个记录当前表里有几行数据的字段,直接读这个字段返回就好了,因此速度快得飞起。

而使用innodb引擎的数据表,则会选择体积最小的索引树,然后通过遍历叶子节点的个数挨个加起来,这样也能得到全表数据。

因此回到文章开头的问题里,当数据表行数变大后,单次count就需要扫描大量的数据,因此很可能就会出现超时报错。

那么问题就来了。

为什么innodb不能像myisam那样实现count()方法

myisam和innodb这两个引擎,有几个比较明显的区别,这个是八股文常考了。

其中最大的区别在于myisam不支持事务,而innodb支持事务。

而事务,有四层隔离级别,其中默认隔离级别就是可重复读隔离级别(RR)。

四层隔离级别:

innodb引擎通过MVCC实现了可重复隔离级别,事务开启后,多次执行同样的select快照读,要能读到同样的数据。

于是我们看个例子:为什么innodb不单独记录表行数?

对于两个事务A和B,一开始sms表假设就2条数据,那事务A一开始确实是读到2条数据。事务B在这期间插入了1条数据,按道理数据库其实有3条数据了,但由于可重复读的隔离级别,事务A依然还是只能读到2条数据。

因此由于事务隔离级别的存在,不同的事务在同一时间下,看到的表内数据行数是不一致的,因此innodb,没办法,也没必要像myisam那样单纯的加个count字段信息在数据表上。

那如果不可避免要使用count(),有没有办法让它快一点?

各种count()方法的原理

count()的括号里,可以放各种奇奇怪怪的东西,想必大家应该看过,比如放个星号*,放个1,放个索引列啥的。

我们来分析下他们的执行流程。

count方法的大原则是server层会从innodb存储引擎里读来一行行数据,并且只累计非null的值。但这个过程,根据count()方法括号内的传参,有略有不同。

count(*):server层拿到innodb返回的行数据,不对里面的行数据做任何解析和判断,默认取出的值肯定都不是null,直接行数+1。

count(1):server层拿到innodb返回的行数据,每行放个1进去,默认不可能为null,直接行数+1.

count(某个列字段):由于指明了要count某个字段,innodb在取数据的时候,会把这个字段解析出来返回给server层,所以会比count(1)和count(*)多了个解析字段出来的流程。

如果这个列字段是主键id,主键是不可能为null的,所以server层也不用判断是否为null,innodb每返回一行,行数结果就+1.

如果这个列是普通索引字段,innodb一般会走普通索引,每返回一行数据,server层就会判断这个字段是否为null,不是null的情况下+1。当然如果建表sql里字段定义为not null的话,那就不用做这一步判断直接+1。

如果这个列没有加过索引,那innodb可能会全表扫描,返回的每一行数据,server层都会判断这个字段是否为null,不是null的情况下+1。同上面的情况一样,字段加了not null也就省下这一步判断了。

理解了原理后我们大概可以知道他们的性能排序是

count(*) ≈ count(1) > count(主键id) > count(普通索引列) > count(未加索引列)

所以说count(*),已经是最快的了。

知道真相的我眼泪掉下来。​

那有没有其他更好的办法?

允许粗略估计行数的场景

我们回过头来细品下文章开头的需求,我们只是希望知道数据库里还有多少短信是堆积在那没发的,具体是1k还是2k其实都是差不多量级,等到了百万以上,具体数值已经不重要了,我们知道它现在堆积得很离谱,就够了。因此这个场景,其实是允许使用比较粗略的估计的。

那怎么样才能获得粗略的数值呢?

还记得我们平时为了查看sql执行计划用的explain命令不。

其中有个rows,会用来估计接下来执行这条sql需要扫描和检查多少行。它是通过采样的方式计算出来的,虽然会有一定的偏差,但它能反映一定的数量级。

explain里的rows

有些语言的orm里可能没有专门的explain语法,但是肯定有执行raw sql的功能,你可以把explain语句当做raw sql传入,从返回的结果里将rows那一列读出来使用。

一般情况下,explain的sql如果能走索引,那会比不走索引的情况更准 。单个字段的索引会比多个字段组成的复合索引要准。索引区分度越高,rows的值也会越准。

这种情况几乎满足大部分的监控场景。但总有一些场景,它要求必须得到精确的行数,这种情况该怎么办呢?

必须精确估计行数的场景

这种场景就比较头疼了,但也不是不能做。

我们可以单独拉一张新的数据库表,只为保存各种场景下的count。

CREATE TABLE `count_table` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
  `cnt_what` char(20) NOT NULL DEFAULT '' COMMENT '各种需要计算的指标',
  `cnt` tinyint NOT NULL COMMENT 'cnt指标值',
  PRIMARY KEY (`id`),
  KEY `idx_cnt_what` (`cnt_what`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8mb4;

count_table表保存各种场景下的count

当需要获取某个场景下的cout值时,可以使用下面的sql进行直接读取,快得飞起。

select cnt from count_table where cnt_what = "未发送的短信数量"; 

那这些count的结果值从哪来呢?这里分成两种情况。

实时性要求较高的场景

如果你对这个cnt计算结果的实时性要求很高,那你需要将更新cnt的sql加入到对应变更行数的事务中。比如我们有两个事务A和B,分别是增加未发送短信和减少未发送短信。

将更改表行数的操作放入到事务里

这样做的好处是事务内的cnt行数依然符合隔离级别,事务回滚的时候,cnt的值也会跟着回滚。

坏处也比较明显,多个线程对同一个cnt进行写操作,会触发悲观锁,多个线程之间需要互相等待。对于高频写的场景,性能会有折损。

实时性没那么高的场景

如果实时性要求不高的话,比如可以一天一次,那你可以通过全表扫描后做计算。

举个例子,比如上面的短信表,可以按id排序,每次取出1w条数据,记下这一批里最大的id,然后下次从最大id开始再拿1w条数据出来,不断循环。

对于未发送的短信,就只需要在捞出的那1w条数据里,筛选出state=0的条数。

batch分批获取短信表

当然如果有条件,这种场景最好的方式还是消费binlog将数据导入到hive里,然后在hive里做查询,不少公司也已经有现成的组件可以做这种事情,不用自己写脚本,岂不美哉。

mysql同步hive

总结

mysql用count方法查全表数据,在不同的存储引擎里实现不同,myisam有专门字段记录全表的行数,直接读这个字段就好了。而innodb则需要一行行去算。

性能方面 count(*) ≈ count(1) > count(主键id) > count(普通索引列) > count(未加索引列),但哪怕是性能最好的count(*),由于实现上就需要一行行去算,所以数据量大的时候就是不给力。

如果确实需要获取行数,且可以接受不那么精确的行数(只需要判断大概的量级)的话,那可以用explain里的rows,这可以满足大部分的监控场景,实现简单。

如果要求行数准确,可以建个新表,里面专门放表行数的信息。

如果对实时性要求比较高的话,可以将更新行数的sql放入到对应事务里,这样既能满足事务隔离性,还能快速读取到行数信息。

如果对实时性要求不高,接受一小时或者一天的更新频率,那既可以自己写脚本遍历全表后更新行数信息。也可以将通过监听binlog将数据导入hive,需要数据时直接通过hive计算得出。

到此这篇关于一文解答为什么MySQL的count()方法这么慢的文章就介绍到这了,更多相关MySQL count()方法内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详解 MySQL中count函数的正确使用方法

    1. 描述 在MySQL中,当我们需要获取某张表中的总行数时,一般会选择使用下面的语句 select count(*) from table; 其实count函数中除了*还可以放其他参数,比如常数.主键id.字段,那么它们有什么区别?各自效率如何?我们应该使用哪种方式来获取表的行数呢? 当搞清楚count函数的运行原理后,相信上面几个问题的答案就会了然于胸. 2. 表结构 为了解决上述的问题,我创建了一张 user 表,它有两个字段:主键id和name,后者可以为null,建表语句如下. CRE

  • mysql count提高方法总结

    mysql想必很多程序猿都是不陌生的.很多人在纠结count的用法,怎么样才能查询效果最好.今天来谈谈小编的一些看法,仅供参考. 1.咱们先准备建一个表,并准备好测试数据方便后续步骤测试 以InnoDB引擎表为例 建表语句如下 CREATE TABLE test.test ( a VARCHAR(50) NOT NULL COMMENT 'ddfdf', b VARCHAR(15) NOT NULL COMMENT 'fds', c VARCHAR(20) NOT NULL COMMENT 'a

  • MySQL中distinct和count(*)的使用方法比较

    首先对于MySQL的DISTINCT的关键字的一些用法: 1.在count 不重复的记录的时候能用到,比如SELECT COUNT( DISTINCT id ) FROM tablename:就是计算talbebname表中id不同的记录有多少条. 2,在需要返回记录不同的id的具体值的时候可以用,比如SELECT DISTINCT id FROM tablename:返回talbebname表中不同的id的具体的值. 3.上面的情况2对于需要返回mysql表中2列以上的结果时会有歧义,比如SE

  • MYSQL中统计查询结果总行数的便捷方法省去count(*)

    MYSQL的关键词 : SQL_CALC_FOUND_ROWS 查看手册后发现此关键词的作用是在查询时统计满足过滤条件后的结果的总数(不受 Limit 的限制) 例如: 复制代码 代码如下: SELECT SQL_CALC_FOUND_ROWS tid FROM cdb_threads WHERE fid=14 LIMIT 1,10; 假设满足条件的有1000条,这里返回10条. 立即使用 复制代码 代码如下: SELECT found_rows() AS rowcount; 则返回的 rowc

  • 一文解答为什么MySQL的count()方法这么慢

    目录 前言 count()的原理 各种count()方法的原理 允许粗略估计行数的场景 必须精确估计行数的场景 总结 前言 mysql用count方法查全表数据,在不同的存储引擎里实现不同,myisam有专门字段记录全表的行数,直接读这个字段就好了.而innodb则需要一行行去算. 比如说,你有一张短信表(sms),里面放了各种需要发送的短信信息. sms建表sql: sms表; 需要注意的是state字段,为0的时候说明这时候短信还未发送. 此时还会有一个异步线程不断的捞起未发送(state=

  • 一文搞清楚MySQL count(*)、count(1)、count(col)区别

    目录 count作用 测试 count(*) count(1) count(col) count(id):统计id count(indexcol):统计带索引的字段 count(normalcol):统计不带索引的字段 count(1)和count(*)取舍 总结 在工作中遇到count(*).count(1).count(col) ,可能会让你分不清楚,都是计数,干嘛这么搞这么多东西. count 作用 COUNT(expression):返回查询的记录总数,expression 参数是一个字

  • 一文解答什么是MySQL的回表

    目录 引言 聚簇索引和非聚簇索引是什么? 主键索引和非主键索引有什么区别? B-Tree 和 B+Tree 的简单理解 如何避免回表? 引言 简单来说,回表就是 MySQL 要先查询到主键索引,然后再用主键索引定位到数据. 下面,对一些问题进行分析与回答: 什么是聚簇索引?什么是非聚簇索引? 为什么回表要先查到主键索引? 主键索引和非主键索引有什么区别? 如何避免回表? 聚簇索引和非聚簇索引是什么? MySQL 的索引有不同的角度的分类方式,例如:按数据结构分.按逻辑角度分.按物理存储分. 其中

  • MySQL去重的方法整理

    MySQL去重的方法整理 [初级]有极少的重复行 使用distinct查出来,然后手动一行一行删除. [中级]按照单个字段的重复去重 例如:对id字段去重 使用方法:获取id的重复字段的值,利用相同id字段所在的行中,比较出数据不同的字段,删除 除了最小(或最大)的字段所在的该行之外的所有重复的行.一般使用主键来比较,因为主键的值一定是唯一值,绝对不相同. id name 1 a 1 b 2 c 2 a 3 c 结果: id name 1 a 2 a 操作: delete from a_tmp

  • Python操作MySQL简单实现方法

    本文实例讲述了Python操作MySQL简单实现方法.分享给大家供大家参考.具体分析如下: 一.安装: 安装MySQL 安装MySQL不用多说了,下载下来安装就是,没有特别需要注意的地方. 一个下载地址:点击打开链接 二.示例: 复制代码 代码如下: # coding=utf-8 import MySQLdb   #查询数量 def Count(cur):    count=cur.execute('select * from Student')    print 'there has %s r

  • .net文件上传时实现通过文件头确认文件类型的方法

    本文实例讲述了.net文件上传时实现通过文件头确认文件类型的方法,其中 script 用来返回给页面的数据,读者还可以根据自身需要对相关部分自行修改.另外,文件头也可以自行添加定义. 主要代码如下: AppCode/FileUpload.cs using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Text.RegularExpressions; using

  • PHP编程实现csv文件导入mysql数据库的方法

    本文实例讲述了PHP编程实现csv文件导入mysql数据库的方法.分享给大家供大家参考,具体如下: config.db.php内容如下: <?php $username="root"; $userpass="123"; $dbhost="localhost"; $dbdatabase="credits2stakes"; //生成一个连接 $db_connect=mysql_connect($dbhost,$usernam

  • MySQL中count(*)、count(1)和count(col)的区别汇总

    前言 count函数是用来统计表中或数组中记录的一个函数,count(*) 它返回检索行的数目, 不论其是否包含 NULL值.最近感觉大家都在讨论count的区别,那么我也写下吧:欢迎留言讨论,话不多说了,来一起看看详细的介绍吧. 1.表结构: dba_jingjing@3306>[rds_test]>CREATE TABLE `test_count` ( -> `c1` varchar(10) DEFAULT NULL, -> `c2` varchar(10) DEFAULT N

  • Spring Boot 与 Kotlin 使用JdbcTemplate连接MySQL数据库的方法

    之前介绍了一些Web层的例子,包括构建RESTful API.使用Thymeleaf模板引擎渲染Web视图,但是这些内容还不足以构建一个动态的应用.通常我们做App也好,做Web应用也好,都需要内容,而内容通常存储于各种类型的数据库,服务端在接收到访问请求之后需要访问数据库获取并处理成展现给用户使用的数据形式. 本文介绍在Spring Boot基础下配置数据源和通过 JdbcTemplate 编写数据访问的示例. 数据源配置 在我们访问数据库的时候,需要先配置一个数据源,下面分别介绍一下几种不同

随机推荐