而 MyISAM 表中可以快速取到表的行数。这些实践履历的背后是若何的机制,以及为什么须要/可以是这样,便是此文想要磋商的。
先来看一下概况: MySQL COUNT( ) 在 2 种存储引擎中的部分问题:
下面就带着这些问题,以 InnoDB 存储引擎为主来进行谈论。

一、InnoDB 全表 COUNT( )
紧张问题:
实行过程是若何的?如何打算 count?影响 count 结果的成分有哪些?count 值存在哪里?涉及的数据构造是若何的?为什么 InnoDB 只能通过扫表来实现 count( )?(见本文末了的问题)全表COUNT( )作为 table scan 类型操作的一个 case,有什么风险?COUNT( )操作是否会像“SELECT ”一样可能读取大字段涉及的溢出页?1
1. 实行框架 – 循环: 读取 + 计数?
1.1 基本结论
全表扫描,一个循环办理问题。循环内: 先读取一行,再决定该行是否计入 count。循环内是一行一行进行计数处理的。1.2 解释
大略 SELELCT-SQL 的实行框架,类比 INSERT INTO … SELECT 是同样的过程。
下面会逐步细化如何读取与计数 ( count++ ) 。
2
实行过程?
引述: 实行过程部分,分为 4 个部分:
COUNT( ) 前置流程: 从 Client 端发 SQL 语句,到 MySQL-Server 端实行 SELECT 之前,为后面的一些阐述做一铺垫。COUNT( ) 流程: 简要给出代码层面的流程框架及 2 个核心步骤的重点调用栈部分。读取一行: 可见性及 row_search_mvcc 函数,先容可见性如何影响 COUNT( ) 结果。计数一行: Evaluate_join_record 与列是否为空,先容计数过程如何影响 COUNT( ) 结果。如果读者希望直接看如何进行 COUNT( ),那么也可以忽略 (1),而直接跳到 (2) 开始看。
2.1 COUNT( ) 前置流程回顾 – 从 Client 端发 SQL 到 sub_select 函数
为了使看到的调用过程不太突兀,我们还是先回顾一下如何实行到 sub_select 函数这来的:
MySQL-Client 端发送 SQL 语句,根据 MySQL 通信协议封包发送。Mysql-Server 端吸收数据包,由协议解析出 command 类型 ( QUERY ) 及 SQL 语句 ( 字符串 ) 。SQL 语句经由解析器解析输出为 JOIN 类的工具,用于构造化地表达该 SQL 语句。PS: 这里的 JOIN 构造,不仅仅是纯语法构造,而是已经进行了语义处理,粗略地说,汇总了表的列表 ( table_list )、目标列的列表 ( target_list )、WHERE 条件、子查询等语法构造。在全表 COUNT( )-case 中,table_list = [表“t”(别名也是“t”)],target_list = [目标列工具(列名为“COUNT( )”)],当然这里没有 WHERE 条件、子查询等构造。JOIN 工具有 2 个主要的方法: JOIN::optimize(), JOIN::exec(),分别用于进行查询语句的优化 和 查询语句的实行。join->optimize(),优化阶段 (稍后 myisam 下全表 count( ) 操作会涉及这里的一点内容)。join->exec(),实行阶段 ( 重点 ),包含了 InnoDB 下全表count( ) 操作的实行流程。join->exec() 经由多少调用,将调用到 sub_select 函数来实行大略 SQL,包括 COUNT( ) 。END of sub_select 。
2.2 COUNT( ) 流程 ( 于 sub_select 函数中 )
上层的流程与代码是比较大略的,集中在 sub_select 函数中,个中 2 类函数分别对应于前面”实行框架”部分所述的 2 个步骤 – 读取、计数。先给出结论如下:
读取一行:从相对顶层的 sub_select 函数经由一番调用,终极所有分支将调用到 row_search_mvcc 函数中,该函数便是用于从 InnoDB 存储引擎所存储的 B+-tree 构造中读取一行到内存中的一个 buf (uchar ) 中,待后续处理利用。这里会涉及行锁的获取、MVCC 及行可见性的问题。当然对 于 SELECT COUNT( ) 这类快照读而言,只会涉及 MVCC 及其可见性,而不涉及行锁。详情可跳至“可见性与 row_search_mvcc 函数”部分。计数一行: 代码层面,将会在 evaluate_join_record 函数中对所读取的行进行评估,看其是否应该计入 count 中 ( 即是否要 count++ )。大略来说,COUNT(arg) 本身为 MySQL 的函数操作,对付一行来说,若括号内的参数 arg ( 某列或整行 ) 的值若不是 NULL,则 count++,否则对该行不予计数。详情可跳至“ Evaluate_join_record 与列是否为空”部分。这两个阶段对 COUNT( )结果的影响如下: (两层过滤)
SQL 层流程框架干系代码择要如下:
1210 enum_nested_loop_state1211 sub_select(JOIN join, QEP_TAB const qep_tab,bool end_of_records)1212 {1213 DBUG_ENTER(\公众sub_select\"大众);... ... // 此处省略1000字1265 while (rc == NESTED_LOOP_OK && join->return_tab >= qep_tab_idx)1266 {1267 int error;// 第一步,从存储引擎中获取一行;1268 if (in_first_read)1269 {1270 in_first_read= false;// 第一步,首次读取,扫描第一个知足条件的记录;// 初始化cursor,从”头”扫描到某个位置// 类似: SELECT id FROM t LIMIT 1;1271 error= (qep_tab->read_first_record)(qep_tab);1272 }1273 else// 第一步,后续读取,在前次扫描的位置上连续遍历,找到一个知足条件的记录;// 类似: SELECT id FROM t WHERE id > $last_id LIMIT 1;1274 error= info->read_record(info);... ... // 此处省略1000字// 第二步,处理刚刚取出的一行1291 rc= evaluate_join_record(join, qep_tab);... ... // 此处省略1000字1303 DBUG_RETURN(rc);1304 }
Q:代码层面,第一步骤(读取一行)有 2 个分支,为什么?
A:从 InnoDB 接口层面考虑,分为 “读第一行” 和 “读下一行”,是 2 个不同的实行过程,读第一行须要找到一个 ( cursor ) 位置并做一些初始化事情让后续的过程可递归。
正如我们如果用脚本/程序来进行逐行的扫表操作,实现上就会涉及下面 2 个 SQL:
// SELECT id FROM t LIMIT 1; OR SELECT MIN(id)-1 FROM t; -> $last_id// SELECT id FROM t WHERE id > $last_id LIMIT 1;
详细涉及到此例的代码,SQL 层到存储引擎层的调用关系,读取阶段的调用栈如下:(供参考)
sub_select 函数中从 SQL 层到 InnoDB 层的函数调用关系:(同颜色、同缩进 表示同一层)Ø (qep_tab->read_first_record) ()| -- > join_read_first(tab) | -- > tab->read_record.read_record=join_read_next; | -- > table->file->ha_index_init() | -- > handler::ha_index_init(uint idx, bool sorted) | -- > ha_innobase::index_init() | -- > table->file->ha_index_first() | -- > handler::ha_index_first(uint idx, bool sorted) | -- > ha_innobase::index_first() | -- > ha_innobase::index_read() | -- > row_search_mvcc() 初始化cursor并将其放到一个有效的初始位置上;Ø info->read_record (info)| -- > join_read_next(info) | -- > info->table->file->ha_index_next(info->record)) | -- > handler::ha_index_next(uchar buf) | -- > ha_innobase::index_next(uchar buf) | -- > general_fetch(buf, ROW_SEL_NEXT, 0) | -- > row_search_mvcc() “向前”移动一次cursor;
我们可以看到,无论是哪一个分支的读取,终极都殊途同归于 row_search_mvcc 函数。
以上是对 LOOP 中的代码做一些简要的解释,下面来看 row_search_mvcc 与 evaluate_join_record 如何输出终极的 count 结果。
2.3 行可见性及 row_search_mvcc 函数
这里我们紧张通过一组 case 和几个问题来看行可见性对 COUNT( ) 的影响。
Q:对付“SELECT COUNT( ) FROM t”或者“SELECT MIN(id) FROM t”操作,第一次的读行操作读到的是表 t 中 ( B+ 树最左叶节点 page 内 ) 的最小记录吗?( ha_index_first 为何也调用 row_search_mvcc 来获取最小 key 值?)
A:不一定。纵然是 MIN ( id ) 也不一定就读取的是 id 最小的那一行,由于也同样有行可见性的问题,实际上 index_read 取到的是 当前事务内语句可见的最小 index 记录。这也反响了前面提到的 join_read_first 与 join_read_next “殊途同归”到 row_search_mvcc 是理所应该的。
Q:针对图中末了一问,如果事务 X 是 RU ( Read-Uncommitted ) 隔离级别,且 C-Insert ( 100 ) 的完成是在 X-count( ) 实行过程中 ( 仅扫描到 5 或 10 这条记录 ) 完成的,那么 X-count( ) 在事务 C-Insert ( 100 ) 完成后,能否在之后的读取过程中看到 100 这条记录呢?
A:MySQL 采纳”读到什么便是什么”的策略,即 X-count( ) 在后面可以读到 100 这条记录。
2.4 evaluate_join_record 与列是否为空
Q:某一行如何计入 count?
A:两种情形会将所读的行计入 count:
如果 COUNT 函数中的参数是某列,则会判断所读行中该列定义是否 Nullable 以及该列的值是否为 NULL;若两者均为是,则不司帐入 count,否则将计入 count。e.g. SELECT COUNT(col_name) FROM tcol_name 可以是主键、唯一键、非唯一键、非索引字段如果 COUNT 中带有 ,则会判断这部分的整行是否为 NULL,如果判断参数为 NULL,则忽略该行,否则 count++。e.g-1. SELECT COUNT() FROM te.g-2. SELECT COUNT(B.) FROM A LEFT JOIN B ON A.id = B.idQ:特殊地,对付 SELECT COUNT(id) FROM t,个中 id 字段是表 t 的主键,则如何?
A:效果上等价于 COUNT( )。由于无论是 COUNT( ),还是 COUNT ( pk_col ) 都是由于有主键从而充分断定索取数据不为 NULL,这类 COUNT 表达式可以用于获取当前可见的表行数。
Q:用户层面对 InnoDB COUNT( ) 的优化操作问题
A:这个问题是业界熟习的一个问题,扫描非空唯一键可得到表行数,但所涉及的字节数可能会少很多(在表的行长与主键、唯一键的长度相差较多时),相对的 IO 代价小很多。
干系调用栈参考如下:
参考一:evaluate_join_record()| -- > rc= (qep_tab->next_select)(join, qep_tab+1, 0); | -- > end_send_group(...) | -- > init_sum_functions(join->sum_funcs, join->sum_funcs_end[idx+1])) | -- > (func_ptr)->reset_and_add() | -- > Item_sum::aggregator_clear() | -- > Item_sum::aggregator_add() | -- > update_sum_func(Item_sum func_ptr) | -- > (func_ptr)->add() | -- > Item_sum::aggregator_add()参考二: (Item_sum::aggregator_add)((Item_sum ) (func_ptr))->aggregator_add()| -- > (Item_sum )this->aggr->add() | -- > ((Aggregator_simple ) aggr)->item_sum->add() | -- > if (! aggr->arg_is_null(false)) | ------ > ((Item_sum_count )aggr->item_sum)->count++;
二、数据构造:
Q:count 值存储在哪个内存变量里?
A:SQL 解析后,存储于表达 COUNT( ) 这一项中,((Item_sum_count)item_sum)->count
如下图所示回顾我们之前“COUNT( )前置流程”部分提到的 JOIN 构造。
即 SQL 解析器为每个 SQL 语句进行构造化,将其放在一个 JOIN 工具 ( join ) 中来表达。在该工具中创建并添补了一个列表 result_field_list 用于存放结果列,列表中每个元素则是一个结果列的 ( Item_result_field ) 工具 ( 指针 ) 。
在 COUNT( )-case 中,结果列列表只包含一个元素,( Item_sum_count: public Item_result_field ) 类型工具 ( name = “COUNT( )”),个中该类所特有的成员变量 count即为所求。
三、MyISAM 全表 COUNT( )
由于 MyISAM 引擎并不常用于实际业务中,仅做简要描述如下:
MyISAM-COUNT( ) 操作是 O(1) 韶光繁芜度的操作。每张 MyISAM 表中存放了一个 meta 信息-count 值,在内存中与文件中各有一份,内存中的 count 变量值通过读取文件中的 count 值来进行初始化。SELECT COUNT( ) FROM t 会直接读取内存中的表 t 对应的 count 变量值。内存中的 count 值与文件中的 count 值由写操作来进行更新,其同等性由表级锁来担保。表级锁担保的写入串行化使得,同一时候所有用户线程的读操作要么被锁,要么只会看到一种数据状态。四、几个问题
Q:MyISAM 与 InnoDB 在 COUNT( ) 操作的实行过程在哪里开始分道扬镳?
共性:共性存在于 SQL 层,即 SQL 解析之后的数据构造是同等的,count 变量都是存在于作为结果列的 Item_sum_count 类型工具中;返回给客户真个过程也类似 – 对该 count 变量进行赋值并经由 MySQL 通信协议返回给客户端。差异:InnoDB 的 count 值打算是在 SQL 实行阶段进行的;而 MyISAM 表本身在内存中有一份包含了表 row_count 值的 meta 信息,在 SQL 优化阶段通过存储引擎的标记给优化器一个 hint,表明该表所用的存储引擎保存了精确行数,可以直接获取到,无需再进入实行器。Q:InnoDB 中为何无法向 MyISAM 一样掩护住一个 row_count 变量?
A:从 MVCC 机制与行可见性问题中可得到缘故原由,每个事务所看到的行可能是不一样的,其 count( ) 结果也可能是不同的;反过来看,则是 MySQL-Server 端无法在同一时候对所有用户线程供应一个统一的读视图,也就无法供应一个统一的 count 值。
PS: 对付多个访问 MySQL 的用户线程 ( COUNT( ) ) 而言,决定它们各自的结果的成分有几个:
一组事务实行前的数据状态(初始数据状态)。有韶光重叠的事务们的实行序列 (操作时序,事务理论表明 并发事务操作的可串行化是精确性的必要条件)。事务们各自的隔离级别(每个操作的输入)。个中 1、2 对付 Server 而言都是全局或者说可控的,只有 3 是每个用户线程中事务所独占的属性,这是 Server 端不可控的成分,因此 Server 端也就对每个 COUNT( ) 结果不可控了。
Q:InnoDB-COUNT( ) 属 table scan 操作,是否会将现有 Buffer Pool 中其它用户线程所需热点页从 LRU-list 中挤占掉,从而其它用户线程还需从磁盘 load 一次,溘然加重 IO 花费,可能对现有要求造成壅塞?
A:MySQL 有这样的优化策略,将扫表操作所 load 的 page 放在 LRU-list 的 oung/old 的交界处 ( LRU 尾部约 3/8 处 )。这样用户线程所需的热点页仍旧在 LRU-list-young 区域,而扫表操作不断 load 的页则会不断冲刷 old 区域的页,这部分的页本身便是被认为非热点的页,因此也相对符合逻辑。
PS: 个人认为还有一种类似的优化思路,是限定扫描操作所利用的 Buffer Pool 的大小为 O(1) 级别,但这样做须要付出额外的内存管理本钱。
Q:InnoDB-COUNT( ) 是否会像 SELECT FROM t 那样读取存储大字段的溢出页(如果存在)?
A:否。由于 InnoDB-COUNT( ) 只须要数行数,而每一行的主键肯定不是 NULL,因此只须要读主键索引页内的行数据,而无需读取额外的溢出页。
———— e n d ————
本期专题推举
手
专
题
写
【手写专题】师长说:想要进阶架构师,不仅仅只是懂得框架事理。下面,就让师长手把手带你手写Spring MVC、Spring、Mybatis、秒杀架构、RPC等框架,让你提升架构思维,真正吃透!
↓↓↓↓↓点击标题即可跳转↓↓↓↓↓
跳转前别忘了先在本篇文章留言,在看
写出我的第一个框架:迷你版Spring MVC透彻理解MyBatis设计思想之手写实现透彻理解Spring事务设计思想之手写实现理解数据库连接池底层事理之手写实现手把手带你实现JDK动态代理手写实现一个迷你版的Tomcat自己动手写一个做事网关手把手带你设计一个百万级的推送系统手把手带你秒杀架构实践(含完全代码)解密Dubbo:自己动手编写一个较为完善的RPC框架(两万字干货)别的的微做事、分布式、高并发、JVM调头等19大进阶架构师专题请关注后在菜单栏查看。