←←←←←←←←←←←← 快!点关注sql
“SELECT COUNT( ) FROM t” 是个再常见不过的 SQL 需求了。在 MySQL 的使用规范中,咱们通常使用事务引擎 InnoDB 做为(通常业务)表的存储引擎,在此前提下,COUNT( )操做的时间复杂度为 O(N),其中 N 为表的行数。数据结构
而 MyISAM 表中能够快速取到表的行数。这些实践经验的背后是怎样的机制,以及为何须要/能够是这样,就是此文想要探讨的。架构
先来看一下概况: MySQL COUNT( * ) 在 2 种存储引擎中的部分问题:并发
下面就带着这些问题,以 InnoDB 存储引擎为主来进行讨论。mvc
简单 SELELCT-SQL 的执行框架,类比 INSERT INTO … SELECT 是一样的过程。框架
下面会逐步细化如何读取与计数 ( count++ ) 。分布式
引述: 执行过程部分,分为 4 个部分:函数
若是读者但愿直接看如何进行 COUNT( * ),那么也能够忽略 (1),而直接跳到 (2) 开始看。高并发
为了使看到的调用过程不太突兀,咱们仍是先回忆一下如何执行到 sub_select 函数这来的:oop
PS: 这里的 JOIN 结构,不只仅是纯语法结构,而是已经进行了语义处理,粗略地说,汇总了表的列表 ( table_list )、目标列的列表 ( target_list )、WHERE 条件、子查询等语法结构。
在全表 COUNT( )-case 中,table_list = [表“t”(别名也是“t”)],target_list = [目标列对象(列名为“COUNT( )”)],固然这里没有 WHERE 条件、子查询等结构。
JOIN 对象有 2 个重要的方法: JOIN::optimize(), JOIN::exec(),分别用于进行查询语句的优化 和 查询语句的执行。
上层的流程与代码是比较简单的,集中在 sub_select 函数中,其中 2 类函数分别对应于前面”执行框架”部分所述的 2 个步骤 – 读取、计数。先给出结论以下:
这里会涉及行锁的获取、MVCC 及行可见性的问题。固然对 于 SELECT COUNT( * ) 这类快照读而言,只会涉及 MVCC 及其可见性,而不涉及行锁。详情可跳至“可见性与 row_search_mvcc 函数”部分。
简单来讲,COUNT(arg) 自己为 MySQL 的函数操做,对于一行来讲,若括号内的参数 arg ( 某列或整行 ) 的值若不是 NULL,则 count++,不然对该行不予计数。详情可跳至“ Evaluate_join_record 与列是否为空”部分。
这两个阶段对 COUNT( * )结果的影响以下: (两层过滤)
SQL 层流程框架相关代码摘要以下:
1210 enum_nested_loop_state 1211 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 结果。
这里咱们主要经过一组 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 这条记录。
Q:某一行如何计入 count?
A:两种状况会将所读的行计入 count:
若是 COUNT 函数中的参数是某列,则会判断所读行中该列定义是否 Nullable 以及该列的值是否为 NULL;若二者均为是,则不会计入 count,不然将计入 count。
若是 COUNT 中带有 * ,则会判断这部分的整行是否为 NULL,若是判断参数为 NULL,则忽略该行,不然 count++。
Q: 特别地,对于 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 引擎并不经常使用于实际业务中,仅作简要描述以下:
Q:MyISAM 与 InnoDB 在 COUNT( * ) 操做的执行过程在哪里开始分道扬镳?
Q:InnoDB 中为什么没法向 MyISAM 同样维护住一个 row_count 变量?
A:从 MVCC 机制与行可见性问题中可获得缘由,每一个事务所看到的行多是不同的,其 count( * ) 结果也多是不一样的;反过来看,则是 MySQL-Server 端没法在同一时刻对全部用户线程提供一个统一的读视图,也就没法提供一个统一的 count 值。
PS: 对于多个访问 MySQL 的用户线程 ( COUNT( * ) ) 而言,决定它们各自的结果的因素有几个:
其中 一、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,所以只须要读主键索引页内的行数据,而无需读取额外的溢出页。
欢迎Java工程师朋友们加入Java高级架构进阶: 963944895,群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用本身每一分每一秒的时间来学习提高本身,不要再用"没有时间“来掩饰本身思想上的懒惰!趁年轻,使劲拼,给将来的本身一个交代!