深入理解selectcount()底层究竟做了什么 2019061112:58Java技术架构 专注于Java领域优质技术,欢迎关注 作者:贾春生 SELECTCOUNT()FROMt是个再常见不过的SQL需求了。在MySQL的使用规范中,我们一般使用事务引擎InnoDB作为(一般业务)表的存储引擎,在此前提下,COUNT()操作的时间复杂度为O(N),其中N为表的行数。 而MyISAM表中可以快速取到表的行数。这些实践经验的背后是怎样的机制,以及为什么需要可以是这样,就是此文想要探讨的。 先来看一下概况:MySQLCOUNT()在2种存储引擎中的部分问题: 下面就带着这些问题,以InnoDB存储引擎为主来进行讨论。一、InnoDB全表COUNT() 主要问题:执行过程是怎样的?如何计算count?影响count结果的因素有哪些?count值存在哪里?涉及的数据结构是怎样的?为什么InnoDB只能通过扫表来实现count()?(见本文最后的问题)全表COUNT()作为tablescan类型操作的一个case,有什么风险?COUNT()操作是否会像SELECT一样可能读取大字段涉及的溢出页? 1。执行框架循环:读取计数 1。1基本结论全表扫描,一个循环解决问题。循环内:先读取一行,再决定该行是否计入count。循环内是一行一行进行计数处理的。 1。2说明 简单SELELCTSQL的执行框架,类比INSERTINTOSELECT是同样的过程。 下面会逐步细化如何读取与计数(count)。 2。执行过程 引述:执行过程部分,分为4个部分:COUNT()前置流程:从Client端发SQL语句,到MySQLServer端执行SELECT之前,为后面的一些阐述做一铺垫。COUNT()流程:简要给出代码层面的流程框架及2个核心步骤的重点调用栈部分。读取一行:可见性及rowsearchmvcc函数,介绍可见性如何影响COUNT()结果。计数一行:Evaluatejoinrecord与列是否为空,介绍计数过程如何影响COUNT()结果。 如果读者希望直接看如何进行COUNT(),那么也可以忽略(1),而直接跳到(2)开始看。 2。1COUNT()前置流程回忆从Client端发SQL到subselect函数 为了使看到的调用过程不太突兀,我们还是先回忆一下如何执行到subselect函数这来的: 1。MySQLClient端发送SQL语句,根据MySQL通信协议封包发送。 2。MysqlServer端接收数据包,由协议解析出command类型(QUERY)及SQL语句(字符串)。 3。SQL语句经过解析器解析输出为JOIN类的对象,用于结构化地表达该SQL语句。PS:这里的JOIN结构,不仅仅是纯语法结构,而是已经进行了语义处理,粗略地说,汇总了表的列表(tablelist)、目标列的列表(targetlist)、WHERE条件、子查询等语法结构。在全表COUNT()case中,tablelist〔表t(别名也是t)〕,targetlist〔目标列对象(列名为COUNT())〕,当然这里没有WHERE条件、子查询等结构。 4。JOIN对象有2个重要的方法:JOIN::optimize(),JOIN::exec(),分别用于进行查询语句的优化和查询语句的执行。 joinoptimize(),优化阶段(稍后myisam下全表count()操作会涉及这里的一点内容)。 joinexec(),执行阶段(重点),包含了InnoDB下全表count()操作的执行流程。 5。joinexec()经过若干调用,将调用到subselect函数来执行简单SQL,包括COUNT()。 6。ENDofsubselect。 2。2COUNT()流程(于subselect函数中) 上层的流程与代码是比较简单的,集中在subselect函数中,其中2类函数分别对应于前面执行框架部分所述的2个步骤读取、计数。先给出结论如下:读取一行:从相对顶层的subselect函数经过一番调用,最终所有分支将调用到rowsearchmvcc函数中,该函数就是用于从InnoDB存储引擎所存储的Btree结构中读取一行到内存中的一个buf(uchar)中,待后续处理使用。这里会涉及行锁的获取、MVCC及行可见性的问题。当然对于SELECTCOUNT()这类快照读而言,只会涉及MVCC及其可见性,而不涉及行锁。详情可跳至可见性与rowsearchmvcc函数部分。计数一行:代码层面,将会在evaluatejoinrecord函数中对所读取的行进行评估,看其是否应当计入count中(即是否要count)。 简单来说,COUNT(arg)本身为MySQL的函数操作,对于一行来说,若括号内的参数arg(某列或整行)的值若不是NULL,则count,否则对该行不予计数。详情可跳至Evaluatejoinrecord与列是否为空部分。 这两个阶段对COUNT()结果的影响如下:(两层过滤) SQL层流程框架相关代码摘要如下: Q:代码层面,第一步骤(读取一行)有2个分支,为什么? A:从InnoDB接口层面考虑,分为读第一行和读下一行,是2个不同的执行过程,读第一行需要找到一个(cursor)位置并做一些初始化工作让后续的过程可递归。 正如我们如果用脚本程序来进行逐行的扫表操作,实现上就会涉及下面2个SQL:SELECTidFROMtLIMIT1;ORSELECTMIN(id)1FROMt;lastidSELECTidFROMtWHEREidlastidLIMIT1; 具体涉及到此例的代码,SQL层到存储引擎层的调用关系,读取阶段的调用栈如下:(供参考) 我们可以看到,无论是哪一个分支的读取,最终都殊途同归于rowsearchmvcc函数。 以上是对LOOP中的代码做一些简要的说明,下面来看rowsearchmvcc与evaluatejoinrecord如何输出最终的count结果。 2。3行可见性及rowsearchmvcc函数 这里我们主要通过一组case和几个问题来看行可见性对COUNT()的影响。 Q:对于SELECTCOUNT()FROMt或者SELECTMIN(id)FROMt操作,第一次的读行操作读到的是表t中(B树最左叶节点page内)的最小记录吗?(haindexfirst为何也调用rowsearchmvcc来获取最小key值?) A:不一定。即使是MIN(id)也不一定就读取的是id最小的那一行,因为也同样有行可见性的问题,实际上indexread取到的是当前事务内语句可见的最小index记录。这也反映了前面提到的joinreadfirst与joinreadnext殊途同归到rowsearchmvcc是理所应当的。 Q:针对图中最后一问,如果事务X是RU(ReadUncommitted)隔离级别,且CInsert(100)的完成是在Xcount()执行过程中(仅扫描到5或10这条记录)完成的,那么Xcount()在事务CInsert(100)完成后,能否在之后的读取过程中看到100这条记录呢? A:MySQL采取读到什么就是什么的策略,即Xcount()在后面可以读到100这条记录。 2。4evaluatejoinrecord与列是否为空 Q:某一行如何计入count? A:两种情况会将所读的行计入count: 1、如果COUNT函数中的参数是某列,则会判断所读行中该列定义是否Nullable以及该列的值是否为NULL;若两者均为是,则不会计入count,否则将计入count。e。g。SELECTCOUNT(colname)FROMtcolname可以是主键、唯一键、非唯一键、非索引字段 2、如果COUNT中带有,则会判断这部分的整行是否为NULL,如果判断参数为NULL,则忽略该行,否则count。e。g1。SELECTCOUNT()FROMte。g2。SELECTCOUNT(B。)FROMALEFTJOINBONA。idB。id Q:特别地,对于SELECTCOUNT(id)FROMt,其中id字段是表t的主键,则如何? A:效果上等价于COUNT()。因为无论是COUNT(),还是COUNT(pkcol)都是因为有主键从而充分断定索取数据不为NULL,这类COUNT表达式可以用于获取当前可见的表行数。 Q:用户层面对InnoDBCOUNT()的优化操作问题 A:这个问题是业界熟悉的一个问题,扫描非空唯一键可得到表行数,但所涉及的字节数可能会少很多(在表的行长与主键、唯一键的长度相差较多时),相对的IO代价小很多。 相关调用栈参考如下: 二、数据结构: Q:count值存储在哪个内存变量里? A:SQL解析后,存储于表达COUNT()这一项中,((Itemsumcount)itemsum)count 如下图所示回顾我们之前COUNT()前置流程部分提到的JOIN结构。 即SQL解析器为每个SQL语句进行结构化,将其放在一个JOIN对象(join)中来表达。在该对象中创建并填充了一个列表resultfieldlist用于存放结果列,列表中每个元素则是一个结果列的(Itemresultfield)对象(指针)。 在COUNT()case中,结果列列表只包含一个元素,(Itemsumcount:publicItemresultfield)类型对象(nameCOUNT()),其中该类所特有的成员变量count即为所求。三、MyISAM全表COUNT() 由于MyISAM引擎并不常用于实际业务中,仅做简要描述如下:MyISAMCOUNT()操作是O(1)时间复杂度的操作。每张MyISAM表中存放了一个meta信息count值,在内存中与文件中各有一份,内存中的count变量值通过读取文件中的count值来进行初始化。SELECTCOUNT()FROMt会直接读取内存中的表t对应的count变量值。内存中的count值与文件中的count值由写操作来进行更新,其一致性由表级锁来保证。表级锁保证的写入串行化使得,同一时刻所有用户线程的读操作要么被锁,要么只会看到一种数据状态。四、几个问题 Q:MyISAM与InnoDB在COUNT()操作的执行过程在哪里开始分道扬镳?共性:共性存在于SQL层,即SQL解析之后的数据结构是一致的,count变量都是存在于作为结果列的Itemsumcount类型对象中;返回给客户端的过程也类似对该count变量进行赋值并经由MySQL通信协议返回给客户端。区别:InnoDB的count值计算是在SQL执行阶段进行的;而MyISAM表本身在内存中有一份包含了表rowcount值的meta信息,在SQL优化阶段通过存储引擎的标记给优化器一个hint,表明该表所用的存储引擎保存了精确行数,可以直接获取到,无需再进入执行器。 Q:InnoDB中为何无法向MyISAM一样维护住一个rowcount变量? A:从MVCC机制与行可见性问题中可得到原因,每个事务所看到的行可能是不一样的,其count()结果也可能是不同的;反过来看,则是MySQLServer端无法在同一时刻对所有用户线程提供一个统一的读视图,也就无法提供一个统一的count值。 PS:对于多个访问MySQL的用户线程(COUNT())而言,决定它们各自的结果的因素有几个:一组事务执行前的数据状态(初始数据状态)。有时间重叠的事务们的执行序列(操作时序,事务理论表明并发事务操作的可串行化是正确性的必要条件)。事务们各自的隔离级别(每个操作的输入)。 其中1、2对于Server而言都是全局或者说可控的,只有3是每个用户线程中事务所独有的属性,这是Server端不可控的因素,因此Server端也就对每个COUNT()结果不可控了。 Q:InnoDBCOUNT()属tablescan操作,是否会将现有BufferPool中其它用户线程所需热点页从LRUlist中挤占掉,从而其它用户线程还需从磁盘load一次,突然加重IO消耗,可能对现有请求造成阻塞? A:MySQL有这样的优化策略,将扫表操作所load的page放在LRUlist的oungold的交界处(LRU尾部约38处)。这样用户线程所需的热点页仍然在LRUlistyoung区域,而扫表操作不断load的页则会不断冲刷old区域的页,这部分的页本身就是被认为非热点的页,因此也相对符合逻辑。 PS:个人认为还有一种类似的优化思路,是限定扫描操作所使用的BufferPool的大小为O(1)级别,但这样做需要付出额外的内存管理成本。 Q:InnoDBCOUNT()是否会像SELECTFROMt那样读取存储大字段的溢出页(如果存在)? A:否。因为InnoDBCOUNT()只需要数行数,而每一行的主键肯定不是NULL,因此只需要读主键索引页内的行数据,而无需读取额外的溢出页。 blog。didiyun。comindex。php20190108mysqlcount